Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"lint-staged": "^10.5.3",
"npm-run-all": "^4.1.5",
"preact": "^10.5.7",
"prettier": "2.1.2",
"prettier": "3.8.1",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"rimraf": "^3.0.2",
Expand Down
61 changes: 44 additions & 17 deletions packages/angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,68 @@ See also the [mono repo README](../../README.md) for more information.

## Installation

Add the `@appsignal/angular` and `@appsignal/javascript` packages to your `package.json`. Then, run `yarn install`/`npm install`.
Add the `@appsignal/angular` and `@appsignal/javascript` packages to your `package.json`. Then, run `npm install`/`yarn install`/`pnpm install`.

You can also add these packages to your `package.json` on the command line:

```
```bash
npm install @appsignal/javascript @appsignal/angular
# Or if you use yarn
yarn add @appsignal/javascript @appsignal/angular
npm install --save @appsignal/javascript @appsignal/angular
# Or if you use pnpm
pnpm install @appsignal/javascript @appsignal/angular
```

## Usage

### `AppsignalErrorHandler`

The default Angular integration is a class that extends the `ErrorHandler` class provided by `@angular/core`. In a new app created using `@angular/cli`, your `app.module.ts` file might include something like this:
The default Angular integration is a class that extends the `ErrorHandler` class provided by `@angular/core`. In a new app created using `@angular/cli`, your `app.config.ts` file might include something like this:

```ts
// app.config.ts
import { type ApplicationConfig, ErrorHandler } from '@angular/core';
import AppSignal from '@appsignal/javascript';
import { createErrorHandlerFactory } from '@appsignal/angular';

const appSignalFactory = () =>
new AppSignal({
key: 'YOUR FRONTEND API KEY',
});

export const appConfig: ApplicationConfig = {
providers: [
{
provide: ErrorHandler,
useFactory: createErrorHandlerFactory(appSignalFactory),
},
],
};
```

This accepts a factory because you **lazy-load** `@appsignal/javascript` only when the first error is logged:

```js
import { ErrorHandler, NgModule } from '@angular/core';
import Appsignal from '@appsignal/javascript';
```ts
// app.config.ts
import { type ApplicationConfig, ErrorHandler } from '@angular/core';
import { createErrorHandlerFactory } from '@appsignal/angular';

const appsignal = new Appsignal({
key: 'YOUR FRONTEND API KEY'
});
const appSignalFactory = () =>
import('@appsignal/javascript').then((m) => {
const AppSignal = m.default;
return new AppSignal({
key: 'YOUR FRONTEND API KEY',
});
});

@NgModule({
// other properties
export const appConfig: ApplicationConfig = {
providers: [
{
provide: ErrorHandler,
useFactory: createErrorHandlerFactory(appsignal)
}
useFactory: createErrorHandlerFactory(appSignalFactory),
},
],
// other properties
})
export class AppModule {}
};
```

## Development
Expand Down
8 changes: 7 additions & 1 deletion packages/angular/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NgZone } from "@angular/core"
import { AppsignalErrorHandler } from "../index"

describe("Angular handleError", () => {
Expand All @@ -19,7 +20,12 @@ describe("Angular handleError", () => {

it("calls AppSignal helper methods", () => {
const err = new Error("test")
const errorHandler = new AppsignalErrorHandler(appsignal)
const errorHandler = new AppsignalErrorHandler(
<NgZone>{
runOutsideAngular: fn => fn()
},
() => appsignal
)

errorHandler.handleError(err)

Expand Down
65 changes: 50 additions & 15 deletions packages/angular/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,63 @@
import type Appsignal from "@appsignal/javascript"
import { ErrorHandler, Injectable } from "@angular/core"
import {
type ErrorHandler,
inject,
Injectable,
ɵisPromise as isPromise,
NgZone
} from "@angular/core"
import { defer, of } from "rxjs"
import { shareReplay } from "rxjs/operators"

// Sync factory: returns an Appsignal instance directly.
type AppsignalFactory = () => Appsignal
// Async loader: returns a Promise, enabling lazy/dynamic import of the Appsignal bundle.
type AppsignalLoader = () => Promise<Appsignal>
// Accepts either form so the consumer can choose between eager and lazy initialization.
type AppsignalFactoryOrLoader = AppsignalFactory | AppsignalLoader

@Injectable()
export class AppsignalErrorHandler extends ErrorHandler {
private _appsignal: Appsignal
export class AppsignalErrorHandler implements ErrorHandler {
// Deferred observable that resolves the Appsignal instance on first subscription
// (i.e. on the first error) and replays the result to all subsequent subscribers,
// so the factory/loader is invoked exactly once regardless of how many errors occur.
private readonly _appsignal$ = defer(() => {
const appsignalOrPromise = this._appSignalFactory()
// Normalise both sync and async return values into an observable.
return isPromise(appsignalOrPromise)
? appsignalOrPromise
: of(appsignalOrPromise)
}).pipe(shareReplay({ bufferSize: 1, refCount: false }))

constructor(appsignal: Appsignal) {
super()
this._appsignal = appsignal
}
constructor(
private readonly _ngZone: NgZone,
// The factory/loader is injected rather than an Appsignal instance directly,
// keeping Appsignal out of the bundle until the first error is actually handled.
private readonly _appSignalFactory: AppsignalFactoryOrLoader
) {}

public handleError(error: any): void {
const span = this._appsignal.createSpan()

span.setError(error).setTags({ framework: "Angular" })
// Run outside Angular's zone to avoid triggering unnecessary change detection
// while waiting for the Appsignal instance to resolve.
this._ngZone.runOutsideAngular(() => {
this._appsignal$.subscribe(appSignal => {
const span = appSignal.createSpan()

this._appsignal.send(span)
span.setError(error).setTags({ framework: "Angular" })

ErrorHandler.prototype.handleError.call(this, error)
appSignal.send(span)
})
})
}
}

export function createErrorHandlerFactory(appsignal: Appsignal): Function {
return function errorHandlerFactory(): AppsignalErrorHandler {
return new AppsignalErrorHandler(appsignal)
// Wraps the factory/loader in an Angular-compatible provider factory,
// so consumers can pass their own lazy import (e.g. `() => import("@appsignal/javascript")`)
// and Appsignal will only be loaded when the first error is caught.
export function createErrorHandlerFactory(
appsignalFactory: AppsignalFactoryOrLoader
): Function {
return function errorHandlerFactory() {
return new AppsignalErrorHandler(inject(NgZone), appsignalFactory)
}
}