Skip to content

Commit 207a32c

Browse files
committed
test(effect): Add Effect v3 compatibility tests for backwards compat
1 parent 104b64e commit 207a32c

11 files changed

Lines changed: 1746 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ jobs:
164164
changed_browser_integration:
165165
${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected,
166166
'@sentry-internal/browser-integration-tests') }}
167+
changed_effect_v3_compatibility:
168+
${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected,
169+
'@sentry-internal/effect-3-compatibility-tests') }}
167170

168171
job_check_branches:
169172
name: Check PR branches
@@ -749,6 +752,29 @@ jobs:
749752
working-directory: dev-packages/node-core-integration-tests
750753
run: yarn test
751754

755+
job_effect_v3_compatibility_tests:
756+
name: Effect v3 Compatibility Tests
757+
needs: [job_get_metadata, job_build]
758+
if: needs.job_build.outputs.changed_effect_v3_compatibility == 'true' || github.event_name != 'pull_request'
759+
runs-on: ubuntu-24.04
760+
timeout-minutes: 10
761+
steps:
762+
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
763+
uses: actions/checkout@v6
764+
with:
765+
ref: ${{ env.HEAD_COMMIT }}
766+
- name: Set up Node
767+
uses: actions/setup-node@v6
768+
with:
769+
node-version-file: 'package.json'
770+
- name: Restore caches
771+
uses: ./.github/actions/restore-cache
772+
with:
773+
dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }}
774+
- name: Run tests
775+
working-directory: dev-packages/effect-3-compatibility-tests
776+
run: yarn test
777+
752778
job_cloudflare_integration_tests:
753779
name: Cloudflare Integration Tests
754780
needs: [job_get_metadata, job_build]
@@ -1123,6 +1149,7 @@ jobs:
11231149
job_node_unit_tests,
11241150
job_node_integration_tests,
11251151
job_node_core_integration_tests,
1152+
job_effect_v3_compatibility_tests,
11261153
job_cloudflare_integration_tests,
11271154
job_bun_integration_tests,
11281155
job_browser_playwright_tests,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@sentry-internal/effect-3-compatibility-tests",
3+
"version": "10.49.0",
4+
"license": "MIT",
5+
"engines": {
6+
"node": ">=18"
7+
},
8+
"private": true,
9+
"scripts": {
10+
"lint": "oxlint . --type-aware",
11+
"lint:fix": "oxlint . --fix --type-aware",
12+
"type-check": "tsc",
13+
"test": "vitest run",
14+
"test:watch": "vitest --watch"
15+
},
16+
"dependencies": {
17+
"@sentry/core": "link:../../packages/core",
18+
"@sentry/effect": "link:../../packages/effect"
19+
},
20+
"devDependencies": {
21+
"@effect/vitest": "^0.29.0",
22+
"effect": "^3.21.1",
23+
"vitest": "^3.2.4"
24+
},
25+
"volta": {
26+
"extends": "../../package.json"
27+
}
28+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { describe, expect, it } from 'vitest';
2+
import * as index from '@sentry/effect/client';
3+
4+
describe('effect index export', () => {
5+
it('has correct exports', () => {
6+
expect(index.captureException).toBeDefined();
7+
});
8+
});
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { describe, expect, it } from '@effect/vitest';
2+
import * as sentryCore from '@sentry/core';
3+
import { getClient, getCurrentScope, getIsolationScope, SDK_VERSION } from '@sentry/core';
4+
import { Effect, Layer, Logger, LogLevel } from 'effect';
5+
import { afterEach, beforeEach, vi } from 'vitest';
6+
import * as sentryClient from '@sentry/effect/client';
7+
import * as sentryServer from '@sentry/effect/server';
8+
9+
const TEST_DSN = 'https://username@domain/123';
10+
11+
function getMockTransport() {
12+
return () => ({
13+
send: vi.fn().mockResolvedValue({}),
14+
flush: vi.fn().mockResolvedValue(true),
15+
});
16+
}
17+
18+
describe.each([
19+
[
20+
{
21+
subSdkName: 'browser',
22+
effectLayer: sentryClient.effectLayer,
23+
SentryEffectTracer: sentryClient.SentryEffectTracer,
24+
SentryEffectLogger: sentryClient.SentryEffectLogger,
25+
SentryEffectMetricsLayer: sentryClient.SentryEffectMetricsLayer,
26+
},
27+
],
28+
[
29+
{
30+
subSdkName: 'node-light',
31+
effectLayer: sentryServer.effectLayer,
32+
SentryEffectTracer: sentryServer.SentryEffectTracer,
33+
SentryEffectLogger: sentryServer.SentryEffectLogger,
34+
SentryEffectMetricsLayer: sentryServer.SentryEffectMetricsLayer,
35+
},
36+
],
37+
])('effectLayer ($subSdkName)', ({ subSdkName, effectLayer, SentryEffectTracer, SentryEffectLogger }) => {
38+
beforeEach(() => {
39+
getCurrentScope().clear();
40+
getIsolationScope().clear();
41+
});
42+
43+
afterEach(() => {
44+
getCurrentScope().setClient(undefined);
45+
vi.restoreAllMocks();
46+
});
47+
48+
it('creates a valid Effect layer', () => {
49+
const layer = effectLayer({
50+
dsn: TEST_DSN,
51+
transport: getMockTransport(),
52+
});
53+
54+
expect(layer).toBeDefined();
55+
expect(Layer.isLayer(layer)).toBe(true);
56+
});
57+
58+
it.effect('applies SDK metadata', () =>
59+
Effect.gen(function* () {
60+
yield* Effect.void;
61+
62+
const client = getClient();
63+
const metadata = client?.getOptions()._metadata?.sdk;
64+
65+
expect(metadata?.name).toBe('sentry.javascript.effect');
66+
expect(metadata?.packages).toEqual([
67+
{ name: 'npm:@sentry/effect', version: SDK_VERSION },
68+
{ name: `npm:@sentry/${subSdkName}`, version: SDK_VERSION },
69+
]);
70+
}).pipe(
71+
Effect.provide(
72+
effectLayer({
73+
dsn: TEST_DSN,
74+
transport: getMockTransport(),
75+
}),
76+
),
77+
),
78+
);
79+
80+
it.effect('layer can be provided to an Effect program', () =>
81+
Effect.gen(function* () {
82+
const result = yield* Effect.succeed('test-result');
83+
expect(result).toBe('test-result');
84+
}).pipe(
85+
Effect.provide(
86+
effectLayer({
87+
dsn: TEST_DSN,
88+
transport: getMockTransport(),
89+
}),
90+
),
91+
),
92+
);
93+
94+
it.effect('layer enables tracing when tracer is set', () =>
95+
Effect.gen(function* () {
96+
const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan');
97+
98+
const result = yield* Effect.withSpan('test-span')(Effect.succeed('traced'));
99+
expect(result).toBe('traced');
100+
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'test-span' }));
101+
}).pipe(
102+
Effect.withTracer(SentryEffectTracer),
103+
Effect.provide(
104+
effectLayer({
105+
dsn: TEST_DSN,
106+
transport: getMockTransport(),
107+
}),
108+
),
109+
),
110+
);
111+
112+
it.effect('layer can be composed with tracer layer', () =>
113+
Effect.gen(function* () {
114+
const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan');
115+
116+
const result = yield* Effect.succeed(42).pipe(
117+
Effect.map(n => n * 2),
118+
Effect.withSpan('computation'),
119+
);
120+
expect(result).toBe(84);
121+
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' }));
122+
}).pipe(
123+
Effect.provide(
124+
Layer.mergeAll(
125+
effectLayer({
126+
dsn: TEST_DSN,
127+
transport: getMockTransport(),
128+
}),
129+
Layer.setTracer(SentryEffectTracer),
130+
),
131+
),
132+
),
133+
);
134+
135+
it.effect('layer can be composed with logger layer', () =>
136+
Effect.gen(function* () {
137+
yield* Effect.logInfo('test log');
138+
const result = yield* Effect.succeed('logged');
139+
expect(result).toBe('logged');
140+
}).pipe(
141+
Effect.provide(
142+
Layer.mergeAll(
143+
effectLayer({
144+
dsn: TEST_DSN,
145+
transport: getMockTransport(),
146+
}),
147+
Logger.replace(Logger.defaultLogger, SentryEffectLogger),
148+
Logger.minimumLogLevel(LogLevel.All),
149+
),
150+
),
151+
),
152+
);
153+
154+
it.effect('layer can be composed with all Effect features', () =>
155+
Effect.gen(function* () {
156+
const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan');
157+
158+
yield* Effect.logInfo('starting computation');
159+
const result = yield* Effect.succeed(42).pipe(
160+
Effect.map(n => n * 2),
161+
Effect.withSpan('computation'),
162+
);
163+
yield* Effect.logInfo('computation complete');
164+
expect(result).toBe(84);
165+
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' }));
166+
}).pipe(
167+
Effect.provide(
168+
Layer.mergeAll(
169+
effectLayer({
170+
dsn: TEST_DSN,
171+
transport: getMockTransport(),
172+
}),
173+
Layer.setTracer(SentryEffectTracer),
174+
Logger.replace(Logger.defaultLogger, SentryEffectLogger),
175+
Logger.minimumLogLevel(LogLevel.All),
176+
),
177+
),
178+
),
179+
);
180+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, expect, it } from '@effect/vitest';
2+
import * as sentryCore from '@sentry/core';
3+
import { Effect, Layer, Logger, LogLevel } from 'effect';
4+
import { afterEach, vi } from 'vitest';
5+
import { SentryEffectLogger } from '@sentry/effect';
6+
7+
vi.mock('@sentry/core', async importOriginal => {
8+
const original = await importOriginal<typeof sentryCore>();
9+
return {
10+
...original,
11+
logger: {
12+
...original.logger,
13+
error: vi.fn(),
14+
warn: vi.fn(),
15+
info: vi.fn(),
16+
debug: vi.fn(),
17+
trace: vi.fn(),
18+
fatal: vi.fn(),
19+
},
20+
};
21+
});
22+
23+
describe('SentryEffectLogger', () => {
24+
afterEach(() => {
25+
vi.clearAllMocks();
26+
});
27+
28+
const loggerLayer = Layer.mergeAll(
29+
Logger.replace(Logger.defaultLogger, SentryEffectLogger),
30+
Logger.minimumLogLevel(LogLevel.All),
31+
);
32+
33+
it.effect('forwards fatal logs to Sentry', () =>
34+
Effect.gen(function* () {
35+
yield* Effect.logFatal('This is a fatal message');
36+
expect(sentryCore.logger.fatal).toHaveBeenCalledWith('This is a fatal message');
37+
}).pipe(Effect.provide(loggerLayer)),
38+
);
39+
40+
it.effect('forwards error logs to Sentry', () =>
41+
Effect.gen(function* () {
42+
yield* Effect.logError('This is an error message');
43+
expect(sentryCore.logger.error).toHaveBeenCalledWith('This is an error message');
44+
}).pipe(Effect.provide(loggerLayer)),
45+
);
46+
47+
it.effect('forwards warning logs to Sentry', () =>
48+
Effect.gen(function* () {
49+
yield* Effect.logWarning('This is a warning message');
50+
expect(sentryCore.logger.warn).toHaveBeenCalledWith('This is a warning message');
51+
}).pipe(Effect.provide(loggerLayer)),
52+
);
53+
54+
it.effect('forwards info logs to Sentry', () =>
55+
Effect.gen(function* () {
56+
yield* Effect.logInfo('This is an info message');
57+
expect(sentryCore.logger.info).toHaveBeenCalledWith('This is an info message');
58+
}).pipe(Effect.provide(loggerLayer)),
59+
);
60+
61+
it.effect('forwards debug logs to Sentry', () =>
62+
Effect.gen(function* () {
63+
yield* Effect.logDebug('This is a debug message');
64+
expect(sentryCore.logger.debug).toHaveBeenCalledWith('This is a debug message');
65+
}).pipe(Effect.provide(loggerLayer)),
66+
);
67+
68+
it.effect('forwards trace logs to Sentry', () =>
69+
Effect.gen(function* () {
70+
yield* Effect.logTrace('This is a trace message');
71+
expect(sentryCore.logger.trace).toHaveBeenCalledWith('This is a trace message');
72+
}).pipe(Effect.provide(loggerLayer)),
73+
);
74+
75+
it.effect('handles object messages by stringifying', () =>
76+
Effect.gen(function* () {
77+
yield* Effect.logInfo({ key: 'value', nested: { foo: 'bar' } });
78+
expect(sentryCore.logger.info).toHaveBeenCalledWith('{"key":"value","nested":{"foo":"bar"}}');
79+
}).pipe(Effect.provide(loggerLayer)),
80+
);
81+
82+
it.effect('handles multiple log calls', () =>
83+
Effect.gen(function* () {
84+
yield* Effect.logInfo('First message');
85+
yield* Effect.logInfo('Second message');
86+
yield* Effect.logWarning('Third message');
87+
expect(sentryCore.logger.info).toHaveBeenCalledTimes(2);
88+
expect(sentryCore.logger.info).toHaveBeenNthCalledWith(1, 'First message');
89+
expect(sentryCore.logger.info).toHaveBeenNthCalledWith(2, 'Second message');
90+
expect(sentryCore.logger.warn).toHaveBeenCalledWith('Third message');
91+
}).pipe(Effect.provide(loggerLayer)),
92+
);
93+
94+
it.effect('works with Effect.tap for logging side effects', () =>
95+
Effect.gen(function* () {
96+
const result = yield* Effect.succeed('data').pipe(
97+
Effect.tap(data => Effect.logInfo(`Processing: ${data}`)),
98+
Effect.map(d => d.toUpperCase()),
99+
);
100+
expect(result).toBe('DATA');
101+
expect(sentryCore.logger.info).toHaveBeenCalledWith('Processing: data');
102+
}).pipe(Effect.provide(loggerLayer)),
103+
);
104+
});

0 commit comments

Comments
 (0)