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 33 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,7 +1,11 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../constants';
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';

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,7 +1,11 @@
import { expect } from '@playwright/test';
import type { Scope } from '@sentry/browser';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';

sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1.0,
tracesSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
Sentry.featureFlagsIntegration(),
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const btnStartSpan = document.getElementById('btnStartSpan');
const btnEndSpan = document.getElementById('btnEndSpan');
const btnStartNestedSpan = document.getElementById('btnStartNestedSpan');
const btnEndNestedSpan = document.getElementById('btnEndNestedSpan');

window.withNestedSpans = callback => {
window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => {
window.traceId = rootSpan.spanContext().traceId;

window.Sentry.startSpan({ name: 'test-span' }, _span => {
window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => {
callback();
});
});
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="btnStartSpan">Start Span</button>
<button id="btnEndSpan">End Span</button>
<button id="btnStartNestedSpan">Start Nested Span</button>
<button id="btnEndNestedSpan">End Nested Span</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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,
eventAndTraceHeaderRequestParser,
getMultipleSentryEnvelopeRequests,
shouldSkipFeatureFlagsTest,
shouldSkipTracingTest,
} from '../../../../../utils/helpers';

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

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({}),
});
});

const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
await page.goto(url);

const envelopeRequestPromise = getMultipleSentryEnvelopeRequests<EventAndTraceHeader>(
page,
1,
{},
eventAndTraceHeaderRequestParser,
);

// withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span.
await page.evaluate(maxFlags => {
(window as any).withNestedSpans(() => {
const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags');
for (let i = 1; i <= maxFlags; i++) {
flagsIntegration.addFeatureFlag(`feat${i}`, false);
}
flagsIntegration.addFeatureFlag(`feat${maxFlags + 1}`, true); // dropped flag
flagsIntegration.addFeatureFlag('feat3', true); // update
});
return true;
}, MAX_FLAGS_PER_SPAN);

const event = (await envelopeRequestPromise)[0][0];
const innerSpan = event.spans?.[0];
const outerSpan = event.spans?.[1];
const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) =>
key.startsWith('flag.evaluation'),
);
const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) =>
key.startsWith('flag.evaluation'),
);

expect(innerSpanFlags).toEqual([]);

const expectedOuterSpanFlags = [];
for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]);
}
// Order agnostic (attribute dict is unordered).
expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort());
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../constants';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';

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,7 +1,11 @@
import { expect } from '@playwright/test';
import type { Scope } from '@sentry/browser';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';

sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.sentryLDIntegration = Sentry.launchDarklyIntegration();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1.0,
tracesSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
window.sentryLDIntegration,
],
});

// Manually mocking this because LD only has mock test utils for the React SDK.
// Also, no SDK has mock utils for FlagUsedHandler's.
const MockLaunchDarkly = {
initialize(_clientId, context, options) {
const flagUsedHandler = options.inspectors ? options.inspectors[0].method : undefined;

return {
variation(key, defaultValue) {
if (flagUsedHandler) {
flagUsedHandler(key, { value: defaultValue }, context);
}
return defaultValue;
},
};
},
};

window.initializeLD = () => {
return MockLaunchDarkly.initialize(
'example-client-id',
{ kind: 'user', key: 'example-context-key' },
{ inspectors: [Sentry.buildLaunchDarklyFlagUsedHandler()] },
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const btnStartSpan = document.getElementById('btnStartSpan');
const btnEndSpan = document.getElementById('btnEndSpan');
const btnStartNestedSpan = document.getElementById('btnStartNestedSpan');
const btnEndNestedSpan = document.getElementById('btnEndNestedSpan');

window.withNestedSpans = callback => {
window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => {
window.traceId = rootSpan.spanContext().traceId;

window.Sentry.startSpan({ name: 'test-span' }, _span => {
window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => {
callback();
});
});
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="btnStartSpan">Start Span</button>
<button id="btnEndSpan">End Span</button>
<button id="btnStartNestedSpan">Start Nested Span</button>
<button id="btnEndNestedSpan">End Nested Span</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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,
eventAndTraceHeaderRequestParser,
getMultipleSentryEnvelopeRequests,
shouldSkipFeatureFlagsTest,
shouldSkipTracingTest,
} from '../../../../../utils/helpers';

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

await page.route('https://dsn.ingest.sentry.io/**/*', route => {
return route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({}),
});
});

const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
await page.goto(url);

const envelopeRequestPromise = getMultipleSentryEnvelopeRequests<EventAndTraceHeader>(
page,
1,
{},
eventAndTraceHeaderRequestParser,
);

// withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span.
await page.evaluate(maxFlags => {
(window as any).withNestedSpans(() => {
const ldClient = (window as any).initializeLD();
for (let i = 1; i <= maxFlags; i++) {
ldClient.variation(`feat${i}`, false);
}
ldClient.variation(`feat${maxFlags + 1}`, true); // dropped
ldClient.variation('feat3', true); // update
});
return true;
}, MAX_FLAGS_PER_SPAN);

const event = (await envelopeRequestPromise)[0][0];
const innerSpan = event.spans?.[0];
const outerSpan = event.spans?.[1];
const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) =>
key.startsWith('flag.evaluation'),
);
const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) =>
key.startsWith('flag.evaluation'),
);

expect(innerSpanFlags).toEqual([]);

const expectedOuterSpanFlags = [];
for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) {
expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]);
}
// Order agnostic (attribute dict is unordered).
expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort());
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../constants';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';

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,7 +1,11 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
import { FLAG_BUFFER_SIZE } from '../../constants';
import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';

sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { expect } from '@playwright/test';
import type { Scope } from '@sentry/browser';
import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers';
import { sentryTest } from '../../../../../../utils/fixtures';
import {
envelopeRequestParser,
shouldSkipFeatureFlagsTest,
waitForErrorRequest,
} from '../../../../../../utils/helpers';

sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => {
if (shouldSkipFeatureFlagsTest()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;
window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
sampleRate: 1.0,
tracesSampleRate: 1.0,
integrations: [
window.sentryOpenFeatureIntegration,
Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }),
],
});

window.initialize = () => {
return {
getBooleanValue(flag, value) {
let hook = new Sentry.OpenFeatureIntegrationHook();
hook.after(null, { flagKey: flag, value: value });
return value;
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const btnStartSpan = document.getElementById('btnStartSpan');
const btnEndSpan = document.getElementById('btnEndSpan');
const btnStartNestedSpan = document.getElementById('btnStartNestedSpan');
const btnEndNestedSpan = document.getElementById('btnEndNestedSpan');

window.withNestedSpans = callback => {
window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => {
window.traceId = rootSpan.spanContext().traceId;

window.Sentry.startSpan({ name: 'test-span' }, _span => {
window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => {
callback();
});
});
});
};
Loading
Loading