Skip to content

Commit 9c4533c

Browse files
authored
fix: skip exposure events when no identifier resolves (#407)
1 parent 41c09d1 commit 9c4533c

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

flagsmith-core.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -983,9 +983,14 @@ const Flagsmith = class {
983983
}) => {
984984
// No-op when events are disabled, mirroring enableAnalytics: false.
985985
if (!this.eventProcessor) return;
986+
const identifier = opts?.identifier ?? this.evaluationContext.identity?.identifier ?? null;
987+
if (!identifier) {
988+
this.log(`Flagsmith: trackExposureEvent called for "${featureName}" without an identity; call identify() (optionally with transient: true) or pass opts.identifier. No exposure recorded.`);
989+
return;
990+
}
986991
this.eventProcessor.trackExposureEvent({
987992
featureName,
988-
identifier: opts?.identifier ?? this.evaluationContext.identity?.identifier ?? null,
993+
identifier,
989994
value: opts?.value ?? null,
990995
traits: resolveTraitValues(opts?.traits ?? this.evaluationContext.identity?.traits),
991996
metadata: opts?.metadata ?? null,

test/events.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,47 @@ describe('trackEvent', () => {
9999
});
100100
});
101101

102+
describe('trackExposureEvent', () => {
103+
test('skips the exposure when no identity resolves', async () => {
104+
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig());
105+
await flagsmith.init(initConfig); // anonymous
106+
107+
flagsmith.trackExposureEvent('font_size', { value: 'control' });
108+
await flagsmith.flushEvents();
109+
110+
expect(eventCalls(mockFetch)).toHaveLength(0);
111+
});
112+
113+
test('explicit identifier records the exposure without a context identity', async () => {
114+
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig());
115+
await flagsmith.init(initConfig); // anonymous
116+
117+
flagsmith.trackExposureEvent('font_size', { identifier: 'anon-device-1', value: 'control' });
118+
await flagsmith.flushEvents();
119+
120+
const events = JSON.parse(eventCalls(mockFetch)[0][1].body).events;
121+
expect(events).toHaveLength(1);
122+
expect(events[0]).toEqual(expect.objectContaining({
123+
event: FLAG_EXPOSURE_EVENT,
124+
feature_name: 'font_size',
125+
identifier: 'anon-device-1',
126+
value: 'control',
127+
}));
128+
});
129+
130+
test('trackEvent still sends anonymous events with identifier null', async () => {
131+
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig());
132+
await flagsmith.init(initConfig); // anonymous
133+
134+
flagsmith.trackEvent('purchase');
135+
await flagsmith.flushEvents();
136+
137+
const events = JSON.parse(eventCalls(mockFetch)[0][1].body).events;
138+
expect(events).toHaveLength(1);
139+
expect(events[0]).toEqual(expect.objectContaining({ event: 'purchase', identifier: null }));
140+
});
141+
});
142+
102143
describe('getExperimentFlag', () => {
103144
test('returns the flag and fires one $flag_exposure when identified and source is SERVER', async () => {
104145
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig({ identity: experimentIdentity }));

types.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ T extends string = string
307307
/**
308308
* Record that an identity was exposed to a flag/variant (emits the reserved
309309
* "$flag_exposure" event). No-op when events are disabled (enableEvents is
310-
* not set).
310+
* not set). Skipped (with a log) when no identifier resolves — identify()
311+
* first (optionally with transient: true) or pass opts.identifier.
311312
* @experimental @internal
312313
*/
313314
trackExposureEvent: (featureName: string, opts?: {

0 commit comments

Comments
 (0)