Skip to content

Commit 69c76d1

Browse files
authored
fix(rum-angular): capture angular navigation errors (#1662)
1 parent bf696ae commit 69c76d1

File tree

7 files changed

+126
-18
lines changed

7 files changed

+126
-18
lines changed

docs/reference/agent-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ apm.addFilter(function (payload) {
130130
```
131131

132132
::::{note}
133-
The payload will be dropped if one of the filters return a falsy value.
133+
The payload will be dropped if one of the filters return a falsy value or if after applying all filters the transactions and errors arrays are empty.
134134
::::
135135

136136

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@
195195
"@elastic/apm-rum-vue": "file:packages/rum-vue"
196196
},
197197
"engines": {
198-
"node": ">=12 <=16",
199-
"npm": "6"
198+
"node": ">=20.0.0"
200199
}
201200
}

packages/rum-angular/src/apm.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export class ApmService {
5959
}
6060

6161
observe() {
62+
const apm = this.apm
6263
let transaction
6364
this.router.events.subscribe(event => {
6465
const eventName = event.toString()
@@ -69,6 +70,11 @@ export class ApmService {
6970
canReuse: true
7071
})
7172
} else if (eventName.indexOf('NavigationError') >= 0) {
73+
// @ts-expect-error - error prop not defined in types for this angular version
74+
const err = event.error
75+
if (err) {
76+
apm.captureError(err)
77+
}
7278
transaction && transaction.detectFinish()
7379
} else if (eventName.indexOf('NavigationEnd') >= 0) {
7480
if (!transaction) {

packages/rum-angular/test/specs/apm.service.spec.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
*/
2525

2626
import { TestBed, ComponentFixture } from '@angular/core/testing'
27-
import { NgModule, Component } from '@angular/core'
28-
import { Routes, Router } from '@angular/router'
27+
import { NgModule, Component, Injectable } from '@angular/core'
28+
import { Routes, Router, Resolve } from '@angular/router'
2929
import { RouterTestingModule } from '@angular/router/testing'
3030
import { Location } from '@angular/common'
3131
import { ApmBase } from '@elastic/apm-rum'
@@ -77,11 +77,25 @@ class SlugComponent {}
7777
})
7878
class AppComponent {}
7979

80+
@Injectable()
81+
class ResolveBogus implements Resolve<any> {
82+
resolve() {
83+
throw new Error('Method not implemented.')
84+
}
85+
}
86+
8087
const routes: Routes = [
8188
{ path: '', redirectTo: 'home', pathMatch: 'full' },
8289
{ path: 'home', component: HomeComponent },
8390
{ path: 'lazy', loadChildren: () => LazyModule },
84-
{ path: 'slug/:id', component: SlugComponent }
91+
{ path: 'slug/:id', component: SlugComponent },
92+
{
93+
path: 'error-route',
94+
component: SlugComponent,
95+
resolve: {
96+
data: ResolveBogus
97+
}
98+
}
8599
]
86100

87101
describe('ApmService', () => {
@@ -95,7 +109,7 @@ describe('ApmService', () => {
95109
TestBed.configureTestingModule({
96110
imports: [ApmModule, RouterTestingModule.withRoutes(routes)],
97111
declarations: [HomeComponent, AppComponent, SlugComponent],
98-
providers: [ApmService]
112+
providers: [ApmService, ResolveBogus]
99113
}).compileComponents()
100114

101115
TestBed.overrideProvider(APM, {
@@ -248,5 +262,34 @@ describe('ApmService', () => {
248262
})
249263
})
250264
})
265+
266+
it('should capture navigation errors', done => {
267+
spyOn(service.apm, 'startTransaction').and.callThrough()
268+
spyOn(service.apm, 'captureError')
269+
const tr = service.apm.getCurrentTransaction()
270+
271+
fixture.ngZone.run(() => {
272+
router.navigate(['/error-route']).then(
273+
() => {
274+
throw new Error('navigation should fail')
275+
},
276+
navError => {
277+
expect(location.path()).toBe('/')
278+
expect(service.apm.startTransaction).toHaveBeenCalledWith(
279+
'/error-route',
280+
'route-change',
281+
{
282+
managed: true,
283+
canReuse: true
284+
}
285+
)
286+
expect(service.apm.captureError).toHaveBeenCalledWith(navError)
287+
expect(tr.name).toEqual('/error-route')
288+
289+
done()
290+
}
291+
)
292+
})
293+
})
251294
})
252295
})

packages/rum-core/src/common/config-service.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import { getCurrentScript, setLabel, merge, extend, isUndefined } from './utils'
2727
import EventHandler from './event-handler'
2828
import { CONFIG_CHANGE, LOCAL_CONFIG_KEY } from './constants'
29+
import { __DEV__ } from '../state'
2930

3031
function getConfigFromScript() {
3132
var script = getCurrentScript()
@@ -124,11 +125,23 @@ class Config {
124125
}
125126

126127
applyFilters(data) {
127-
for (var i = 0; i < this.filters.length; i++) {
128-
data = this.filters[i](data)
129-
if (!data) {
128+
try {
129+
for (var i = 0; i < this.filters.length; i++) {
130+
data = this.filters[i](data)
131+
if (!data) {
132+
return
133+
}
134+
}
135+
data.transactions = data.transactions.filter(t => t)
136+
data.errors = data.errors.filter(e => e)
137+
if (data.transactions.length === 0 && data.errors.length === 0) {
130138
return
131139
}
140+
} catch (e) {
141+
if (__DEV__) {
142+
console.log('[Elastic APM] Error applying filters for RUM payload', e)
143+
}
144+
return
132145
}
133146
return data
134147
}

packages/rum-core/test/common/config-service.spec.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,29 +68,54 @@ describe('ConfigService', function () {
6868
})
6969

7070
it('should addFilter correctly', function () {
71+
var rawPayload = { transactions: [{}], errors: [{}] }
72+
var filterPayload1 = { transactions: [{}, {}], errors: [{}, {}] }
73+
var filterPayload2 = { transactions: [{}, {}, {}], errors: [{}, {}, {}] }
7174
expect(function () {
7275
configService.addFilter('test')
7376
}).toThrow()
7477

7578
configService.addFilter(function (testArg) {
76-
expect(testArg).toBe('hamid-test')
77-
return 'hamid-test-1'
79+
expect(testArg).toBe(rawPayload)
80+
return filterPayload1
7881
})
7982

8083
configService.addFilter(function (testArg) {
81-
expect(testArg).toBe('hamid-test-1')
82-
return 'hamid-test-2'
84+
expect(testArg).toBe(filterPayload1)
85+
return filterPayload2
8386
})
8487

85-
var result = configService.applyFilters('hamid-test')
86-
expect(result).toBe('hamid-test-2')
88+
var result = configService.applyFilters(rawPayload)
89+
expect(result).toBe(filterPayload2)
8790

8891
configService.addFilter(function () {})
8992
configService.addFilter(function () {
9093
throw new Error('Out of reach!')
9194
})
9295

93-
result = configService.applyFilters('hamid-test')
96+
result = configService.applyFilters(rawPayload)
97+
expect(result).toBeUndefined()
98+
})
99+
100+
it('should applyFilters without throwing', function () {
101+
var rawPayload = { transactions: [{}], errors: [{}] }
102+
103+
configService.addFilter(function () {
104+
throw new Error('Should now throw!')
105+
})
106+
107+
var result = configService.applyFilters(rawPayload)
108+
expect(result).toBeUndefined()
109+
})
110+
111+
it('should applyFilters and filter empty payloads', function () {
112+
var rawPayload = { transactions: [{}], errors: [{}] }
113+
114+
configService.addFilter(function () {
115+
return { transactions: [], errors: [] }
116+
})
117+
118+
var result = configService.applyFilters(rawPayload)
94119
expect(result).toBeUndefined()
95120
})
96121

packages/rum/src/index.d.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,29 @@ interface ServiceFactory {
187187
}
188188

189189
type FilterFn = (payload: Payload) => Payload | boolean | void
190-
type Payload = { [key: string]: any }
190+
type SpanPayload = {
191+
id: string
192+
type: string
193+
name: string
194+
}
195+
type TransactionPayload = {
196+
id: string
197+
type: string
198+
name: string
199+
spans: Partial<SpanPayload>[]
200+
}
201+
type ErrorPayload = {
202+
id: string
203+
exception: {
204+
type: string
205+
message: string
206+
}
207+
}
208+
type Payload = {
209+
transactions: Partial<TransactionPayload>[]
210+
errors: Partial<ErrorPayload>[]
211+
[key: string]: any
212+
}
191213

192214
type TaskId = string | number
193215
type LabelValue = string | number | boolean

0 commit comments

Comments
 (0)