Skip to content

Commit 65c3b91

Browse files
Support automatic collection of NextJS route errors (#9246)
* Add support for collection of NextJS server-side errors * typo * Remove next dependency * format * format * documentation * Add next as a devDependency for types * documentation --------- Co-authored-by: Christina Holland <[email protected]>
1 parent ec64f4f commit 65c3b91

File tree

12 files changed

+558
-14
lines changed

12 files changed

+558
-14
lines changed

common/api-review/telemetry.api.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,30 @@
44
55
```ts
66

7+
import { AnyValueMap } from '@opentelemetry/api-logs';
78
import { FirebaseApp } from '@firebase/app';
89
import { LoggerProvider } from '@opentelemetry/sdk-logs';
910

1011
// @public
11-
export function captureError(telemetry: Telemetry, error: unknown): void;
12+
export function captureError(telemetry: Telemetry, error: unknown, attributes?: AnyValueMap): void;
1213

1314
// @public
1415
export function flush(telemetry: Telemetry): Promise<void>;
1516

1617
// @public
1718
export function getTelemetry(app?: FirebaseApp): Telemetry;
1819

20+
// @public (undocumented)
21+
export namespace Instrumentation {
22+
// Warning: (ae-forgotten-export) The symbol "InstrumentationOnRequestError" needs to be exported by the entry point index.d.ts
23+
//
24+
// (undocumented)
25+
export type onRequestError = InstrumentationOnRequestError;
26+
}
27+
28+
// @public
29+
export const nextOnRequestError: Instrumentation.onRequestError;
30+
1931
// @public
2032
export interface Telemetry {
2133
app: FirebaseApp;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Project: /docs/reference/js/_project.yaml
2+
Book: /docs/reference/_book.yaml
3+
page_type: reference
4+
5+
{% comment %}
6+
DO NOT EDIT THIS FILE!
7+
This is generated by the JS SDK team, and any local changes will be
8+
overwritten. Changes should be made in the source code at
9+
https://github.com/firebase/firebase-js-sdk
10+
{% endcomment %}
11+
12+
# Instrumentation namespace
13+
<b>Signature:</b>
14+
15+
```typescript
16+
export declare namespace Instrumentation
17+
```
18+
19+
## Type Aliases
20+
21+
| Type Alias | Description |
22+
| --- | --- |
23+
| [onRequestError](./telemetry.instrumentation.md#instrumentationonrequesterror) | |
24+
25+
## Instrumentation.onRequestError
26+
27+
<b>Signature:</b>
28+
29+
```typescript
30+
type onRequestError = InstrumentationOnRequestError;
31+
```

docs-devsite/telemetry.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ https://github.com/firebase/firebase-js-sdk
1818
| <b>function(app, ...)</b> |
1919
| [getTelemetry(app)](./telemetry.md#gettelemetry_cf608e1) | Returns the default [Telemetry](./telemetry.telemetry.md#telemetry_interface) instance that is associated with the provided [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface)<!-- -->. If no instance exists, initializes a new instance with the default settings. |
2020
| <b>function(telemetry, ...)</b> |
21-
| [captureError(telemetry, error)](./telemetry.md#captureerror_7c2d94e) | Enqueues an error to be uploaded to the Firebase Telemetry API. |
21+
| [captureError(telemetry, error, attributes)](./telemetry.md#captureerror_862e6b3) | Enqueues an error to be uploaded to the Firebase Telemetry API. |
2222
| [flush(telemetry)](./telemetry.md#flush_8975134) | Flushes all enqueued telemetry data immediately, instead of waiting for default batching. |
2323

2424
## Interfaces
@@ -27,6 +27,18 @@ https://github.com/firebase/firebase-js-sdk
2727
| --- | --- |
2828
| [Telemetry](./telemetry.telemetry.md#telemetry_interface) | An instance of the Firebase Telemetry SDK.<!-- -->Do not create this instance directly. Instead, use [getTelemetry()](./telemetry.md#gettelemetry_cf608e1)<!-- -->. |
2929

30+
## Namespaces
31+
32+
| Namespace | Description |
33+
| --- | --- |
34+
| [Instrumentation](./telemetry.instrumentation.md#instrumentation_namespace) | |
35+
36+
## Variables
37+
38+
| Variable | Description |
39+
| --- | --- |
40+
| [nextOnRequestError](./telemetry.md#nextonrequesterror) | Automatically report uncaught errors from server routes to Firebase Telemetry. |
41+
3042
## function(app, ...)
3143

3244
### getTelemetry(app) {:#gettelemetry_cf608e1}
@@ -61,22 +73,23 @@ const telemetry = getTelemetry(app);
6173

6274
## function(telemetry, ...)
6375

64-
### captureError(telemetry, error) {:#captureerror_7c2d94e}
76+
### captureError(telemetry, error, attributes) {:#captureerror_862e6b3}
6577

6678
Enqueues an error to be uploaded to the Firebase Telemetry API.
6779

6880
<b>Signature:</b>
6981

7082
```typescript
71-
export declare function captureError(telemetry: Telemetry, error: unknown): void;
83+
export declare function captureError(telemetry: Telemetry, error: unknown, attributes?: AnyValueMap): void;
7284
```
7385

7486
#### Parameters
7587

7688
| Parameter | Type | Description |
7789
| --- | --- | --- |
7890
| telemetry | [Telemetry](./telemetry.telemetry.md#telemetry_interface) | The [Telemetry](./telemetry.telemetry.md#telemetry_interface) instance. |
79-
| error | unknown | the caught exception, typically an |
91+
| error | unknown | The caught exception, typically an |
92+
| attributes | AnyValueMap | = Optional, arbitrary attributes to attach to the error log |
8093

8194
<b>Returns:</b>
8295

@@ -104,3 +117,22 @@ Promise&lt;void&gt;
104117

105118
a promise which is resolved when all flushes are complete
106119

120+
## nextOnRequestError
121+
122+
Automatically report uncaught errors from server routes to Firebase Telemetry.
123+
124+
<b>Signature:</b>
125+
126+
```typescript
127+
nextOnRequestError: Instrumentation.onRequestError
128+
```
129+
130+
### Example
131+
132+
133+
```javascript
134+
// In instrumentation.ts (https://nextjs.org/docs/app/guides/instrumentation):
135+
export { nextOnRequestError as onRequestError } from 'firebase/telemetry'
136+
137+
```
138+

packages/telemetry/api-extractor.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
"enabled": true,
77
"untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.d.ts",
88
"betaTrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>-public.d.ts"
9-
}
9+
},
10+
"bundledPackages": ["next"]
1011
}

packages/telemetry/index.node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424

2525
import { registerTelemetry } from './src/register.node';
2626

27-
console.log('Hi Node.js Users!');
2827
registerTelemetry();
2928

3029
export * from './src/api';
3130
export * from './src/public-types';
31+
export * from './src/next';

packages/telemetry/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ registerTelemetry();
2121

2222
export * from './src/api';
2323
export * from './src/public-types';
24+
export * from './src/next';

packages/telemetry/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@firebase/app": "0.14.2",
5959
"@opentelemetry/sdk-trace-web": "2.1.0",
6060
"@rollup/plugin-json": "6.1.0",
61+
"next": "15.5.2",
6162
"rollup": "2.79.2",
6263
"rollup-plugin-replace": "2.2.0",
6364
"rollup-plugin-typescript2": "0.36.0",

packages/telemetry/src/api.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,34 @@ describe('Top level API', () => {
146146
'logging.googleapis.com/spanId': `my-span`
147147
});
148148
});
149+
150+
it('should propagate custom attributes', () => {
151+
const error = new Error('This is a test error');
152+
error.stack = '...stack trace...';
153+
error.name = 'TestError';
154+
155+
captureError(fakeTelemetry, error, {
156+
strAttr: 'string attribute',
157+
mapAttr: {
158+
boolAttr: true,
159+
numAttr: 2
160+
},
161+
arrAttr: [1, 2, 3]
162+
});
163+
164+
expect(emittedLogs.length).to.equal(1);
165+
const log = emittedLogs[0];
166+
expect(log.attributes).to.deep.equal({
167+
'error.type': 'TestError',
168+
'error.stack': '...stack trace...',
169+
strAttr: 'string attribute',
170+
mapAttr: {
171+
boolAttr: true,
172+
numAttr: 2
173+
},
174+
arrAttr: [1, 2, 3]
175+
});
176+
});
149177
});
150178

151179
describe('flush()', () => {

packages/telemetry/src/api.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,14 @@ export function getTelemetry(app: FirebaseApp = getApp()): Telemetry {
6060
* @public
6161
*
6262
* @param telemetry - The {@link Telemetry} instance.
63-
* @param error - the caught exception, typically an {@link Error}
63+
* @param error - The caught exception, typically an {@link Error}
64+
* @param attributes = Optional, arbitrary attributes to attach to the error log
6465
*/
65-
export function captureError(telemetry: Telemetry, error: unknown): void {
66+
export function captureError(
67+
telemetry: Telemetry,
68+
error: unknown,
69+
attributes?: AnyValueMap
70+
): void {
6671
const logger = telemetry.loggerProvider.getLogger('error-logger');
6772

6873
const activeSpanContext = trace.getActiveSpan()?.spanContext();
@@ -77,30 +82,35 @@ export function captureError(telemetry: Telemetry, error: unknown): void {
7782
}
7883
}
7984

85+
const customAttributes = attributes || {};
86+
8087
if (error instanceof Error) {
8188
logger.emit({
8289
severityNumber: SeverityNumber.ERROR,
8390
body: error.message,
8491
attributes: {
8592
'error.type': error.name || 'Error',
8693
'error.stack': error.stack || 'No stack trace available',
87-
...traceAttributes
94+
...traceAttributes,
95+
...customAttributes
8896
}
8997
});
9098
} else if (typeof error === 'string') {
9199
logger.emit({
92100
severityNumber: SeverityNumber.ERROR,
93101
body: error,
94102
attributes: {
95-
...traceAttributes
103+
...traceAttributes,
104+
...customAttributes
96105
}
97106
});
98107
} else {
99108
logger.emit({
100109
severityNumber: SeverityNumber.ERROR,
101110
body: `Unknown error type: ${typeof error}`,
102111
attributes: {
103-
...traceAttributes
112+
...traceAttributes,
113+
...customAttributes
104114
}
105115
});
106116
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import sinonChai from 'sinon-chai';
20+
import chaiAsPromised from 'chai-as-promised';
21+
import { restore, stub } from 'sinon';
22+
import * as app from '@firebase/app';
23+
import * as telemetry from './api';
24+
import { FirebaseApp } from '@firebase/app';
25+
import { Telemetry } from './public-types';
26+
import { nextOnRequestError } from './next';
27+
28+
use(sinonChai);
29+
use(chaiAsPromised);
30+
31+
describe('nextOnRequestError', () => {
32+
let getTelemetryStub: sinon.SinonStub;
33+
let captureErrorStub: sinon.SinonStub;
34+
let fakeApp: FirebaseApp;
35+
let fakeTelemetry: Telemetry;
36+
37+
beforeEach(() => {
38+
fakeApp = {} as FirebaseApp;
39+
fakeTelemetry = {} as Telemetry;
40+
41+
stub(app, 'getApp').returns(fakeApp);
42+
getTelemetryStub = stub(telemetry, 'getTelemetry').returns(fakeTelemetry);
43+
captureErrorStub = stub(telemetry, 'captureError');
44+
});
45+
46+
afterEach(() => {
47+
restore();
48+
});
49+
50+
it('should capture errors with correct attributes', async () => {
51+
const error = new Error('test error');
52+
const errorRequest = {
53+
path: '/test-path?some=param',
54+
method: 'GET',
55+
headers: {}
56+
};
57+
const errorContext: {
58+
routerKind: 'Pages Router';
59+
routePath: string;
60+
routeType: 'render';
61+
revalidateReason: undefined;
62+
} = {
63+
routerKind: 'Pages Router',
64+
routePath: '/test-path',
65+
routeType: 'render',
66+
revalidateReason: undefined
67+
};
68+
69+
await nextOnRequestError(error, errorRequest, errorContext);
70+
71+
expect(getTelemetryStub).to.have.been.calledOnceWith(fakeApp);
72+
expect(captureErrorStub).to.have.been.calledOnceWith(fakeTelemetry, error, {
73+
'nextjs_path': '/test-path?some=param',
74+
'nextjs_method': 'GET',
75+
'nextjs_router_kind': 'Pages Router',
76+
'nextjs_route_path': '/test-path',
77+
'nextjs_route_type': 'render'
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)