Skip to content

Commit bbb6c8d

Browse files
committed
add node integration tests
1 parent 3b0b82d commit bbb6c8d

File tree

5 files changed

+176
-0
lines changed

5 files changed

+176
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
// Minimal GrowthBook-like class that matches the real API for testing
6+
// This is necessary since we don't want to add @growthbook/growthbook as a dependency
7+
// just for integration tests, but we want to test the actual integration behavior
8+
class GrowthBookLike {
9+
private _features: Record<string, { value: unknown }> = {};
10+
11+
public isOn(featureKey: string): boolean {
12+
const feature = this._features[featureKey];
13+
return feature ? !!feature.value : false;
14+
}
15+
16+
public getFeatureValue(featureKey: string, defaultValue: unknown): unknown {
17+
const feature = this._features[featureKey];
18+
return feature ? feature.value : defaultValue;
19+
}
20+
21+
// Helper method to set feature values for testing
22+
public setFeature(featureKey: string, value: unknown): void {
23+
this._features[featureKey] = { value };
24+
}
25+
}
26+
27+
Sentry.init({
28+
dsn: 'https://[email protected]/1337',
29+
sampleRate: 1.0,
30+
transport: loggingTransport,
31+
integrations: [Sentry.growthbookIntegration({ growthbookClass: GrowthBookLike })],
32+
});
33+
34+
const gb = new GrowthBookLike();
35+
36+
// Fill buffer with flags 1-100 (all false by default)
37+
for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) {
38+
gb.isOn(`feat${i}`);
39+
}
40+
41+
// Add feat101 (true), which should evict feat1
42+
gb.setFeature(`feat${FLAG_BUFFER_SIZE + 1}`, true);
43+
gb.isOn(`feat${FLAG_BUFFER_SIZE + 1}`);
44+
45+
// Update feat3 to true, which should move it to the end
46+
gb.setFeature('feat3', true);
47+
gb.isOn('feat3');
48+
49+
// Test getFeatureValue with boolean values (should be captured)
50+
gb.setFeature('bool-feat', true);
51+
gb.getFeatureValue('bool-feat', false);
52+
53+
// Test getFeatureValue with non-boolean values (should NOT be captured)
54+
gb.setFeature('string-feat', 'hello');
55+
gb.getFeatureValue('string-feat', 'default');
56+
gb.setFeature('number-feat', 42);
57+
gb.getFeatureValue('number-feat', 0);
58+
59+
throw new Error('Test error');
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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('GrowthBook flags captured on error with eviction, update, and no async tasks', async () => {
10+
11+
const expectedFlags = [];
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+
expectedFlags.push({ flag: 'bool-feat', result: true }); // Only boolean getFeatureValue should be captured
18+
19+
await createRunner(__dirname, 'scenario.ts')
20+
.expect({
21+
event: {
22+
exception: { values: [{ type: 'Error', value: 'Test error' }] },
23+
contexts: {
24+
flags: {
25+
values: expectedFlags,
26+
},
27+
},
28+
},
29+
})
30+
.start()
31+
.completed();
32+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
// Minimal GrowthBook-like class that matches the real API for testing
5+
class GrowthBookLike {
6+
private _features: Record<string, { value: unknown }> = {};
7+
8+
public isOn(featureKey: string): boolean {
9+
const feature = this._features[featureKey];
10+
return feature ? !!feature.value : false;
11+
}
12+
13+
public getFeatureValue(featureKey: string, defaultValue: unknown): unknown {
14+
const feature = this._features[featureKey];
15+
return feature ? feature.value : defaultValue;
16+
}
17+
18+
// Helper method to set feature values for testing
19+
public setFeature(featureKey: string, value: unknown): void {
20+
this._features[featureKey] = { value };
21+
}
22+
}
23+
24+
Sentry.init({
25+
dsn: 'https://[email protected]/1337',
26+
sampleRate: 1.0,
27+
tracesSampleRate: 1.0,
28+
transport: loggingTransport,
29+
integrations: [Sentry.growthbookIntegration({ growthbookClass: GrowthBookLike })],
30+
});
31+
32+
const gb = new GrowthBookLike();
33+
34+
// Set up feature flags
35+
gb.setFeature('feat1', true);
36+
gb.setFeature('feat2', false);
37+
gb.setFeature('bool-feat', true);
38+
39+
Sentry.startSpan({ name: 'test-span', op: 'function' }, () => {
40+
// Evaluate feature flags during the span
41+
gb.isOn('feat1');
42+
gb.isOn('feat2');
43+
44+
// Test getFeatureValue with boolean values (should be captured)
45+
gb.getFeatureValue('bool-feat', false);
46+
47+
// Test getFeatureValue with non-boolean values (should NOT be captured)
48+
gb.setFeature('string-feat', 'hello');
49+
gb.getFeatureValue('string-feat', 'default');
50+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { afterAll, expect, test } from 'vitest';
2+
import { cleanupChildProcesses, createRunner } from '../../../../utils/runner';
3+
4+
afterAll(() => {
5+
cleanupChildProcesses();
6+
});
7+
8+
test('GrowthBook flags are added to active span attributes on span end', async () => {
9+
await createRunner(__dirname, 'scenario.ts')
10+
.expect({
11+
transaction: {
12+
contexts: {
13+
trace: {
14+
data: {
15+
'flag.evaluation.feat1': true,
16+
'flag.evaluation.feat2': false,
17+
'flag.evaluation.bool-feat': true,
18+
// string-feat should NOT be here since it's not boolean
19+
},
20+
op: 'function',
21+
origin: 'manual',
22+
status: 'ok',
23+
span_id: expect.stringMatching(/[a-f0-9]{16}/),
24+
trace_id: expect.stringMatching(/[a-f0-9]{32}/)
25+
},
26+
},
27+
spans: [],
28+
transaction: 'test-span',
29+
type: 'transaction',
30+
},
31+
})
32+
.start()
33+
.completed();
34+
});

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export {
3434
OpenFeatureIntegrationHook,
3535
statsigIntegration,
3636
unleashIntegration,
37+
growthbookIntegration,
3738
} from './integrations/featureFlagShims';
3839
export { firebaseIntegration } from './integrations/tracing/firebase';
3940

0 commit comments

Comments
 (0)