Skip to content

Commit f94f49b

Browse files
feat(crashlytics): collect Angular context when calling recordError directly (#9456)
* Collect Angular context when calling recordError directly * Update attribute priority --------- Co-authored-by: abrook <abrook@google.com>
1 parent a0a50d1 commit f94f49b

File tree

5 files changed

+85
-26
lines changed

5 files changed

+85
-26
lines changed

packages/crashlytics/src/angular/index.test.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
BrowserTestingModule,
3838
platformBrowserTesting
3939
} from '@angular/platform-browser/testing';
40+
import { CrashlyticsService } from '../service';
4041

4142
use(sinonChai);
4243
use(chaiAsPromised);
@@ -88,33 +89,31 @@ describe('FirebaseErrorHandler', () => {
8889
});
8990

9091
it('should log the error to the console', async () => {
91-
await router.navigate(['/static-route']);
92-
9392
const testError = new Error('Test error message');
9493
errorHandler.handleError(testError);
9594
expect(getCrashlyticsStub).to.have.been.called;
96-
expect(recordErrorStub).to.have.been.calledWith(
97-
fakeCrashlytics,
98-
testError,
99-
{
100-
'angular_route_path': '/static-route'
101-
}
102-
);
95+
expect(recordErrorStub).to.have.been.calledWith(fakeCrashlytics, testError);
10396
});
10497

105-
it('should remove dynamic content from route', async () => {
106-
await router.navigate(['/dynamic/my-name/route']);
98+
describe('frameworkAttributesProvider', () => {
99+
it('should report framework attributes', async () => {
100+
await router.navigate(['/static-route']);
107101

108-
const testError = new Error('Test error message');
109-
errorHandler.handleError(testError);
110-
expect(recordErrorStub).to.have.been.called;
111-
expect(recordErrorStub).to.have.been.calledWith(
112-
fakeCrashlytics,
113-
testError,
114-
{
115-
// eslint-disable-next-line camelcase
116-
angular_route_path: '/dynamic/:id/route'
117-
}
118-
);
102+
expect(
103+
(fakeCrashlytics as CrashlyticsService).frameworkAttributesProvider!()
104+
).to.deep.equal({
105+
'angular_route_path': '/static-route'
106+
});
107+
});
108+
109+
it('should remove dynamic content from route', async () => {
110+
await router.navigate(['/dynamic/my-name/route']);
111+
112+
expect(
113+
(fakeCrashlytics as CrashlyticsService).frameworkAttributesProvider!()
114+
).to.deep.equal({
115+
'angular_route_path': '/dynamic/:id/route'
116+
});
117+
});
119118
});
120119
});

packages/crashlytics/src/angular/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { registerCrashlytics } from '../register';
2323
import { recordError, getCrashlytics } from '../api';
2424
import { Crashlytics, CrashlyticsOptions } from '../public-types';
2525
import { FirebaseApp } from '@firebase/app';
26+
import { CrashlyticsService } from '../service';
2627

2728
registerCrashlytics();
2829

@@ -83,14 +84,22 @@ export class FirebaseErrorHandler implements ErrorHandler {
8384

8485
constructor(app: FirebaseApp, crashlyticsOptions?: CrashlyticsOptions) {
8586
this.crashlytics = getCrashlytics(app, crashlyticsOptions);
87+
(this.crashlytics as CrashlyticsService).frameworkAttributesProvider = () =>
88+
this.getAttributes();
8689
}
8790

8891
handleError(error: unknown): void {
89-
const attributes = {
92+
recordError(this.crashlytics, error);
93+
}
94+
95+
/**
96+
* Returns a record of framework-specific attributes based on the current application state to be
97+
* attached to the error log.
98+
*/
99+
private getAttributes(): Record<string, string> {
100+
return {
90101
'angular_route_path': this.getSafeRoutePath(this.router)
91102
};
92-
93-
recordError(this.crashlytics, error, attributes);
94103
}
95104

96105
/**

packages/crashlytics/src/api.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ describe('Top level API', () => {
128128
value: originalCrypto,
129129
writable: true
130130
});
131+
delete AUTO_CONSTANTS.appVersion;
131132
});
132133

133134
describe('getCrashlytics()', () => {
@@ -338,6 +339,31 @@ describe('Top level API', () => {
338339
});
339340
});
340341

342+
it('should retrieve framework-specific attributes', () => {
343+
const error = new Error('This is a test error');
344+
error.stack = '...stack trace...';
345+
error.name = 'TestError';
346+
347+
(fakeCrashlytics as CrashlyticsService).frameworkAttributesProvider =
348+
() => ({
349+
'framework_attr1': 'framework attribute #1',
350+
'framework_attr2': 'framework attribute #2'
351+
});
352+
353+
recordError(fakeCrashlytics, error);
354+
355+
expect(emittedLogs.length).to.equal(1);
356+
const log = emittedLogs[0];
357+
expect(log.attributes).to.deep.equal({
358+
'error.type': 'TestError',
359+
'error.stack': '...stack trace...',
360+
[LOG_ENTRY_ATTRIBUTE_KEYS.APP_VERSION]: 'unset',
361+
'framework_attr1': 'framework attribute #1',
362+
'framework_attr2': 'framework attribute #2',
363+
[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID]: MOCK_SESSION_ID
364+
});
365+
});
366+
341367
describe('Session Metadata', () => {
342368
it('should retrieve existing session ID from sessionStorage', () => {
343369
storage[CRASHLYTICS_SESSION_ID_KEY] = 'existing-session-id';

packages/crashlytics/src/api.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,15 @@ export function recordError(
8383
const logger = (crashlytics as CrashlyticsInternal).loggerProvider.getLogger(
8484
'error-logger'
8585
);
86-
const customAttributes = attributes || {};
86+
const customAttributes: AnyValueMap = {};
87+
88+
// Add framework-specific metadata
89+
const frameworkAttributesProvider = (crashlytics as CrashlyticsService)
90+
.frameworkAttributesProvider;
91+
if (frameworkAttributesProvider) {
92+
const frameworkAttributes = frameworkAttributesProvider();
93+
Object.assign(customAttributes, frameworkAttributes);
94+
}
8795

8896
// Add trace metadata
8997
const activeSpanContext = trace.getActiveSpan()?.spanContext();
@@ -107,6 +115,12 @@ export function recordError(
107115
customAttributes[LOG_ENTRY_ATTRIBUTE_KEYS.SESSION_ID] = sessionId;
108116
}
109117

118+
// Merge in any additional attributes. Explicitly provided attributes take precedence over
119+
// automatically added attributes.
120+
if (attributes) {
121+
Object.assign(customAttributes, attributes);
122+
}
123+
110124
if (error instanceof Error) {
111125
logger.emit({
112126
severityNumber: SeverityNumber.ERROR,

packages/crashlytics/src/service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { LoggerProvider } from '@opentelemetry/sdk-logs';
2121

2222
export class CrashlyticsService implements Crashlytics, _FirebaseService {
2323
private _options?: CrashlyticsOptions;
24+
private _frameworkAttributesProvider?: () => Record<string, string>;
2425

2526
constructor(public app: FirebaseApp, public loggerProvider: LoggerProvider) {}
2627

@@ -35,4 +36,14 @@ export class CrashlyticsService implements Crashlytics, _FirebaseService {
3536
get options(): CrashlyticsOptions | undefined {
3637
return this._options;
3738
}
39+
40+
get frameworkAttributesProvider():
41+
| (() => Record<string, string>)
42+
| undefined {
43+
return this._frameworkAttributesProvider;
44+
}
45+
46+
set frameworkAttributesProvider(provider: () => Record<string, string>) {
47+
this._frameworkAttributesProvider = provider;
48+
}
3849
}

0 commit comments

Comments
 (0)