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
24 changes: 18 additions & 6 deletions packages/browser/src/experimentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,27 +107,39 @@ export class ExperimentClient implements Client {
/**
* Returns the variant for the provided key.
*
* Fetches {@link all} variants, falling back on the given fallback, then the
* configured fallbackVariant.
* Access the variant from {@link Source}, falling back on the given
* fallback, then the configured fallbackVariant.
*
* If an {@link ExperimentAnalyticsProvider} is configured, this function will
* call the provider with an {@link ExposureEvent}. The exposure event does
* not count towards your event volume within Amplitude.
*
* @param key The key to get the variant for.
* @param fallback The highest priority fallback.
* @see ExperimentConfig
* @see ExperimentAnalyticsProvider
*/
public variant(key: string, fallback?: string | Variant): Variant {
if (!this.apiKey) {
return { value: undefined };
}
const sourceVariant = this.sourceVariants()[key];
if (sourceVariant?.value) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not if (sourceVariant) {?

I had to check but we do currently require a value so this existing code is correct.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, sourceVariant could be non-null with a null value, although this is definitely not the regular case and I'd doubt it'd ever happen. That being said, a null value means an invalid variant, which we do not want to track.

this.config.analyticsProvider?.track(
new ExposureEvent(
this.addContext(this.getUser()),
key,
this.convertVariant(sourceVariant),
),
);
}
const variant =
this.sourceVariants()[key] ??
sourceVariant ??
fallback ??
this.secondaryVariants()[key] ??
this.config.fallbackVariant;
const converted = this.convertVariant(variant);
this.debug(`[Experiment] variant for ${key} is ${converted.value}`);
this.config.analyticsProvider?.track(
new ExposureEvent(this.addContext(this.getUser()), key, converted),
);
return converted;
}

Expand Down
64 changes: 64 additions & 0 deletions packages/browser/test/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Source } from '../src/config';
import { ExperimentClient } from '../src/experimentClient';
import {
ExperimentAnalyticsEvent,
ExposureEvent,
} from '../src/types/analytics';
import { ExperimentAnalyticsProvider } from '../src/types/provider';
import { ExperimentUser, ExperimentUserProvider } from '../src/types/user';
import { Variant, Variants } from '../src/types/variant';

Expand Down Expand Up @@ -195,3 +200,62 @@ test('ExperimentClient.fetch, with user provider, success', async () => {
const variant = client.variant('sdk-ci-test');
expect(variant).toEqual({ value: 'on', payload: 'payload' });
});

/**
* Utility class for testing analytics provider & exposure tracking.
*/
class TestAnalyticsProvider implements ExperimentAnalyticsProvider {
public didTrack = false;
public block: (ExposureEvent) => void;
public constructor(block: (ExposureEvent) => void) {
this.block = block;
}
track(event: ExperimentAnalyticsEvent): void {
this.block(event);
this.didTrack = true;
}
}

/**
* Configure a client with an analytics provider which checks that a valid
* exposure event is tracked when the client's variant function is called.
*/
test('ExperimentClient.variant, with analytics provider, exposure tracked', async () => {
const analyticsProvider = new TestAnalyticsProvider(
(event: ExperimentAnalyticsEvent) => {
expect(event.name).toEqual('[Experiment] Exposure');
expect(event.properties).toEqual({
key: serverKey,
variant: serverVariant.value,
});
expect(event).toBeInstanceOf(ExposureEvent);
const exposureEvent = event as ExposureEvent;
expect(exposureEvent.key).toEqual(serverKey);
expect(exposureEvent.variant).toEqual(serverVariant);
},
);
const client = new ExperimentClient(API_KEY, {
analyticsProvider: analyticsProvider,
});
await client.fetch(testUser);
client.variant(serverKey);
expect(analyticsProvider.didTrack).toEqual(true);
});

/**
* Configure a client with an analytics provider which fails the test if called.
* Tests that the analytics provider is not called with an exposure event when
* the client exposes the user to a fallback/initial variant.
*/
test('ExperimentClient.variant, with analytics provider, exposure not tracked on fallback', async () => {
const analyticsProvider = new TestAnalyticsProvider(() => {
fail(
'analytics provider should not be called when user exposed to fallback',
);
});
const client = new ExperimentClient(API_KEY, {
analyticsProvider: analyticsProvider,
});
client.variant(initialKey);
client.variant(unknownKey);
});