Skip to content

Commit 426ce94

Browse files
committed
Remove unnecessary continueTrace calls
1 parent 33fa050 commit 426ce94

File tree

7 files changed

+107
-131
lines changed

7 files changed

+107
-131
lines changed

dev-packages/e2e-tests/test-applications/solidstart/src/routes/users/[id].tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default function User() {
1414
return (
1515
<div>
1616
User ID: {params.id}
17+
<br />
1718
Prefecture: {userData()?.prefecture}
1819
</div>
1920
);

dev-packages/e2e-tests/test-applications/solidstart/tests/errors.server.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ test.describe('server-side errors', () => {
2525
},
2626
],
2727
},
28-
transaction: 'getPrefecture',
28+
transaction: 'GET /server-error',
2929
});
3030
});
3131
});
Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,55 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForTransaction } from '@sentry-internal/test-utils';
3+
import {
4+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
5+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
6+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
7+
} from '@sentry/core';
38

4-
test('sends a server action transaction', async ({ page }) => {
9+
test('sends a server action transaction on pageload', async ({ page }) => {
510
const transactionPromise = waitForTransaction('solidstart', transactionEvent => {
6-
return transactionEvent?.transaction === 'getPrefecture';
11+
return transactionEvent?.transaction === 'GET /users/6';
712
});
813

914
await page.goto('/users/6');
1015

1116
const transaction = await transactionPromise;
1217

13-
expect(transaction).toMatchObject({
14-
transaction: 'getPrefecture',
15-
tags: { runtime: 'node' },
16-
transaction_info: { source: 'url' },
17-
type: 'transaction',
18-
contexts: {
19-
trace: {
20-
op: 'function.server_action',
21-
origin: 'manual',
22-
},
23-
},
18+
expect(transaction.spans).toEqual(
19+
expect.arrayContaining([
20+
expect.objectContaining({
21+
description: 'getPrefecture',
22+
data: {
23+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action',
24+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual',
25+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
26+
},
27+
}),
28+
]),
29+
);
30+
});
31+
32+
test('sends a server action transaction on client navigation', async ({ page }) => {
33+
const transactionPromise = waitForTransaction('solidstart', transactionEvent => {
34+
return transactionEvent?.transaction === 'POST getPrefecture';
2435
});
36+
37+
await page.goto('/');
38+
await page.locator('#navLink').click();
39+
await page.waitForURL('/users/5');
40+
41+
const transaction = await transactionPromise;
42+
43+
expect(transaction.spans).toEqual(
44+
expect.arrayContaining([
45+
expect.objectContaining({
46+
description: 'getPrefecture',
47+
data: {
48+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action',
49+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual',
50+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
51+
},
52+
}),
53+
]),
54+
);
2555
});

packages/solidstart/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
"@sentry/solid": "8.20.0",
7373
"@sentry/types": "8.20.0",
7474
"@sentry/utils": "8.20.0",
75-
"@sentry/vite-plugin": "2.19.0"
75+
"@sentry/vite-plugin": "2.19.0",
76+
"@opentelemetry/instrumentation": "^0.52.1"
7677
},
7778
"devDependencies": {
7879
"@solidjs/router": "^0.13.4",

packages/solidstart/src/server/utils.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@ import { logger } from '@sentry/utils';
33
import type { RequestEvent } from 'solid-js/web';
44
import { DEBUG_BUILD } from '../common/debug-build';
55

6+
/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */
7+
export async function flushIfServerless(): Promise<void> {
8+
const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL;
9+
10+
if (isServerless) {
11+
try {
12+
DEBUG_BUILD && logger.log('Flushing events...');
13+
await flush(2000);
14+
DEBUG_BUILD && logger.log('Done flushing events');
15+
} catch (e) {
16+
DEBUG_BUILD && logger.log('Error while flushing events:\n', e);
17+
}
18+
}
19+
}
20+
621
/**
722
* Takes a request event and extracts traceparent and DSC data
823
* from the `sentry-trace` and `baggage` DSC headers.
@@ -19,21 +34,6 @@ export function getTracePropagationData(event: RequestEvent | undefined): {
1934
return { sentryTrace, baggage };
2035
}
2136

22-
/** Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda ends */
23-
export async function flushIfServerless(): Promise<void> {
24-
const isServerless = !!process.env.LAMBDA_TASK_ROOT || !!process.env.VERCEL;
25-
26-
if (isServerless) {
27-
try {
28-
DEBUG_BUILD && logger.log('Flushing events...');
29-
await flush(2000);
30-
DEBUG_BUILD && logger.log('Done flushing events');
31-
} catch (e) {
32-
DEBUG_BUILD && logger.log('Error while flushing events:\n', e);
33-
}
34-
}
35-
}
36-
3737
/**
3838
* Determines if a thrown "error" is a redirect Response which Solid Start users can throw to redirect to another route.
3939
* see: https://docs.solidjs.com/solid-router/reference/data-apis/response-helpers#redirect

packages/solidstart/src/server/withServerActionInstrumentation.ts

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,38 @@
11
import { SPAN_STATUS_ERROR, handleCallbackErrors } from '@sentry/core';
2-
import {
3-
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
4-
captureException,
5-
continueTrace,
6-
startSpan,
7-
withIsolationScope,
8-
} from '@sentry/node';
9-
import { winterCGRequestToRequestData } from '@sentry/utils';
10-
import { getRequestEvent } from 'solid-js/web';
11-
import { flushIfServerless, getTracePropagationData, isRedirect } from './utils';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, getActiveSpan, spanToJSON, startSpan } from '@sentry/node';
3+
import { flushIfServerless, isRedirect } from './utils';
124

135
/**
14-
* Wraps a server action (functions that use the 'use server' directive) function body with Sentry Error and Performance instrumentation.
6+
* Wraps a server action (functions that use the 'use server' directive)
7+
* function body with Sentry Error and Performance instrumentation.
158
*/
169
export async function withServerActionInstrumentation<A extends (...args: unknown[]) => unknown>(
1710
serverActionName: string,
1811
callback: A,
1912
): Promise<ReturnType<A>> {
20-
return withIsolationScope(isolationScope => {
21-
const event = getRequestEvent();
13+
const activeSpan = getActiveSpan();
2214

23-
if (event && event.request) {
24-
isolationScope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(event.request) });
25-
}
26-
isolationScope.setTransactionName(serverActionName);
15+
if (activeSpan) {
16+
const spanData = spanToJSON(activeSpan).data;
2717

28-
return continueTrace(getTracePropagationData(event), () => instrumentServerAction(serverActionName, callback));
29-
});
30-
}
18+
// In solid start, server function calls are made to `/_server` which doesn't tell us
19+
// a lot. We rewrite the span's route to be that of the sever action name but only
20+
// if the target is `/_server`, otherwise we'd overwrite pageloads on routes that use
21+
// server actions (which are more meaningful, e.g. a request to `GET /users/5` is more
22+
// meaningful than overwriting it with `GET doSomeFunctionCall`).
23+
if (spanData && !spanData['http.route'] && spanData['http.target'] === '/_server') {
24+
activeSpan.setAttribute('http.route', serverActionName);
25+
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
26+
}
27+
}
3128

32-
async function instrumentServerAction<A extends (...args: unknown[]) => unknown>(
33-
name: string,
34-
callback: A,
35-
): Promise<ReturnType<A>> {
3629
try {
3730
return await startSpan(
3831
{
3932
op: 'function.server_action',
40-
name,
41-
forceTransaction: true,
33+
name: serverActionName,
4234
attributes: {
43-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
35+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
4436
},
4537
},
4638
async span => {

packages/solidstart/test/server/withServerActionInstrumentation.test.ts

Lines changed: 26 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ import {
1212
import { NodeClient } from '@sentry/node';
1313
import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter';
1414
import { redirect } from '@solidjs/router';
15-
import { Headers } from 'node-fetch';
1615
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1716

1817
const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => '');
1918
const mockFlush = vi.spyOn(SentryNode, 'flush').mockImplementation(async () => true);
20-
const mockContinueTrace = vi.spyOn(SentryNode, 'continueTrace');
19+
const mockGetActiveSpan = vi.spyOn(SentryNode, 'getActiveSpan');
2120

2221
const mockGetRequestEvent = vi.fn();
2322
vi.mock('solid-js/web', async () => {
@@ -28,6 +27,7 @@ vi.mock('solid-js/web', async () => {
2827
};
2928
});
3029

30+
import { SentrySpan } from '@sentry/core';
3131
import { withServerActionInstrumentation } from '../../src/server';
3232

3333
describe('withServerActionInstrumentation', () => {
@@ -107,53 +107,13 @@ describe('withServerActionInstrumentation', () => {
107107
description: 'getPrefecture',
108108
data: expect.objectContaining({
109109
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action',
110-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
110+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
111111
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual',
112112
}),
113113
}),
114114
);
115115
});
116116

117-
it('calls `continueTrace` with the right sentry-trace and baggage', async () => {
118-
const baggage =
119-
'sentry-environment=qa,sentry-public_key=12345678,sentry-trace_id=4c9b164c5b5f4a0c8db3ce490b935ea8,sentry-sample_rate=1,sentry-sampled=true';
120-
const sentryTrace = '4c9b164c5b5f4a0c8db3ce490b935ea8';
121-
mockGetRequestEvent.mockReturnValue({
122-
request: {
123-
method: 'GET',
124-
url: '/_server',
125-
headers: new Headers([
126-
['sentry-trace', sentryTrace],
127-
['baggage', baggage],
128-
]),
129-
},
130-
});
131-
132-
await serverActionGetPrefecture();
133-
134-
expect(mockContinueTrace).to.toHaveBeenCalledWith(
135-
expect.objectContaining({
136-
sentryTrace,
137-
baggage,
138-
}),
139-
expect.any(Function),
140-
);
141-
});
142-
143-
it('calls `continueTrace` with no sentry-trace or baggage', async () => {
144-
mockGetRequestEvent.mockReturnValue({ request: {} });
145-
146-
await serverActionGetPrefecture();
147-
148-
expect(mockContinueTrace).to.toHaveBeenCalledWith(
149-
expect.objectContaining({
150-
sentryTrace: undefined,
151-
baggage: null,
152-
}),
153-
expect.any(Function),
154-
);
155-
});
156-
157117
it('calls `flush` if lambda', async () => {
158118
vi.stubEnv('LAMBDA_TASK_ROOT', '1');
159119

@@ -168,7 +128,12 @@ describe('withServerActionInstrumentation', () => {
168128
expect(mockFlush).toHaveBeenCalledTimes(1);
169129
});
170130

171-
it('sets a server action transaction name', async () => {
131+
it('sets a server action name on the active span', async () => {
132+
const span = new SentrySpan();
133+
span.setAttribute('http.target', '/_server');
134+
mockGetActiveSpan.mockReturnValue(span);
135+
const mockSpanSetAttribute = vi.spyOn(span, 'setAttribute');
136+
172137
const getPrefecture = async function load() {
173138
return withServerActionInstrumentation('getPrefecture', () => {
174139
return { prefecture: 'Kagoshima' };
@@ -177,39 +142,26 @@ describe('withServerActionInstrumentation', () => {
177142

178143
await getPrefecture();
179144

180-
expect(getIsolationScope().getScopeData().transactionName).toEqual('getPrefecture');
145+
expect(mockGetActiveSpan).to.toHaveBeenCalledTimes(1);
146+
expect(mockSpanSetAttribute).to.toHaveBeenCalledWith('http.route', 'getPrefecture');
147+
expect(mockSpanSetAttribute).to.toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
181148
});
182149

183-
it('sets request data on the isolation scope', async () => {
184-
const baggage =
185-
'sentry-environment=qa,sentry-public_key=12345678,sentry-trace_id=4c9b164c5b5f4a0c8db3ce490b935ea8,sentry-sample_rate=1,sentry-sampled=true';
186-
const sentryTrace = '4c9b164c5b5f4a0c8db3ce490b935ea8';
187-
mockGetRequestEvent.mockReturnValue({
188-
request: {
189-
method: 'POST',
190-
url: '/_server',
191-
headers: new Headers([
192-
['sentry-trace', sentryTrace],
193-
['baggage', baggage],
194-
]),
195-
},
196-
});
150+
it('does not set a server action name if the active span had a non `/_server` target', async () => {
151+
const span = new SentrySpan();
152+
span.setAttribute('http.target', '/users/5');
153+
mockGetActiveSpan.mockReturnValue(span);
154+
const mockSpanSetAttribute = vi.spyOn(span, 'setAttribute');
197155

198-
await serverActionGetPrefecture();
156+
const getPrefecture = async function load() {
157+
return withServerActionInstrumentation('getPrefecture', () => {
158+
return { prefecture: 'Kagoshima' };
159+
});
160+
};
199161

200-
expect(getIsolationScope().getScopeData()).toEqual(
201-
expect.objectContaining({
202-
sdkProcessingMetadata: {
203-
request: {
204-
method: 'POST',
205-
url: '/_server',
206-
headers: {
207-
'sentry-trace': sentryTrace,
208-
baggage,
209-
},
210-
},
211-
},
212-
}),
213-
);
162+
await getPrefecture();
163+
164+
expect(mockGetActiveSpan).to.toHaveBeenCalledTimes(1);
165+
expect(mockSpanSetAttribute).not.toHaveBeenCalled();
214166
});
215167
});

0 commit comments

Comments
 (0)