Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ jobs:
changed_browser_integration:
${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected,
'@sentry-internal/browser-integration-tests') }}
changed_effect_v3_compatibility:
${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected,
'@sentry-internal/effect-3-compatibility-tests') }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI affected detection will never trigger for this package

Medium Severity

The changed_effect_v3_compatibility output uses contains(steps.checkForAffected.outputs.affected, '@sentry-internal/effect-3-compatibility-tests'), but this package is intentionally excluded from the yarn workspace (confirmed it's absent from the workspaces list in root package.json). Since NX only tracks workspace projects, @sentry-internal/effect-3-compatibility-tests will never appear in the affected list. On PRs, the job condition needs.job_build.outputs.changed_effect_v3_compatibility == 'true' will only be true when changed_ci is true (CI config changes), never when the test files themselves or @sentry/effect source code changes. This means the compatibility tests effectively won't run on most PRs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c328112. Configure here.


job_check_branches:
name: Check PR branches
Expand Down Expand Up @@ -749,6 +752,32 @@ jobs:
working-directory: dev-packages/node-core-integration-tests
run: yarn test

job_effect_v3_compatibility_tests:
name: Effect v3 Compatibility Tests
needs: [job_get_metadata, job_build]
if: needs.job_build.outputs.changed_effect_v3_compatibility == 'true' || github.event_name != 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
uses: actions/checkout@v6
with:
ref: ${{ env.HEAD_COMMIT }}
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version-file: 'package.json'
- name: Restore caches
uses: ./.github/actions/restore-cache
with:
dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }}
- name: Install dependencies
working-directory: dev-packages/effect-3-compatibility-tests
run: yarn install
- name: Run tests
working-directory: dev-packages/effect-3-compatibility-tests
run: yarn test

job_cloudflare_integration_tests:
name: Cloudflare Integration Tests
needs: [job_get_metadata, job_build]
Expand Down Expand Up @@ -1123,6 +1152,7 @@ jobs:
job_node_unit_tests,
job_node_integration_tests,
job_node_core_integration_tests,
job_effect_v3_compatibility_tests,
job_cloudflare_integration_tests,
job_bun_integration_tests,
job_browser_playwright_tests,
Expand Down
28 changes: 28 additions & 0 deletions dev-packages/effect-3-compatibility-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@sentry-internal/effect-3-compatibility-tests",
"version": "10.49.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"private": true,
"scripts": {
"lint": "oxlint . --type-aware",
"lint:fix": "oxlint . --fix --type-aware",
"type-check": "tsc",
"test": "vitest run",
"test:watch": "vitest --watch"
},
"dependencies": {
"@sentry/core": "link:../../packages/core",
"@sentry/effect": "link:../../packages/effect"
},
"devDependencies": {
"@effect/vitest": "^0.29.0",
"effect": "^3.21.1",
"vitest": "^3.2.4"
},
"volta": {
"extends": "../../package.json"
}
}
8 changes: 8 additions & 0 deletions dev-packages/effect-3-compatibility-tests/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest';
import * as index from '@sentry/effect/client';

describe('effect index export', () => {
it('has correct exports', () => {
expect(index.captureException).toBeDefined();
});
});
180 changes: 180 additions & 0 deletions dev-packages/effect-3-compatibility-tests/test/layer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { describe, expect, it } from '@effect/vitest';
import * as sentryCore from '@sentry/core';
import { getClient, getCurrentScope, getIsolationScope, SDK_VERSION } from '@sentry/core';
import { Effect, Layer, Logger, LogLevel } from 'effect';
import { afterEach, beforeEach, vi } from 'vitest';
import * as sentryClient from '@sentry/effect/client';
import * as sentryServer from '@sentry/effect/server';

const TEST_DSN = 'https://username@domain/123';

function getMockTransport() {
return () => ({
send: vi.fn().mockResolvedValue({}),
flush: vi.fn().mockResolvedValue(true),
});
}

describe.each([
[
{
subSdkName: 'browser',
effectLayer: sentryClient.effectLayer,
SentryEffectTracer: sentryClient.SentryEffectTracer,
SentryEffectLogger: sentryClient.SentryEffectLogger,
SentryEffectMetricsLayer: sentryClient.SentryEffectMetricsLayer,
},
],
[
{
subSdkName: 'node-light',
effectLayer: sentryServer.effectLayer,
SentryEffectTracer: sentryServer.SentryEffectTracer,
SentryEffectLogger: sentryServer.SentryEffectLogger,
SentryEffectMetricsLayer: sentryServer.SentryEffectMetricsLayer,
},
],
])('effectLayer ($subSdkName)', ({ subSdkName, effectLayer, SentryEffectTracer, SentryEffectLogger }) => {
beforeEach(() => {
getCurrentScope().clear();
getIsolationScope().clear();
});

afterEach(() => {
getCurrentScope().setClient(undefined);
vi.restoreAllMocks();
});

it('creates a valid Effect layer', () => {
const layer = effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
});

expect(layer).toBeDefined();
expect(Layer.isLayer(layer)).toBe(true);
});

it.effect('applies SDK metadata', () =>
Effect.gen(function* () {
yield* Effect.void;

const client = getClient();
const metadata = client?.getOptions()._metadata?.sdk;

expect(metadata?.name).toBe('sentry.javascript.effect');
expect(metadata?.packages).toEqual([
{ name: 'npm:@sentry/effect', version: SDK_VERSION },
{ name: `npm:@sentry/${subSdkName}`, version: SDK_VERSION },
]);
}).pipe(
Effect.provide(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
),
),
);

it.effect('layer can be provided to an Effect program', () =>
Effect.gen(function* () {
const result = yield* Effect.succeed('test-result');
expect(result).toBe('test-result');
}).pipe(
Effect.provide(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
),
),
);

it.effect('layer enables tracing when tracer is set', () =>
Effect.gen(function* () {
const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan');

const result = yield* Effect.withSpan('test-span')(Effect.succeed('traced'));
expect(result).toBe('traced');
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'test-span' }));
}).pipe(
Effect.withTracer(SentryEffectTracer),
Effect.provide(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
),
),
);

it.effect('layer can be composed with tracer layer', () =>
Effect.gen(function* () {
const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan');

const result = yield* Effect.succeed(42).pipe(
Effect.map(n => n * 2),
Effect.withSpan('computation'),
);
expect(result).toBe(84);
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' }));
}).pipe(
Effect.provide(
Layer.mergeAll(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
Layer.setTracer(SentryEffectTracer),
),
),
),
);

it.effect('layer can be composed with logger layer', () =>
Effect.gen(function* () {
yield* Effect.logInfo('test log');
const result = yield* Effect.succeed('logged');
expect(result).toBe('logged');
}).pipe(
Effect.provide(
Layer.mergeAll(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
Logger.replace(Logger.defaultLogger, SentryEffectLogger),
Logger.minimumLogLevel(LogLevel.All),
),
),
),
);

it.effect('layer can be composed with all Effect features', () =>
Effect.gen(function* () {
const startInactiveSpanMock = vi.spyOn(sentryCore, 'startInactiveSpan');

yield* Effect.logInfo('starting computation');
const result = yield* Effect.succeed(42).pipe(
Effect.map(n => n * 2),
Effect.withSpan('computation'),
);
yield* Effect.logInfo('computation complete');
expect(result).toBe(84);
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'computation' }));
}).pipe(
Effect.provide(
Layer.mergeAll(
effectLayer({
dsn: TEST_DSN,
transport: getMockTransport(),
}),
Layer.setTracer(SentryEffectTracer),
Logger.replace(Logger.defaultLogger, SentryEffectLogger),
Logger.minimumLogLevel(LogLevel.All),
),
),
),
);
});
104 changes: 104 additions & 0 deletions dev-packages/effect-3-compatibility-tests/test/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { describe, expect, it } from '@effect/vitest';
import * as sentryCore from '@sentry/core';
import { Effect, Layer, Logger, LogLevel } from 'effect';
import { afterEach, vi } from 'vitest';
import { SentryEffectLogger } from '@sentry/effect';

vi.mock('@sentry/core', async importOriginal => {
const original = await importOriginal<typeof sentryCore>();
return {
...original,
logger: {
...original.logger,
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
fatal: vi.fn(),
},
};
});

describe('SentryEffectLogger', () => {
afterEach(() => {
vi.clearAllMocks();
});

const loggerLayer = Layer.mergeAll(
Logger.replace(Logger.defaultLogger, SentryEffectLogger),
Logger.minimumLogLevel(LogLevel.All),
);

it.effect('forwards fatal logs to Sentry', () =>
Effect.gen(function* () {
yield* Effect.logFatal('This is a fatal message');
expect(sentryCore.logger.fatal).toHaveBeenCalledWith('This is a fatal message');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('forwards error logs to Sentry', () =>
Effect.gen(function* () {
yield* Effect.logError('This is an error message');
expect(sentryCore.logger.error).toHaveBeenCalledWith('This is an error message');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('forwards warning logs to Sentry', () =>
Effect.gen(function* () {
yield* Effect.logWarning('This is a warning message');
expect(sentryCore.logger.warn).toHaveBeenCalledWith('This is a warning message');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('forwards info logs to Sentry', () =>
Effect.gen(function* () {
yield* Effect.logInfo('This is an info message');
expect(sentryCore.logger.info).toHaveBeenCalledWith('This is an info message');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('forwards debug logs to Sentry', () =>
Effect.gen(function* () {
yield* Effect.logDebug('This is a debug message');
expect(sentryCore.logger.debug).toHaveBeenCalledWith('This is a debug message');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('forwards trace logs to Sentry', () =>
Effect.gen(function* () {
yield* Effect.logTrace('This is a trace message');
expect(sentryCore.logger.trace).toHaveBeenCalledWith('This is a trace message');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('handles object messages by stringifying', () =>
Effect.gen(function* () {
yield* Effect.logInfo({ key: 'value', nested: { foo: 'bar' } });
expect(sentryCore.logger.info).toHaveBeenCalledWith('{"key":"value","nested":{"foo":"bar"}}');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('handles multiple log calls', () =>
Effect.gen(function* () {
yield* Effect.logInfo('First message');
yield* Effect.logInfo('Second message');
yield* Effect.logWarning('Third message');
expect(sentryCore.logger.info).toHaveBeenCalledTimes(2);
expect(sentryCore.logger.info).toHaveBeenNthCalledWith(1, 'First message');
expect(sentryCore.logger.info).toHaveBeenNthCalledWith(2, 'Second message');
expect(sentryCore.logger.warn).toHaveBeenCalledWith('Third message');
}).pipe(Effect.provide(loggerLayer)),
);

it.effect('works with Effect.tap for logging side effects', () =>
Effect.gen(function* () {
const result = yield* Effect.succeed('data').pipe(
Effect.tap(data => Effect.logInfo(`Processing: ${data}`)),
Effect.map(d => d.toUpperCase()),
);
expect(result).toBe('DATA');
expect(sentryCore.logger.info).toHaveBeenCalledWith('Processing: data');
}).pipe(Effect.provide(loggerLayer)),
);
});
Loading