Skip to content

Commit 468fc8c

Browse files
authored
feat(flags): add node support for generic featureFlagsIntegration and move utils to core (#16585)
The `featureFlagsIntegration` is an integration to manually buffer feature flags on evaluation, and capture them in event contexts and span attributes. This PR moves it from browser to core, as well as the shared functionality/utils of all FF integrations (no browser specific logic). Browser exports and functionality is unchanged. Per @AbhiPrasad 's recommendation I've manually exported the integration in all the packages `zodErrorsIntegration` is exported. Note many backend pkgs use a wildcard (*) export from node. TODO: - [x] add node-integration-tests - [ ] update platform docs Part of - getsentry/team-replay#510
1 parent 98ee4cc commit 468fc8c

File tree

40 files changed

+314
-79
lines changed

40 files changed

+314
-79
lines changed

dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
23
import { sentryTest } from '../../../../../../utils/fixtures';
34
import {
45
envelopeRequestParser,
56
shouldSkipFeatureFlagsTest,
67
waitForErrorRequest,
78
} from '../../../../../../utils/helpers';
8-
import { FLAG_BUFFER_SIZE } from '../../../constants';
99

1010
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
1111
if (shouldSkipFeatureFlagsTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
23
import { sentryTest } from '../../../../../utils/fixtures';
34
import {
45
type EventAndTraceHeader,
@@ -7,7 +8,6 @@ import {
78
shouldSkipFeatureFlagsTest,
89
shouldSkipTracingTest,
910
} from '../../../../../utils/helpers';
10-
import { MAX_FLAGS_PER_SPAN } from '../../constants';
1111

1212
sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
1313
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
23
import { sentryTest } from '../../../../../../utils/fixtures';
34
import {
45
envelopeRequestParser,
56
shouldSkipFeatureFlagsTest,
67
waitForErrorRequest,
78
} from '../../../../../../utils/helpers';
8-
import { FLAG_BUFFER_SIZE } from '../../../constants';
99

1010
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
1111
if (shouldSkipFeatureFlagsTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
23
import { sentryTest } from '../../../../../utils/fixtures';
34
import {
45
type EventAndTraceHeader,
@@ -7,7 +8,6 @@ import {
78
shouldSkipFeatureFlagsTest,
89
shouldSkipTracingTest,
910
} from '../../../../../utils/helpers';
10-
import { MAX_FLAGS_PER_SPAN } from '../../constants';
1111

1212
sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
1313
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
23
import { sentryTest } from '../../../../../../utils/fixtures';
34
import {
45
envelopeRequestParser,
56
shouldSkipFeatureFlagsTest,
67
waitForErrorRequest,
78
} from '../../../../../../utils/helpers';
8-
import { FLAG_BUFFER_SIZE } from '../../../constants';
99

1010
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
1111
if (shouldSkipFeatureFlagsTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
23
import { sentryTest } from '../../../../../../utils/fixtures';
34
import {
45
envelopeRequestParser,
56
shouldSkipFeatureFlagsTest,
67
waitForErrorRequest,
78
} from '../../../../../../utils/helpers';
8-
import { FLAG_BUFFER_SIZE } from '../../../constants';
99

1010
sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => {
1111
if (shouldSkipFeatureFlagsTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
23
import { sentryTest } from '../../../../../utils/fixtures';
34
import {
45
type EventAndTraceHeader,
@@ -7,7 +8,6 @@ import {
78
shouldSkipFeatureFlagsTest,
89
shouldSkipTracingTest,
910
} from '../../../../../utils/helpers';
10-
import { MAX_FLAGS_PER_SPAN } from '../../constants';
1111

1212
sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
1313
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
23
import { sentryTest } from '../../../../../../utils/fixtures';
34
import {
45
envelopeRequestParser,
56
shouldSkipFeatureFlagsTest,
67
waitForErrorRequest,
78
} from '../../../../../../utils/helpers';
8-
import { FLAG_BUFFER_SIZE } from '../../../constants';
99

1010
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
1111
if (shouldSkipFeatureFlagsTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
23
import { sentryTest } from '../../../../../utils/fixtures';
34
import {
45
type EventAndTraceHeader,
@@ -7,7 +8,6 @@ import {
78
shouldSkipFeatureFlagsTest,
89
shouldSkipTracingTest,
910
} from '../../../../../utils/helpers';
10-
import { MAX_FLAGS_PER_SPAN } from '../../constants';
1111

1212
sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
1313
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
23
import { sentryTest } from '../../../../../../utils/fixtures';
34
import {
45
envelopeRequestParser,
56
shouldSkipFeatureFlagsTest,
67
waitForErrorRequest,
78
} from '../../../../../../utils/helpers';
8-
import { FLAG_BUFFER_SIZE } from '../../../constants';
99

1010
sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => {
1111
if (shouldSkipFeatureFlagsTest()) {

dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { expect } from '@playwright/test';
2+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
23
import { sentryTest } from '../../../../../utils/fixtures';
34
import {
45
type EventAndTraceHeader,
@@ -7,7 +8,6 @@ import {
78
shouldSkipFeatureFlagsTest,
89
shouldSkipTracingTest,
910
} from '../../../../../utils/helpers';
10-
import { MAX_FLAGS_PER_SPAN } from '../../constants';
1111

1212
sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => {
1313
if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
2+
import * as Sentry from '@sentry/node';
3+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
sampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.featureFlagsIntegration()],
10+
});
11+
12+
const flagsIntegration = Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>('FeatureFlags');
13+
for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) {
14+
flagsIntegration?.addFeatureFlag(`feat${i}`, false);
15+
}
16+
flagsIntegration?.addFeatureFlag(`feat${FLAG_BUFFER_SIZE + 1}`, true); // eviction
17+
flagsIntegration?.addFeatureFlag('feat3', true); // update
18+
19+
throw new Error('Test error');
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
2+
import { afterAll, test } from 'vitest';
3+
import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner';
4+
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
test('Flags captured on error with eviction, update, and no async tasks', async () => {
10+
// Based on scenario.ts.
11+
const expectedFlags = [{ flag: 'feat2', result: false }];
12+
for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) {
13+
expectedFlags.push({ flag: `feat${i}`, result: false });
14+
}
15+
expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true });
16+
expectedFlags.push({ flag: 'feat3', result: true });
17+
18+
await createRunner(__dirname, 'scenario.ts')
19+
.expect({
20+
event: {
21+
exception: { values: [{ type: 'Error', value: 'Test error' }] },
22+
contexts: {
23+
flags: {
24+
values: expectedFlags,
25+
},
26+
},
27+
},
28+
})
29+
.start()
30+
.completed();
31+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Scope } from '@sentry/node';
2+
import * as Sentry from '@sentry/node';
3+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
sampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.featureFlagsIntegration()],
10+
});
11+
12+
const flagsIntegration = Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>('FeatureFlags');
13+
flagsIntegration?.addFeatureFlag('shared', true);
14+
15+
Sentry.withScope((_scope: Scope) => {
16+
flagsIntegration?.addFeatureFlag('forked', true);
17+
flagsIntegration?.addFeatureFlag('shared', false);
18+
Sentry.captureException(new Error('Error in forked scope'));
19+
});
20+
21+
flagsIntegration?.addFeatureFlag('main', true);
22+
throw new Error('Error in main scope');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { afterAll, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('Flags captured on error are isolated by current scope', async () => {
9+
await createRunner(__dirname, 'scenario.ts')
10+
.expect({
11+
event: {
12+
exception: { values: [{ type: 'Error', value: 'Error in forked scope' }] },
13+
contexts: {
14+
flags: {
15+
values: [
16+
{ flag: 'forked', result: true },
17+
{ flag: 'shared', result: false },
18+
],
19+
},
20+
},
21+
},
22+
})
23+
.expect({
24+
event: {
25+
exception: { values: [{ type: 'Error', value: 'Error in main scope' }] },
26+
contexts: {
27+
flags: {
28+
values: [
29+
{ flag: 'shared', result: true },
30+
{ flag: 'main', result: true },
31+
],
32+
},
33+
},
34+
},
35+
})
36+
.start()
37+
.completed();
38+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
2+
import * as Sentry from '@sentry/node';
3+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
sampleRate: 1.0,
8+
tracesSampleRate: 1.0,
9+
transport: loggingTransport,
10+
integrations: [Sentry.featureFlagsIntegration()],
11+
});
12+
13+
const flagsIntegration = Sentry.getClient()?.getIntegrationByName<Sentry.FeatureFlagsIntegration>('FeatureFlags');
14+
15+
Sentry.startSpan({ name: 'test-root-span' }, () => {
16+
Sentry.startSpan({ name: 'test-span' }, () => {
17+
Sentry.startSpan({ name: 'test-nested-span' }, () => {
18+
for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
19+
flagsIntegration?.addFeatureFlag(`feat${i}`, false);
20+
}
21+
flagsIntegration?.addFeatureFlag(`feat${MAX_FLAGS_PER_SPAN + 1}`, true); // dropped flag
22+
flagsIntegration?.addFeatureFlag('feat3', true); // update
23+
});
24+
});
25+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core';
2+
import { afterAll, expect, test } from 'vitest';
3+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
4+
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
test('Flags captured on span attributes with max limit', async () => {
10+
// Based on scenario.ts.
11+
const expectedFlags: Record<string, boolean> = {};
12+
for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
13+
expectedFlags[`flag.evaluation.feat${i}`] = i === 3;
14+
}
15+
16+
await createRunner(__dirname, 'scenario.ts')
17+
.expect({
18+
transaction: {
19+
spans: [
20+
expect.objectContaining({
21+
description: 'test-span',
22+
data: expect.objectContaining({}),
23+
}),
24+
expect.objectContaining({
25+
description: 'test-nested-span',
26+
data: expect.objectContaining(expectedFlags),
27+
}),
28+
],
29+
},
30+
})
31+
.start()
32+
.completed();
33+
});

packages/astro/src/index.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ export {
135135
consoleLoggingIntegration,
136136
wrapMcpServerWithSentry,
137137
NODE_VERSION,
138+
featureFlagsIntegration,
139+
type FeatureFlagsIntegration,
138140
} from '@sentry/node';
139141

140142
export { init } from './server/sdk';

packages/aws-serverless/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ export {
121121
consoleLoggingIntegration,
122122
wrapMcpServerWithSentry,
123123
NODE_VERSION,
124+
featureFlagsIntegration,
125+
type FeatureFlagsIntegration,
124126
} from '@sentry/node';
125127

126128
export {

packages/browser/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ export {
6161
instrumentSupabaseClient,
6262
zodErrorsIntegration,
6363
thirdPartyErrorFilterIntegration,
64+
featureFlagsIntegration,
6465
} from '@sentry/core';
65-
export type { Span } from '@sentry/core';
66+
export type { Span, FeatureFlagsIntegration } from '@sentry/core';
6667
export { makeBrowserOfflineTransport } from './transports/offline';
6768
export { browserProfilingIntegration } from './profiling/integration';
6869
export { spotlightBrowserIntegration } from './integrations/spotlight';
6970
export { browserSessionIntegration } from './integrations/browsersession';
70-
export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags';
7171
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
7272
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
7373
export { unleashIntegration } from './integrations/featureFlags/unleash';

packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core';
2-
import { defineIntegration } from '@sentry/core';
3-
import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags';
2+
import {
3+
_INTERNAL_addFeatureFlagToActiveSpan,
4+
_INTERNAL_copyFlagsFromScopeToEvent,
5+
_INTERNAL_insertFlagToScope,
6+
defineIntegration,
7+
} from '@sentry/core';
48
import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types';
59

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

2529
processEvent(event: Event, _hint: EventHint, _client: Client): Event {
26-
return copyFlagsFromScopeToEvent(event);
30+
return _INTERNAL_copyFlagsFromScopeToEvent(event);
2731
},
2832
};
2933
}) satisfies IntegrationFn;
@@ -45,8 +49,8 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler
4549
* Handle a flag evaluation by storing its name and value on the current scope.
4650
*/
4751
method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => {
48-
insertFlagToScope(flagKey, flagDetail.value);
49-
addFeatureFlagToActiveSpan(flagKey, flagDetail.value);
52+
_INTERNAL_insertFlagToScope(flagKey, flagDetail.value);
53+
_INTERNAL_addFeatureFlagToActiveSpan(flagKey, flagDetail.value);
5054
},
5155
};
5256
}

0 commit comments

Comments
 (0)