Skip to content

Commit 744e6e9

Browse files
committed
fix: Resolve merge conflicts with develop
- Keep both Spotlight proxy functions and new waitForMetric function - Export all functions from index.ts
2 parents 0041135 + 4a5ba93 commit 744e6e9

File tree

34 files changed

+1235
-158
lines changed

34 files changed

+1235
-158
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7-
Work in this release was contributed by @xgedev, @Mohataseem89 and @sebws. Thank you for your contributions!
7+
Work in this release was contributed by @xgedev, @Mohataseem89, @sebws, and @G-Rath. Thank you for your contributions!
88

99
- ref(nextjs): Drop `resolve` dependency from the Next.js SDK ([#18618](https://github.com/getsentry/sentry-javascript/pull/18618))
1010

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use client';
2+
3+
import * as Sentry from '@sentry/nextjs';
4+
5+
export default function Page() {
6+
const handleClick = async () => {
7+
Sentry.metrics.count('test.page.count', 1, {
8+
attributes: {
9+
page: '/metrics',
10+
'random.attribute': 'Apples',
11+
},
12+
});
13+
Sentry.metrics.distribution('test.page.distribution', 100, {
14+
attributes: {
15+
page: '/metrics',
16+
'random.attribute': 'Manzanas',
17+
},
18+
});
19+
Sentry.metrics.gauge('test.page.gauge', 200, {
20+
attributes: {
21+
page: '/metrics',
22+
'random.attribute': 'Mele',
23+
},
24+
});
25+
await fetch('/metrics/route-handler');
26+
};
27+
28+
return (
29+
<div>
30+
<h1>Metrics page</h1>
31+
<button onClick={handleClick}>Emit</button>
32+
</div>
33+
);
34+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/nextjs';
2+
3+
export const GET = async () => {
4+
Sentry.metrics.count('test.route.handler.count', 1, {
5+
attributes: {
6+
endpoint: '/metrics/route-handler',
7+
'random.attribute': 'Potatoes',
8+
},
9+
});
10+
Sentry.metrics.distribution('test.route.handler.distribution', 100, {
11+
attributes: {
12+
endpoint: '/metrics/route-handler',
13+
'random.attribute': 'Patatas',
14+
},
15+
});
16+
Sentry.metrics.gauge('test.route.handler.gauge', 200, {
17+
attributes: {
18+
endpoint: '/metrics/route-handler',
19+
'random.attribute': 'Patate',
20+
},
21+
});
22+
return Response.json({ message: 'Bueno' });
23+
};
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForMetric } from '@sentry-internal/test-utils';
3+
4+
test('Should emit metrics from server and client', async ({ request, page }) => {
5+
const clientCountPromise = waitForMetric('nextjs-16', async metric => {
6+
return metric.name === 'test.page.count';
7+
});
8+
9+
const clientDistributionPromise = waitForMetric('nextjs-16', async metric => {
10+
return metric.name === 'test.page.distribution';
11+
});
12+
13+
const clientGaugePromise = waitForMetric('nextjs-16', async metric => {
14+
return metric.name === 'test.page.gauge';
15+
});
16+
17+
const serverCountPromise = waitForMetric('nextjs-16', async metric => {
18+
return metric.name === 'test.route.handler.count';
19+
});
20+
21+
const serverDistributionPromise = waitForMetric('nextjs-16', async metric => {
22+
return metric.name === 'test.route.handler.distribution';
23+
});
24+
25+
const serverGaugePromise = waitForMetric('nextjs-16', async metric => {
26+
return metric.name === 'test.route.handler.gauge';
27+
});
28+
29+
await page.goto('/metrics');
30+
await page.getByText('Emit').click();
31+
const clientCount = await clientCountPromise;
32+
const clientDistribution = await clientDistributionPromise;
33+
const clientGauge = await clientGaugePromise;
34+
const serverCount = await serverCountPromise;
35+
const serverDistribution = await serverDistributionPromise;
36+
const serverGauge = await serverGaugePromise;
37+
38+
expect(clientCount).toMatchObject({
39+
timestamp: expect.any(Number),
40+
trace_id: expect.any(String),
41+
span_id: expect.any(String),
42+
name: 'test.page.count',
43+
type: 'counter',
44+
value: 1,
45+
attributes: {
46+
page: { value: '/metrics', type: 'string' },
47+
'random.attribute': { value: 'Apples', type: 'string' },
48+
'sentry.environment': { value: 'qa', type: 'string' },
49+
'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
50+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
51+
},
52+
});
53+
54+
expect(clientDistribution).toMatchObject({
55+
timestamp: expect.any(Number),
56+
trace_id: expect.any(String),
57+
span_id: expect.any(String),
58+
name: 'test.page.distribution',
59+
type: 'distribution',
60+
value: 100,
61+
attributes: {
62+
page: { value: '/metrics', type: 'string' },
63+
'random.attribute': { value: 'Manzanas', type: 'string' },
64+
'sentry.environment': { value: 'qa', type: 'string' },
65+
'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
66+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
67+
},
68+
});
69+
70+
expect(clientGauge).toMatchObject({
71+
timestamp: expect.any(Number),
72+
trace_id: expect.any(String),
73+
span_id: expect.any(String),
74+
name: 'test.page.gauge',
75+
type: 'gauge',
76+
value: 200,
77+
attributes: {
78+
page: { value: '/metrics', type: 'string' },
79+
'random.attribute': { value: 'Mele', type: 'string' },
80+
'sentry.environment': { value: 'qa', type: 'string' },
81+
'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
82+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
83+
},
84+
});
85+
86+
expect(serverCount).toMatchObject({
87+
timestamp: expect.any(Number),
88+
trace_id: expect.any(String),
89+
name: 'test.route.handler.count',
90+
type: 'counter',
91+
value: 1,
92+
attributes: {
93+
'server.address': { value: expect.any(String), type: 'string' },
94+
'random.attribute': { value: 'Potatoes', type: 'string' },
95+
endpoint: { value: '/metrics/route-handler', type: 'string' },
96+
'sentry.environment': { value: 'qa', type: 'string' },
97+
'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
98+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
99+
},
100+
});
101+
102+
expect(serverDistribution).toMatchObject({
103+
timestamp: expect.any(Number),
104+
trace_id: expect.any(String),
105+
name: 'test.route.handler.distribution',
106+
type: 'distribution',
107+
value: 100,
108+
attributes: {
109+
'server.address': { value: expect.any(String), type: 'string' },
110+
'random.attribute': { value: 'Patatas', type: 'string' },
111+
endpoint: { value: '/metrics/route-handler', type: 'string' },
112+
'sentry.environment': { value: 'qa', type: 'string' },
113+
'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
114+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
115+
},
116+
});
117+
118+
expect(serverGauge).toMatchObject({
119+
timestamp: expect.any(Number),
120+
trace_id: expect.any(String),
121+
name: 'test.route.handler.gauge',
122+
type: 'gauge',
123+
value: 200,
124+
attributes: {
125+
'server.address': { value: expect.any(String), type: 'string' },
126+
'random.attribute': { value: 'Patate', type: 'string' },
127+
endpoint: { value: '/metrics/route-handler', type: 'string' },
128+
'sentry.environment': { value: 'qa', type: 'string' },
129+
'sentry.sdk.name': { value: 'sentry.javascript.nextjs', type: 'string' },
130+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
131+
},
132+
});
133+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as Sentry from '@sentry/node';
2+
import { generateText } from 'ai';
3+
4+
// Custom mock model that doesn't set modelId initially (simulates late model ID setting)
5+
// This tests that the op is correctly set even when model ID is not available at span start.
6+
// The span name update (e.g., 'generate_text gpt-4') is skipped when model ID is missing.t
7+
class LateModelIdMock {
8+
specificationVersion = 'v1';
9+
provider = 'late-model-provider';
10+
// modelId is intentionally undefined initially to simulate late setting
11+
modelId = undefined;
12+
defaultObjectGenerationMode = 'json';
13+
14+
async doGenerate() {
15+
// Model ID is only "available" during generation, not at span start
16+
this.modelId = 'late-mock-model-id';
17+
18+
return {
19+
rawCall: { rawPrompt: null, rawSettings: {} },
20+
finishReason: 'stop',
21+
usage: { promptTokens: 5, completionTokens: 10 },
22+
text: 'Response from late model!',
23+
};
24+
}
25+
}
26+
27+
async function run() {
28+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
29+
await generateText({
30+
model: new LateModelIdMock(),
31+
prompt: 'Test prompt for late model ID',
32+
});
33+
});
34+
}
35+
36+
run();

dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,4 +699,40 @@ describe('Vercel AI integration', () => {
699699
expect(errorEvent!.contexts!.trace!.span_id).toBe(transactionEvent!.contexts!.trace!.span_id);
700700
});
701701
});
702+
703+
createEsmAndCjsTests(__dirname, 'scenario-late-model-id.mjs', 'instrument.mjs', (createRunner, test) => {
704+
test('sets op correctly even when model ID is not available at span start', async () => {
705+
const expectedTransaction = {
706+
transaction: 'main',
707+
spans: expect.arrayContaining([
708+
// The generateText span should have the correct op even though model ID was not available at span start
709+
expect.objectContaining({
710+
description: 'generateText',
711+
op: 'gen_ai.invoke_agent',
712+
origin: 'auto.vercelai.otel',
713+
status: 'ok',
714+
data: expect.objectContaining({
715+
'sentry.op': 'gen_ai.invoke_agent',
716+
'sentry.origin': 'auto.vercelai.otel',
717+
'gen_ai.operation.name': 'ai.generateText',
718+
}),
719+
}),
720+
// The doGenerate span - name stays as 'generateText.doGenerate' since model ID is missing
721+
expect.objectContaining({
722+
description: 'generateText.doGenerate',
723+
op: 'gen_ai.generate_text',
724+
origin: 'auto.vercelai.otel',
725+
status: 'ok',
726+
data: expect.objectContaining({
727+
'sentry.op': 'gen_ai.generate_text',
728+
'sentry.origin': 'auto.vercelai.otel',
729+
'gen_ai.operation.name': 'ai.generateText.doGenerate',
730+
}),
731+
}),
732+
]),
733+
};
734+
735+
await createRunner().expect({ transaction: expectedTransaction }).start().completed();
736+
});
737+
});
702738
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core';
2+
import * as Sentry from '@sentry/node';
3+
4+
function bufferedLoggingTransport(_options: BaseTransportOptions): Transport {
5+
const bufferedEnvelopes: Envelope[] = [];
6+
7+
return {
8+
send(envelope: Envelope): Promise<TransportMakeRequestResponse> {
9+
bufferedEnvelopes.push(envelope);
10+
return Promise.resolve({ statusCode: 200 });
11+
},
12+
flush(_timeout?: number): PromiseLike<boolean> {
13+
// Print envelopes once flushed to verify they were sent.
14+
for (const envelope of bufferedEnvelopes.splice(0, bufferedEnvelopes.length)) {
15+
// eslint-disable-next-line no-console
16+
console.log(JSON.stringify(envelope));
17+
}
18+
19+
return Promise.resolve(true);
20+
},
21+
};
22+
}
23+
24+
Sentry.init({
25+
dsn: 'https://[email protected]/1337',
26+
transport: bufferedLoggingTransport,
27+
});
28+
29+
Sentry.captureMessage('SIGTERM flush message');
30+
31+
// Signal that we're ready to receive SIGTERM.
32+
// eslint-disable-next-line no-console
33+
console.log('READY');
34+
35+
// Keep the process alive so the integration test can send SIGTERM.
36+
setInterval(() => undefined, 1_000);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { afterAll, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('flushes buffered events when SIGTERM is received on Vercel', async () => {
9+
const runner = createRunner(__dirname, 'scenario.ts')
10+
.withEnv({ VERCEL: '1' })
11+
.expect({
12+
event: {
13+
message: 'SIGTERM flush message',
14+
},
15+
})
16+
.start();
17+
18+
// Wait for the scenario to signal it's ready (SIGTERM handler is registered).
19+
const waitForReady = async (): Promise<void> => {
20+
const maxWait = 10_000;
21+
const start = Date.now();
22+
while (Date.now() - start < maxWait) {
23+
if (runner.getLogs().some(line => line.includes('READY'))) {
24+
return;
25+
}
26+
await new Promise<void>(resolve => setTimeout(resolve, 50));
27+
}
28+
throw new Error('Timed out waiting for scenario to be ready');
29+
};
30+
31+
await waitForReady();
32+
33+
runner.sendSignal('SIGTERM');
34+
35+
await runner.completed();
36+
37+
// Check that the child didn't crash (it may be killed by the runner after completion).
38+
expect(runner.getLogs().join('\n')).not.toMatch(/Error starting child process/i);
39+
});

dev-packages/node-integration-tests/utils/runner.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type StartResult = {
172172
childHasExited(): boolean;
173173
getLogs(): string[];
174174
getPort(): number | undefined;
175+
sendSignal(signal: NodeJS.Signals): void;
175176
makeRequest<T>(
176177
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
177178
path: string,
@@ -668,6 +669,9 @@ export function createRunner(...paths: string[]) {
668669
getPort(): number | undefined {
669670
return scenarioServerPort;
670671
},
672+
sendSignal(signal: NodeJS.Signals): void {
673+
child?.kill(signal);
674+
},
671675
makeRequest: async function <T>(
672676
method: 'get' | 'post' | 'put' | 'delete' | 'patch',
673677
path: string,

0 commit comments

Comments
 (0)