Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion flagsmith-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,9 +983,14 @@ const Flagsmith = class {
}) => {
// No-op when events are disabled, mirroring enableAnalytics: false.
if (!this.eventProcessor) return;
const identifier = opts?.identifier ?? this.evaluationContext.identity?.identifier ?? null;
if (!identifier) {
this.log(`Flagsmith: trackExposureEvent called for "${featureName}" without an identity; call identify() (optionally with transient: true) or pass opts.identifier. No exposure recorded.`);
return;
}
this.eventProcessor.trackExposureEvent({
featureName,
identifier: opts?.identifier ?? this.evaluationContext.identity?.identifier ?? null,
identifier,
value: opts?.value ?? null,
traits: resolveTraitValues(opts?.traits ?? this.evaluationContext.identity?.traits),
metadata: opts?.metadata ?? null,
Expand Down
41 changes: 41 additions & 0 deletions test/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,47 @@ describe('trackEvent', () => {
});
});

describe('trackExposureEvent', () => {
test('skips the exposure when no identity resolves', async () => {
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig());
await flagsmith.init(initConfig); // anonymous

flagsmith.trackExposureEvent('font_size', { value: 'control' });
await flagsmith.flushEvents();

expect(eventCalls(mockFetch)).toHaveLength(0);
});

test('explicit identifier records the exposure without a context identity', async () => {
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig());
await flagsmith.init(initConfig); // anonymous

flagsmith.trackExposureEvent('font_size', { identifier: 'anon-device-1', value: 'control' });
await flagsmith.flushEvents();

const events = JSON.parse(eventCalls(mockFetch)[0][1].body).events;
expect(events).toHaveLength(1);
expect(events[0]).toEqual(expect.objectContaining({
event: FLAG_EXPOSURE_EVENT,
feature_name: 'font_size',
identifier: 'anon-device-1',
value: 'control',
}));
});

test('trackEvent still sends anonymous events with identifier null', async () => {
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig());
await flagsmith.init(initConfig); // anonymous

flagsmith.trackEvent('purchase');
await flagsmith.flushEvents();

const events = JSON.parse(eventCalls(mockFetch)[0][1].body).events;
expect(events).toHaveLength(1);
expect(events[0]).toEqual(expect.objectContaining({ event: 'purchase', identifier: null }));
});
});

describe('getExperimentFlag', () => {
test('returns the flag and fires one $flag_exposure when identified and source is SERVER', async () => {
const { flagsmith, initConfig, mockFetch } = getFlagsmith(eventsConfig({ identity: experimentIdentity }));
Expand Down
3 changes: 2 additions & 1 deletion types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,8 @@ T extends string = string
/**
* Record that an identity was exposed to a flag/variant (emits the reserved
* "$flag_exposure" event). No-op when events are disabled (enableEvents is
* not set).
* not set). Skipped (with a log) when no identifier resolves — identify()
* first (optionally with transient: true) or pass opts.identifier.
* @experimental @internal
*/
trackExposureEvent: (featureName: string, opts?: {
Expand Down
Loading