Skip to content

feat(flags): add node support for generic featureFlagsIntegration and move utils to core #16585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3f840b4
Buffer with global weakmap and save on spanEnd
aliu39 Jun 4, 2025
af6fa72
Update docstrs
aliu39 Jun 4, 2025
5453c1f
Attribute prefix const
aliu39 Jun 4, 2025
bc183e6
Handle dup evals of same flag
aliu39 Jun 4, 2025
bc439c8
Update docstrs. Todo: update util unit tests
aliu39 Jun 4, 2025
403a02b
Nest existing tests under onError folders
aliu39 Jun 5, 2025
ffe9bcd
Fix imports
aliu39 Jun 5, 2025
7c2c161
Fix global type, add unit tests, add generic ffs test
aliu39 Jun 6, 2025
e6da588
Add ld test
aliu39 Jun 6, 2025
2459a11
Add of, stat, unleash tests
aliu39 Jun 6, 2025
3c13997
fmt
aliu39 Jun 6, 2025
70ccac1
Dup integration and utils to core, todos in index and integration
aliu39 Jun 10, 2025
8085ee8
Reset unrelated otel test
aliu39 Jun 10, 2025
6381f91
Import utils from core with _INTERNAL_ prefix
aliu39 Jun 10, 2025
a93b0e0
Delete old browser utils and integration and yarn fix
aliu39 Jun 10, 2025
4366e5a
Keep browser in docstr example
aliu39 Jun 10, 2025
868cd71
Export from same pkgs as zodErrorsIntegration, except remix, solidsta…
aliu39 Jun 10, 2025
535932c
Merge branch 'develop' into aliu/span-flags-v2
aliu39 Jun 10, 2025
2b9a867
Merge branch 'aliu/span-flags-v2' into aliu/move-ffs-to-core
aliu39 Jun 10, 2025
1611330
Add on error node tests. TODO export/import buffer sizes as _INTERNAL
aliu39 Jun 10, 2025
8124a40
Merge branch 'aliu/move-ffs-to-core' of https://github.com/getsentry/…
aliu39 Jun 10, 2025
9cce949
Add flags to attrs directly on eval
aliu39 Jun 12, 2025
52385ee
Rename util
aliu39 Jun 12, 2025
90d9289
Move FF type to core utils file
aliu39 Jun 12, 2025
bca65dd
Export buffer sizes from core as _INTERNAL_
aliu39 Jun 12, 2025
db2fa33
Remove allowEviction
aliu39 Jun 15, 2025
2b385a4
Set attr directly on eval instead of in hook
aliu39 Jun 15, 2025
44324cc
Remove allowEviction
aliu39 Jun 15, 2025
ea6872a
Fix
aliu39 Jun 15, 2025
a323c38
Merge branch 'develop' into aliu/span-flags-v2
aliu39 Jun 15, 2025
b3d746b
Merge branch 'develop' of https://github.com/getsentry/sentry-javascr…
aliu39 Jun 15, 2025
0f4e2f6
Merge branch 'aliu/span-flags-v2' of https://github.com/getsentry/sen…
aliu39 Jun 15, 2025
2ace2ae
Fix merge
aliu39 Jun 15, 2025
979600c
Merge branch 'develop' of https://github.com/getsentry/sentry-javascr…
aliu39 Jun 17, 2025
60f713b
Redel deleted files after merge, import buf sizes in node tests
aliu39 Jun 17, 2025
d695910
Lint
aliu39 Jun 17, 2025
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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
Copy link
Member

Choose a reason for hiding this comment

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

Is this _INTERNAL pattern common in the codebase?

Copy link
Member

Choose a reason for hiding this comment

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

It is starting with logs - we don't really have a better way to do this because of the way @sentry/core is structured atm.

We could do subpath exports, but that breaks horribly for some bundlers (esbuild 😢)

import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../../constants';

sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test';
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
import {
type EventAndTraceHeader,
Expand All @@ -7,7 +8,6 @@ import {
shouldSkipFeatureFlagsTest,
shouldSkipTracingTest,
} from '../../../../../utils/helpers';
import { MAX_FLAGS_PER_SPAN } from '../../constants';

sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../../constants';

sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test';
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
import {
type EventAndTraceHeader,
Expand All @@ -7,7 +8,6 @@ import {
shouldSkipFeatureFlagsTest,
shouldSkipTracingTest,
} from '../../../../../utils/helpers';
import { MAX_FLAGS_PER_SPAN } from '../../constants';

sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../../constants';

sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../../constants';

sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test';
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
import {
type EventAndTraceHeader,
Expand All @@ -7,7 +8,6 @@ import {
shouldSkipFeatureFlagsTest,
shouldSkipTracingTest,
} from '../../../../../utils/helpers';
import { MAX_FLAGS_PER_SPAN } from '../../constants';

sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../../constants';

sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test';
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
import {
type EventAndTraceHeader,
Expand All @@ -7,7 +8,6 @@ import {
shouldSkipFeatureFlagsTest,
shouldSkipTracingTest,
} from '../../../../../utils/helpers';
import { MAX_FLAGS_PER_SPAN } from '../../constants';

sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { expect } from '@playwright/test';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../../constants';

sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test';
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
import { sentryTest } from '../../../../../utils/fixtures';
import {
type EventAndTraceHeader,
Expand All @@ -7,7 +8,6 @@ import {
shouldSkipFeatureFlagsTest,
shouldSkipTracingTest,
} from '../../../../../utils/helpers';
import { MAX_FLAGS_PER_SPAN } from '../../constants';

sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.featureFlagsIntegration()],
});

const flagsIntegration = Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>('FeatureFlags');
for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) {
flagsIntegration?.addFeatureFlag(`feat${i}`, false);
}
flagsIntegration?.addFeatureFlag(`feat${FLAG_BUFFER_SIZE + 1}`, true); // eviction
flagsIntegration?.addFeatureFlag('feat3', true); // update

throw new Error('Test error');
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { afterAll, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner';

afterAll(() => {
cleanupChildProcesses();
});

test('Flags captured on error with eviction, update, and no async tasks', async () => {
// Based on scenario.ts.
const expectedFlags = [{ flag: 'feat2', result: false }];
for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) {
expectedFlags.push({ flag: `feat${i}`, result: false });
}
expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true });
expectedFlags.push({ flag: 'feat3', result: true });

await createRunner(__dirname, 'scenario.ts')
.expect({
event: {
exception: { values: [{ type: 'Error', value: 'Test error' }] },
contexts: {
flags: {
values: expectedFlags,
},
},
},
})
.start()
.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Scope } from '@sentry/node';
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.featureFlagsIntegration()],
});

const flagsIntegration = Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>('FeatureFlags');
flagsIntegration?.addFeatureFlag('shared', true);

Sentry.withScope((_scope: Scope) => {
flagsIntegration?.addFeatureFlag('forked', true);
flagsIntegration?.addFeatureFlag('shared', false);
Sentry.captureException(new Error('Error in forked scope'));
});

flagsIntegration?.addFeatureFlag('main', true);
throw new Error('Error in main scope');
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { afterAll, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner';

afterAll(() => {
cleanupChildProcesses();
});

test('Flags captured on error are isolated by current scope', async () => {
await createRunner(__dirname, 'scenario.ts')
.expect({
event: {
exception: { values: [{ type: 'Error', value: 'Error in forked scope' }] },
contexts: {
flags: {
values: [
{ flag: 'forked', result: true },
{ flag: 'shared', result: false },
],
},
},
},
})
.expect({
event: {
exception: { values: [{ type: 'Error', value: 'Error in main scope' }] },
contexts: {
flags: {
values: [
{ flag: 'shared', result: true },
{ flag: 'main', result: true },
],
},
},
},
})
.start()
.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1.0,
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.featureFlagsIntegration()],
});

const flagsIntegration = Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>('FeatureFlags');

Sentry.startSpan({ name: 'test-root-span' }, () => {
Sentry.startSpan({ name: 'test-span' }, () => {
Sentry.startSpan({ name: 'test-nested-span' }, () => {
for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
flagsIntegration?.addFeatureFlag(`feat${i}`, false);
}
flagsIntegration?.addFeatureFlag(`feat${MAX_FLAGS_PER_SPAN + 1}`, true); // dropped flag
flagsIntegration?.addFeatureFlag('feat3', true); // update
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
import { afterAll, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';

afterAll(() => {
cleanupChildProcesses();
});

test('Flags captured on span attributes with max limit', async () => {
// Based on scenario.ts.
const expectedFlags: Record<string, boolean> = {};
for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
expectedFlags[`flag.evaluation.feat${i}`] = i === 3;
}

await createRunner(__dirname, 'scenario.ts')
.expect({
transaction: {
spans: [
expect.objectContaining({
description: 'test-span',
data: expect.objectContaining({}),
}),
expect.objectContaining({
description: 'test-nested-span',
data: expect.objectContaining(expectedFlags),
}),
],
},
})
.start()
.completed();
});
2 changes: 2 additions & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export {
consoleLoggingIntegration,
wrapMcpServerWithSentry,
NODE_VERSION,
featureFlagsIntegration,
type FeatureFlagsIntegration,
} from '@sentry/node';

export { init } from './server/sdk';
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ export {
consoleLoggingIntegration,
wrapMcpServerWithSentry,
NODE_VERSION,
featureFlagsIntegration,
type FeatureFlagsIntegration,
} from '@sentry/node';

export {
Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ export {
instrumentSupabaseClient,
zodErrorsIntegration,
thirdPartyErrorFilterIntegration,
featureFlagsIntegration,
} from '@sentry/core';
export type { Span } from '@sentry/core';
export type { Span, FeatureFlagsIntegration } from '@sentry/core';
export { makeBrowserOfflineTransport } from './transports/offline';
export { browserProfilingIntegration } from './profiling/integration';
export { spotlightBrowserIntegration } from './integrations/spotlight';
export { browserSessionIntegration } from './integrations/browsersession';
export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags';
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
export { unleashIntegration } from './integrations/featureFlags/unleash';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core';
import { defineIntegration } from '@sentry/core';
import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags';
import {
_INTERNAL_addFeatureFlagToActiveSpan,
_INTERNAL_copyFlagsFromScopeToEvent,
_INTERNAL_insertFlagToScope,
defineIntegration,
} from '@sentry/core';
import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types';

/**
Expand All @@ -23,7 +27,7 @@ export const launchDarklyIntegration = defineIntegration(() => {
name: 'LaunchDarkly',

processEvent(event: Event, _hint: EventHint, _client: Client): Event {
return copyFlagsFromScopeToEvent(event);
return _INTERNAL_copyFlagsFromScopeToEvent(event);
},
};
}) satisfies IntegrationFn;
Expand All @@ -45,8 +49,8 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler
* Handle a flag evaluation by storing its name and value on the current scope.
*/
method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => {
insertFlagToScope(flagKey, flagDetail.value);
addFeatureFlagToActiveSpan(flagKey, flagDetail.value);
_INTERNAL_insertFlagToScope(flagKey, flagDetail.value);
_INTERNAL_addFeatureFlagToActiveSpan(flagKey, flagDetail.value);
},
};
}
Loading
Loading