Skip to content

Commit caa1316

Browse files
Lms24AbhiPrasad
andauthored
feat(cloudflare): Read SENTRY_RELEASE from env (#16201)
This PR enables reading the `SENTRY_RELEASE` variable from the CF `env` that users should pass to their `withSentry` worker wrapper. This is quite similar to how we'd usually access env variables in Node-based SDKs. We need this for uploading release-based source maps for CF worker functions being bundled and deployed by wrangler. see getsentry/sentry-wizard#824 (comment) ref https://linear.app/getsentry/issue/WIZARD-36/improve-sentrywizard-i-sourcemaps-for-cloudflarewrangler --------- Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent a49c946 commit caa1316

File tree

5 files changed

+195
-3
lines changed

5 files changed

+195
-3
lines changed

packages/cloudflare/src/durableobject.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { DurableObject } from 'cloudflare:workers';
1313
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
1414
import type { CloudflareOptions } from './client';
1515
import { isInstrumented, markAsInstrumented } from './instrument';
16+
import { getFinalOptions } from './options';
1617
import { wrapRequestHandler } from './request';
1718
import { init } from './sdk';
1819

@@ -140,7 +141,7 @@ export function instrumentDurableObjectWithSentry<E, T extends DurableObject<E>>
140141
construct(target, [context, env]) {
141142
setAsyncLocalStorageAsyncContextStrategy();
142143

143-
const options = optionsCallback(env);
144+
const options = getFinalOptions(optionsCallback(env), env);
144145

145146
const obj = new target(context, env);
146147

packages/cloudflare/src/handler.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
1010
import type { CloudflareOptions } from './client';
1111
import { isInstrumented, markAsInstrumented } from './instrument';
12+
import { getFinalOptions } from './options';
1213
import { wrapRequestHandler } from './request';
1314
import { addCloudResourceContext } from './scope-utils';
1415
import { init } from './sdk';
@@ -35,7 +36,9 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM
3536
handler.fetch = new Proxy(handler.fetch, {
3637
apply(target, thisArg, args: Parameters<ExportedHandlerFetchHandler<Env, CfHostMetadata>>) {
3738
const [request, env, context] = args;
38-
const options = optionsCallback(env);
39+
40+
const options = getFinalOptions(optionsCallback(env), env);
41+
3942
return wrapRequestHandler({ options, request, context }, () => target.apply(thisArg, args));
4043
},
4144
});
@@ -48,7 +51,8 @@ export function withSentry<Env = unknown, QueueHandlerMessage = unknown, CfHostM
4851
apply(target, thisArg, args: Parameters<ExportedHandlerScheduledHandler<Env>>) {
4952
const [event, env, context] = args;
5053
return withIsolationScope(isolationScope => {
51-
const options = optionsCallback(env);
54+
const options = getFinalOptions(optionsCallback(env), env);
55+
5256
const client = init(options);
5357
isolationScope.setClient(client);
5458

packages/cloudflare/src/options.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { CloudflareOptions } from './client';
2+
3+
/**
4+
* Merges the options passed in from the user with the options we read from
5+
* the Cloudflare `env` environment variable object.
6+
*
7+
* @param userOptions - The options passed in from the user.
8+
* @param env - The environment variables.
9+
*
10+
* @returns The final options.
11+
*/
12+
export function getFinalOptions(userOptions: CloudflareOptions, env: unknown): CloudflareOptions {
13+
if (typeof env !== 'object' || env === null) {
14+
return userOptions;
15+
}
16+
17+
const release = 'SENTRY_RELEASE' in env && typeof env.SENTRY_RELEASE === 'string' ? env.SENTRY_RELEASE : undefined;
18+
19+
return { release, ...userOptions };
20+
}

packages/cloudflare/test/handler.test.ts

+109
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { withSentry } from '../src/handler';
1010

1111
const MOCK_ENV = {
1212
SENTRY_DSN: 'https://[email protected]/1337',
13+
SENTRY_RELEASE: '1.1.1',
1314
};
1415

1516
describe('withSentry', () => {
@@ -51,6 +52,65 @@ describe('withSentry', () => {
5152

5253
expect(result).toBe(response);
5354
});
55+
56+
test('merges options from env and callback', async () => {
57+
const handler = {
58+
fetch(_request, _env, _context) {
59+
throw new Error('test');
60+
},
61+
} satisfies ExportedHandler<typeof MOCK_ENV>;
62+
63+
let sentryEvent: Event = {};
64+
65+
const wrappedHandler = withSentry(
66+
env => ({
67+
dsn: env.SENTRY_DSN,
68+
beforeSend(event) {
69+
sentryEvent = event;
70+
return null;
71+
},
72+
}),
73+
handler,
74+
);
75+
76+
try {
77+
await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
78+
} catch {
79+
// ignore
80+
}
81+
82+
expect(sentryEvent.release).toEqual('1.1.1');
83+
});
84+
85+
test('callback options take precedence over env options', async () => {
86+
const handler = {
87+
fetch(_request, _env, _context) {
88+
throw new Error('test');
89+
},
90+
} satisfies ExportedHandler<typeof MOCK_ENV>;
91+
92+
let sentryEvent: Event = {};
93+
94+
const wrappedHandler = withSentry(
95+
env => ({
96+
dsn: env.SENTRY_DSN,
97+
release: '2.0.0',
98+
beforeSend(event) {
99+
sentryEvent = event;
100+
return null;
101+
},
102+
}),
103+
handler,
104+
);
105+
106+
try {
107+
await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
108+
} catch {
109+
// ignore
110+
}
111+
112+
expect(sentryEvent.release).toEqual('2.0.0');
113+
});
54114
});
55115

56116
describe('scheduled handler', () => {
@@ -70,6 +130,55 @@ describe('withSentry', () => {
70130
expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV);
71131
});
72132

133+
test('merges options from env and callback', async () => {
134+
const handler = {
135+
scheduled(_controller, _env, _context) {
136+
SentryCore.captureMessage('cloud_resource');
137+
return;
138+
},
139+
} satisfies ExportedHandler<typeof MOCK_ENV>;
140+
141+
let sentryEvent: Event = {};
142+
const wrappedHandler = withSentry(
143+
env => ({
144+
dsn: env.SENTRY_DSN,
145+
beforeSend(event) {
146+
sentryEvent = event;
147+
return null;
148+
},
149+
}),
150+
handler,
151+
);
152+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
153+
154+
expect(sentryEvent.release).toBe('1.1.1');
155+
});
156+
157+
test('callback options take precedence over env options', async () => {
158+
const handler = {
159+
scheduled(_controller, _env, _context) {
160+
SentryCore.captureMessage('cloud_resource');
161+
return;
162+
},
163+
} satisfies ExportedHandler<typeof MOCK_ENV>;
164+
165+
let sentryEvent: Event = {};
166+
const wrappedHandler = withSentry(
167+
env => ({
168+
dsn: env.SENTRY_DSN,
169+
release: '2.0.0',
170+
beforeSend(event) {
171+
sentryEvent = event;
172+
return null;
173+
},
174+
}),
175+
handler,
176+
);
177+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
178+
179+
expect(sentryEvent.release).toEqual('2.0.0');
180+
});
181+
73182
test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => {
74183
const handler = {
75184
scheduled(_controller, _env, _context) {
+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { getFinalOptions } from '../src/options';
3+
4+
describe('getFinalOptions', () => {
5+
it('returns user options when env is not an object', () => {
6+
const userOptions = { dsn: 'test-dsn', release: 'test-release' };
7+
const env = 'not-an-object';
8+
9+
const result = getFinalOptions(userOptions, env);
10+
11+
expect(result).toEqual(userOptions);
12+
});
13+
14+
it('returns user options when env is null', () => {
15+
const userOptions = { dsn: 'test-dsn', release: 'test-release' };
16+
const env = null;
17+
18+
const result = getFinalOptions(userOptions, env);
19+
20+
expect(result).toEqual(userOptions);
21+
});
22+
23+
it('merges options from env with user options', () => {
24+
const userOptions = { dsn: 'test-dsn', release: 'user-release' };
25+
const env = { SENTRY_RELEASE: 'env-release' };
26+
27+
const result = getFinalOptions(userOptions, env);
28+
29+
expect(result).toEqual({ dsn: 'test-dsn', release: 'user-release' });
30+
});
31+
32+
it('uses user options when SENTRY_RELEASE exists but is not a string', () => {
33+
const userOptions = { dsn: 'test-dsn', release: 'user-release' };
34+
const env = { SENTRY_RELEASE: 123 };
35+
36+
const result = getFinalOptions(userOptions, env);
37+
38+
expect(result).toEqual(userOptions);
39+
});
40+
41+
it('uses user options when SENTRY_RELEASE does not exist', () => {
42+
const userOptions = { dsn: 'test-dsn', release: 'user-release' };
43+
const env = { OTHER_VAR: 'some-value' };
44+
45+
const result = getFinalOptions(userOptions, env);
46+
47+
expect(result).toEqual(userOptions);
48+
});
49+
50+
it('takes user options over env options', () => {
51+
const userOptions = { dsn: 'test-dsn', release: 'user-release' };
52+
const env = { SENTRY_RELEASE: 'env-release' };
53+
54+
const result = getFinalOptions(userOptions, env);
55+
56+
expect(result).toEqual(userOptions);
57+
});
58+
});

0 commit comments

Comments
 (0)