Skip to content

Commit 0fae041

Browse files
authored
feat: Add public API for setting event dispatcher (#139)
* feat: Add public API for setting event dispatcher * nit * test * Make BoundedEventQueue implement NamedEventQueue * simplify * export BoundedEventQueue * cleanup import * add unused imports lint
1 parent 7904d04 commit 0fae041

File tree

9 files changed

+121
-28
lines changed

9 files changed

+121
-28
lines changed

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = {
1212
'plugin:promise/recommended',
1313
'plugin:import/recommended',
1414
],
15-
plugins: ['@typescript-eslint', 'prettier', 'import', 'promise'],
15+
plugins: ['@typescript-eslint', 'prettier', 'import', 'promise', 'unused-imports'],
1616
rules: {
1717
'prettier/prettier': [
1818
'warn',
@@ -24,6 +24,7 @@ module.exports = {
2424
],
2525
'import/named': 'off',
2626
'import/no-unresolved': 'off',
27+
'unused-imports/no-unused-imports': 'error',
2728
'@typescript-eslint/no-explicit-any': 'off',
2829
'import/order': [
2930
'warn',

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"eslint-plugin-import": "^2.25.4",
5656
"eslint-plugin-prettier": "^4.0.0",
5757
"eslint-plugin-promise": "^6.0.0",
58+
"eslint-plugin-unused-imports": "^4.1.4",
5859
"jest": "^29.7.0",
5960
"jest-environment-jsdom": "^29.7.0",
6061
"lodash": "^4.17.21",

src/client/eppo-client.ts

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
import { decodeFlag } from '../decoding';
2121
import { EppoValue } from '../eppo_value';
2222
import { Evaluator, FlagEvaluation, noneResult } from '../evaluator';
23-
import ArrayBackedNamedEventQueue from '../events/array-backed-named-event-queue';
2423
import { BoundedEventQueue } from '../events/bounded-event-queue';
2524
import EventDispatcher from '../events/event-dispatcher';
2625
import NoOpEventDispatcher from '../events/no-op-event-dispatcher';
@@ -79,21 +78,12 @@ export interface IContainerExperiment<T> {
7978
treatmentVariationEntries: Array<T>;
8079
}
8180

82-
const DEFAULT_EVENT_DISPATCHER_CONFIG = {
83-
// TODO: Replace with actual ingestion URL
84-
ingestionUrl: 'https://example.com/events',
85-
batchSize: 10,
86-
flushIntervalMs: 10_000,
87-
retryIntervalMs: 5_000,
88-
maxRetries: 3,
89-
};
90-
9181
export default class EppoClient {
92-
private readonly eventDispatcher: EventDispatcher;
82+
private eventDispatcher: EventDispatcher;
9383
private readonly assignmentEventsQueue: BoundedEventQueue<IAssignmentEvent> =
94-
newBoundedArrayEventQueue<IAssignmentEvent>('assignments');
84+
new BoundedEventQueue<IAssignmentEvent>('assignments');
9585
private readonly banditEventsQueue: BoundedEventQueue<IBanditEvent> =
96-
newBoundedArrayEventQueue<IBanditEvent>('bandit');
86+
new BoundedEventQueue<IBanditEvent>('bandit');
9787
private readonly banditEvaluator = new BanditEvaluator();
9888
private banditLogger?: IBanditLogger;
9989
private banditAssignmentCache?: AssignmentCache;
@@ -152,6 +142,12 @@ export default class EppoClient {
152142
this.banditVariationConfigurationStore = banditVariationConfigurationStore;
153143
}
154144

145+
/** Sets the EventDispatcher instance to use when tracking events with {@link track}. */
146+
// noinspection JSUnusedGlobalSymbols
147+
setEventDispatcher(eventDispatcher: EventDispatcher) {
148+
this.eventDispatcher = eventDispatcher;
149+
}
150+
155151
// noinspection JSUnusedGlobalSymbols
156152
setBanditModelConfigurationStore(
157153
banditModelConfigurationStore: IConfigurationStore<BanditParameters>,
@@ -1145,7 +1141,3 @@ export function checkValueTypeMatch(
11451141
return false;
11461142
}
11471143
}
1148-
1149-
function newBoundedArrayEventQueue<T>(name: string): BoundedEventQueue<T> {
1150-
return new BoundedEventQueue<T>(new ArrayBackedNamedEventQueue<T>(name));
1151-
}

src/events/bounded-event-queue.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,32 @@ import { MAX_EVENT_QUEUE_SIZE } from '../constants';
44
import NamedEventQueue from './named-event-queue';
55

66
/** A bounded event queue that drops events when it reaches its maximum size. */
7-
export class BoundedEventQueue<T> {
7+
export class BoundedEventQueue<T> implements NamedEventQueue<T> {
88
constructor(
9-
private readonly queue: NamedEventQueue<T>,
9+
readonly name: string,
10+
private readonly queue = new Array<T>(),
1011
private readonly maxSize = MAX_EVENT_QUEUE_SIZE,
1112
) {}
1213

14+
length = this.queue.length;
15+
16+
splice(count: number): T[] {
17+
return this.queue.splice(count);
18+
}
19+
20+
isEmpty(): boolean {
21+
return this.queue.length === 0;
22+
}
23+
24+
[Symbol.iterator](): IterableIterator<T> {
25+
return this.queue[Symbol.iterator]();
26+
}
27+
1328
push(event: T) {
1429
if (this.queue.length < this.maxSize) {
1530
this.queue.push(event);
1631
} else {
17-
logger.warn(`Dropping event for queue ${this.queue.name} since the queue is full`);
32+
logger.warn(`Dropping event for queue ${this.name} since the queue is full`);
1833
}
1934
}
2035

src/events/default-event-dispatcher.spec.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { resolve } from 'eslint-import-resolver-typescript';
2-
31
import ArrayBackedNamedEventQueue from './array-backed-named-event-queue';
42
import BatchEventProcessor from './batch-event-processor';
5-
import DefaultEventDispatcher, { EventDispatcherConfig } from './default-event-dispatcher';
3+
import DefaultEventDispatcher, {
4+
EventDispatcherConfig,
5+
newDefaultEventDispatcher,
6+
} from './default-event-dispatcher';
67
import { Event } from './event-dispatcher';
78
import NetworkStatusListener from './network-status-listener';
89

@@ -198,4 +199,26 @@ describe('DefaultEventDispatcher', () => {
198199
expect(global.fetch).toHaveBeenCalled();
199200
});
200201
});
202+
203+
describe('newDefaultEventDispatcher', () => {
204+
it('should throw if SDK key is invalid', () => {
205+
expect(() => {
206+
newDefaultEventDispatcher(
207+
new ArrayBackedNamedEventQueue('test-queue'),
208+
mockNetworkStatusListener,
209+
'invalid-sdk-key',
210+
);
211+
}).toThrow('Unable to parse Event ingestion URL from SDK key');
212+
});
213+
214+
it('should create a new DefaultEventDispatcher with the provided configuration', () => {
215+
const eventQueue = new ArrayBackedNamedEventQueue('test-queue');
216+
const dispatcher = newDefaultEventDispatcher(
217+
eventQueue,
218+
mockNetworkStatusListener,
219+
'zCsQuoHJxVPp895.ZWg9MTIzNDU2LmUudGVzdGluZy5lcHBvLmNsb3Vk',
220+
);
221+
expect(dispatcher).toBeInstanceOf(DefaultEventDispatcher);
222+
});
223+
});
201224
});

src/events/default-event-dispatcher.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import BatchEventProcessor from './batch-event-processor';
44
import BatchRetryManager from './batch-retry-manager';
55
import EventDelivery from './event-delivery';
66
import EventDispatcher, { Event } from './event-dispatcher';
7+
import NamedEventQueue from './named-event-queue';
78
import NetworkStatusListener from './network-status-listener';
9+
import SdkKeyDecoder from './sdk-key-decoder';
810

911
export type EventDispatcherConfig = {
1012
// target url to deliver events to
@@ -19,6 +21,16 @@ export type EventDispatcherConfig = {
1921
maxRetries?: number;
2022
};
2123

24+
// TODO: Have more realistic default batch size based on average event payload size once we have
25+
// more concrete data.
26+
export const DEFAULT_EVENT_DISPATCHER_BATCH_SIZE = 100;
27+
export const DEFAULT_EVENT_DISPATCHER_CONFIG: Omit<EventDispatcherConfig, 'ingestionUrl'> = {
28+
deliveryIntervalMs: 10_000,
29+
retryIntervalMs: 5_000,
30+
maxRetryDelayMs: 30_000,
31+
maxRetries: 3,
32+
};
33+
2234
/**
2335
* @internal
2436
* An {@link EventDispatcher} that, given the provided config settings, delivers events in batches
@@ -37,6 +49,7 @@ export default class DefaultEventDispatcher implements EventDispatcher {
3749
private readonly networkStatusListener: NetworkStatusListener,
3850
config: EventDispatcherConfig,
3951
) {
52+
this.ensureConfigFields(config);
4053
this.eventDelivery = new EventDelivery(config.ingestionUrl);
4154
this.retryManager = new BatchRetryManager(this.eventDelivery, {
4255
retryIntervalMs: config.retryIntervalMs,
@@ -94,4 +107,39 @@ export default class DefaultEventDispatcher implements EventDispatcher {
94107
this.dispatchTimer = setTimeout(() => this.deliverNextBatch(), this.deliveryIntervalMs);
95108
}
96109
}
110+
111+
private ensureConfigFields(config: EventDispatcherConfig) {
112+
if (!config.ingestionUrl) {
113+
throw new Error('Missing required ingestionUrl in EventDispatcherConfig');
114+
}
115+
if (!config.deliveryIntervalMs) {
116+
throw new Error('Missing required deliveryIntervalMs in EventDispatcherConfig');
117+
}
118+
if (!config.retryIntervalMs) {
119+
throw new Error('Missing required retryIntervalMs in EventDispatcherConfig');
120+
}
121+
if (!config.maxRetryDelayMs) {
122+
throw new Error('Missing required maxRetryDelayMs in EventDispatcherConfig');
123+
}
124+
}
125+
}
126+
127+
/** Creates a new {@link DefaultEventDispatcher} with the provided configuration. */
128+
export function newDefaultEventDispatcher(
129+
eventQueue: NamedEventQueue<unknown>,
130+
networkStatusListener: NetworkStatusListener,
131+
sdkKey: string,
132+
batchSize: number = DEFAULT_EVENT_DISPATCHER_BATCH_SIZE,
133+
config: Omit<EventDispatcherConfig, 'ingestionUrl'> = DEFAULT_EVENT_DISPATCHER_CONFIG,
134+
): EventDispatcher {
135+
const sdkKeyDecoder = new SdkKeyDecoder();
136+
const ingestionUrl = sdkKeyDecoder.decodeEventIngestionHostName(sdkKey);
137+
if (!ingestionUrl) {
138+
throw new Error('Unable to parse Event ingestion URL from SDK key');
139+
}
140+
return new DefaultEventDispatcher(
141+
new BatchEventProcessor(eventQueue, batchSize),
142+
networkStatusListener,
143+
{ ...config, ingestionUrl },
144+
);
97145
}

src/events/event-delivery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { logger } from '../application-logger';
22

33
export default class EventDelivery {
4-
constructor(private ingestionUrl: string) {}
4+
constructor(private readonly ingestionUrl: string) {}
55

66
async deliver(batch: unknown[]): Promise<boolean> {
77
try {

src/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ import {
2929
import { HybridConfigurationStore } from './configuration-store/hybrid.store';
3030
import { MemoryStore, MemoryOnlyConfigurationStore } from './configuration-store/memory.store';
3131
import * as constants from './constants';
32-
import ArrayBackedNamedEventQueue from './events/array-backed-named-event-queue';
3332
import BatchEventProcessor from './events/batch-event-processor';
34-
import DefaultEventDispatcher from './events/default-event-dispatcher';
33+
import { BoundedEventQueue } from './events/bounded-event-queue';
34+
import DefaultEventDispatcher, {
35+
DEFAULT_EVENT_DISPATCHER_CONFIG,
36+
DEFAULT_EVENT_DISPATCHER_BATCH_SIZE,
37+
newDefaultEventDispatcher,
38+
} from './events/default-event-dispatcher';
3539
import EventDispatcher from './events/event-dispatcher';
3640
import NamedEventQueue from './events/named-event-queue';
3741
import NetworkStatusListener from './events/network-status-listener';
@@ -93,9 +97,13 @@ export {
9397
BanditSubjectAttributes,
9498
BanditActions,
9599

96-
// event queue types
100+
// event dispatcher types
97101
NamedEventQueue,
98102
EventDispatcher,
103+
BoundedEventQueue,
104+
DEFAULT_EVENT_DISPATCHER_CONFIG,
105+
DEFAULT_EVENT_DISPATCHER_BATCH_SIZE,
106+
newDefaultEventDispatcher,
99107
BatchEventProcessor,
100108
NetworkStatusListener,
101109
DefaultEventDispatcher,

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,6 +1831,11 @@ eslint-plugin-promise@^6.0.0:
18311831
resolved "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz"
18321832
integrity sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==
18331833

1834+
eslint-plugin-unused-imports@^4.1.4:
1835+
version "4.1.4"
1836+
resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz#62ddc7446ccbf9aa7b6f1f0b00a980423cda2738"
1837+
integrity sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==
1838+
18341839
[email protected], eslint-scope@^5.1.1:
18351840
version "5.1.1"
18361841
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"

0 commit comments

Comments
 (0)