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
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
1.4.1 (June 13, 2022)
- Bugfixing - Updated submitters logic, to avoid dropping impressions and events that are being tracked while POST request is pending.

1.4.0 (May 24, 2022)
- Added `scheduler.telemetryRefreshRate` property to SDK configuration, and deprecated `scheduler.metricsRefreshRate` property.
- Updated SDK telemetry storage, metrics and updater to be more effective and send less often.
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-commons",
"version": "1.4.0",
"version": "1.4.1",
"description": "Split Javascript SDK common components",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down
8 changes: 4 additions & 4 deletions src/listeners/__tests__/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jest.mock('../../sync/submitters/telemetrySubmitter', () => {
return {
isEmpty: () => false,
clear: () => { },
state: () => ({}),
pop: () => ({}),
};
}
};
Expand Down Expand Up @@ -43,21 +43,21 @@ const fakeStorageOptimized = { // @ts-expect-error
impressions: {
isEmpty: jest.fn(),
clear: jest.fn(),
state() {
pop() {
return [fakeImpression];
}
} as IImpressionsCacheSync, // @ts-expect-error
events: {
isEmpty: jest.fn(),
clear: jest.fn(),
state() {
pop() {
return [fakeEvent];
}
} as IEventsCacheSync, // @ts-expect-error
impressionCounts: {
isEmpty: jest.fn(),
clear: jest.fn(),
state() {
pop() {
return fakeImpressionCounts;
}
} as IImpressionCountsCacheSync,
Expand Down
4 changes: 2 additions & 2 deletions src/listeners/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ export class BrowserSignalListener implements ISignalListener {
if (this.syncManager.pushManager) this.syncManager.pushManager.stop();
}

private _flushData<TState>(url: string, cache: IRecorderCacheProducerSync<TState>, postService: (body: string) => Promise<IResponse>, fromCacheToPayload?: (cacheData: TState) => any, extraMetadata?: {}) {
private _flushData<T>(url: string, cache: IRecorderCacheProducerSync<T>, postService: (body: string) => Promise<IResponse>, fromCacheToPayload?: (cacheData: T) => any, extraMetadata?: {}) {
// if there is data in cache, send it to backend
if (!cache.isEmpty()) {
const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.state()) : cache.state();
const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.pop()) : cache.pop();
if (!this._sendBeacon(url, dataPayload, extraMetadata)) {
postService(JSON.stringify(dataPayload)).catch(() => { }); // no-op just to catch a possible exception
}
Expand Down
2 changes: 1 addition & 1 deletion src/logger/messages/warn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const codesWarn: [number, string][] = codesError.concat([
[c.STREAMING_PARSING_ERROR_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE error notification: %s'],
[c.STREAMING_PARSING_MESSAGE_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE message notification: %s'],
[c.STREAMING_FALLBACK, c.LOG_PREFIX_SYNC_STREAMING + 'Falling back to polling mode. Reason: %s'],
[c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s after retry. Reason: %s.'],
[c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Dropping %s after retry. Reason: %s.'],
[c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s, keeping data to retry on next iteration. Reason: %s.'],
// client status
[c.CLIENT_NOT_READY, '%s: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method.'],
Expand Down
2 changes: 1 addition & 1 deletion src/storages/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function assertStorageInterface(storage: IStorageSync | IStorageAsync) {
export function assertSyncRecorderCacheInterface(cache: IEventsCacheSync | IImpressionsCacheSync) {
expect(typeof cache.isEmpty).toBe('function');
expect(typeof cache.clear).toBe('function');
expect(typeof cache.state).toBe('function');
expect(typeof cache.pop).toBe('function');
expect(typeof cache.track).toBe('function');
}

Expand Down
8 changes: 5 additions & 3 deletions src/storages/inMemory/EventsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ export class EventsCacheInMemory implements IEventsCacheSync {
}

/**
* Get the collected data, used as payload for posting.
* Pop the collected data, used as payload for posting.
*/
state() {
return this.queue;
pop(toMerge?: SplitIO.EventData[]) {
const data = this.queue;
this.clear();
return toMerge ? toMerge.concat(data) : data;
}

/**
Expand Down
25 changes: 21 additions & 4 deletions src/storages/inMemory/ImpressionCountsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,34 @@ export class ImpressionCountsCacheInMemory implements IImpressionCountsCacheSync
this.cache[key] = currentAmount ? currentAmount + amount : amount;
}



/**
* Returns all the elements stored in the cache and resets the cache.
*/
state() {
return this.cache;
* Pop the collected data, used as payload for posting.
*/
pop(toMerge?: Record<string, number>) {
const data = this.cache;
this.clear();
if (toMerge) {
Object.keys(data).forEach((key) => {
if (toMerge[key]) toMerge[key] += data[key];
else toMerge[key] = data[key];
});
return toMerge;
}
return data;
}

/**
* Clear the data stored on the cache.
*/
clear() {
this.cache = {};
}

/**
* Check if the cache is empty.
*/
isEmpty() {
return Object.keys(this.cache).length === 0;
}
Expand Down
8 changes: 5 additions & 3 deletions src/storages/inMemory/ImpressionsCacheInMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ export class ImpressionsCacheInMemory implements IImpressionsCacheSync {
}

/**
* Get the collected data, used as payload for posting.
* Pop the collected data, used as payload for posting.
*/
state() {
return this.queue;
pop(toMerge?: ImpressionDTO[]) {
const data = this.queue;
this.clear();
return toMerge ? toMerge.concat(data) : data;
}

/**
Expand Down
24 changes: 15 additions & 9 deletions src/storages/inMemory/__tests__/EventsCacheInMemory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ test('EVENTS CACHE / Should be able to instantiate and start with an empty queue
const createInstance = () => cache = new EventsCacheInMemory(500); // 500 as eventsQueueSize

expect(createInstance).not.toThrow(); // Creation should not throw.
expect(cache.state()).toEqual([]); // The queue starts empty.
expect(cache.pop()).toEqual([]); // The queue starts empty.
});

test('EVENTS CACHE / Should be able to add items sequentially and retrieve the queue', () => {
Expand All @@ -19,10 +19,11 @@ test('EVENTS CACHE / Should be able to add items sequentially and retrieve the q
cache.track(queueValues[2]);
cache.track(queueValues[3]);

const state = cache.state();
const items = cache.pop();

expect(state.length).toBe(4 /* pushed 4 items */); // The amount of items on queue should match the amount we pushed
expect(state).toEqual(queueValues); // The items should be in the queue and ordered as they were added.
expect(items.length).toBe(4 /* pushed 4 items */); // The amount of items on queue should match the amount we pushed
expect(items).toEqual(queueValues); // The items should be in the queue and ordered as they were added.
expect(cache.isEmpty()).toEqual(true); // Queue is empty
});

test('EVENTS CACHE / Should be able to clear the queue and accumulated byte size', () => {
Expand All @@ -31,20 +32,21 @@ test('EVENTS CACHE / Should be able to clear the queue and accumulated byte size
cache.track('test1', 2019);
cache.clear();

expect(cache.state()).toEqual([]); // The queue should be clear.
expect(cache.pop()).toEqual([]); // The queue should be clear.
expect(cache.queueByteSize).toBe(0); // The accumulated byte size should had been cleared.
});

test('EVENTS CACHE / Should be able to tell if the queue is empty', () => {
const cache = new EventsCacheInMemory(500);

expect(cache.state().length === 0).toBe(true); // The queue is empty,
expect(cache.pop().length).toBe(0); // The queue is empty,
expect(cache.isEmpty()).toBe(true); // so if it is empty, it returns true.

cache.track('test');

expect(cache.state().length > 0).toBe(true); // If we add something to the queue,
expect(cache.isEmpty()).toBe(false); // it will return false.

expect(cache.pop().length).toBe(1); // If we add something to the queue,
expect(cache.isEmpty()).toBe(true); // it will return true.
});

test('EVENTS CACHE / Should be able to return the DTO we will send to BE', () => {
Expand All @@ -56,8 +58,12 @@ test('EVENTS CACHE / Should be able to return the DTO we will send to BE', () =>
cache.track(queueValues[2]);
cache.track(queueValues[3]);

const json = cache.state();
const json = cache.pop();
expect(json).toEqual(queueValues); // For now the DTO is just an array of the saved events.

// pop with merge
cache.track(0); cache.track(1);
expect(cache.pop([2, 3, 4])).toEqual([2, 3, 4, 0, 1]);
});

test('EVENTS CACHE / Should call "onFullQueueCb" when the queue is full (count wise).', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@ test('IMPRESSION COUNTS CACHE / Impression Counter Test BasicUsage', () => {
counter.track('feature2', timestamp + 3, 2);
counter.track('feature2', timestamp + 4, 2);

const counted = counter.state();
const counted = counter.pop();
expect(Object.keys(counted).length).toBe(2);
expect(counted[counter._makeKey('feature1', timestamp)]).toBe(3);
expect(counted[counter._makeKey('feature2', timestamp)]).toBe(4);

// pop with merge
counter.track('feature1', timestamp, 1);
counter.track('feature3', timestamp, 10);
const countedWithMerge = counter.pop(counted);
expect(Object.keys(countedWithMerge).length).toBe(3);
expect(countedWithMerge[counter._makeKey('feature1', timestamp)]).toBe(4);
expect(countedWithMerge[counter._makeKey('feature2', timestamp)]).toBe(4);
expect(countedWithMerge[counter._makeKey('feature3', timestamp)]).toBe(10);

counter.clear();
expect(Object.keys(counter.state()).length).toBe(0);
expect(Object.keys(counter.pop()).length).toBe(0);

const nextHourTimestamp = new Date(2020, 9, 2, 11, 10, 12).getTime();
counter.track('feature1', timestamp, 1);
Expand All @@ -38,12 +48,13 @@ test('IMPRESSION COUNTS CACHE / Impression Counter Test BasicUsage', () => {
counter.track('feature1', nextHourTimestamp + 2, 1);
counter.track('feature2', nextHourTimestamp + 3, 2);
counter.track('feature2', nextHourTimestamp + 4, 2);
const counted2 = counter.state();
expect(counter.isEmpty()).toBe(false);
const counted2 = counter.pop();
expect(counter.isEmpty()).toBe(true);
expect(Object.keys(counted2).length).toBe(4);
expect(counted2[counter._makeKey('feature1', timestamp)]).toBe(3);
expect(counted2[counter._makeKey('feature2', timestamp)]).toBe(4);
expect(counted2[counter._makeKey('feature1', nextHourTimestamp)]).toBe(3);
expect(counted2[counter._makeKey('feature2', nextHourTimestamp)]).toBe(4);
counter.clear();
expect(Object.keys(counter.state()).length).toBe(0);
expect(Object.keys(counter.pop()).length).toBe(0);
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ test('IMPRESSIONS CACHE IN MEMORY / should incrementally store values, clear the
const c = new ImpressionsCacheInMemory();

// queue is initially empty
expect(c.state()).toEqual([]);
expect(c.pop()).toEqual([]);
expect(c.isEmpty()).toBe(true);


c.track([0]);
c.track([1, 2]);
c.track([3]);

expect(c.state()).toEqual([0, 1, 2, 3]); // all the items should be stored in sequential order
expect(c.isEmpty()).toBe(false);
expect(c.pop()).toEqual([0, 1, 2, 3]); // all the items should be stored in sequential order
expect(c.isEmpty()).toBe(true);

// pop with merge
c.track([0]); c.track([1]);
expect(c.pop([2, 3, 4])).toEqual([2, 3, 4, 0, 1]);

// should empty the queue
c.track([0]);
c.clear();
expect(c.state()).toEqual([]);
expect(c.pop()).toEqual([]);
expect(c.isEmpty()).toBe(true);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test('EVENTS CACHE IN REDIS / `track`, `count`, `popNWithMetadata` and `drop` me
await Promise.all([cache.track(fakeEvent1), cache.track(fakeEvent2), cache.track(fakeEvent3)]);
expect(await cache.count()).toBe(3);
await cache.drop();
expect(await cache.count()).toBe(0); // storage should be empty after droping it
expect(await cache.count()).toBe(0); // storage should be empty after dropping it

// Clean up then end.
await connection.del(eventsKey, nonListKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('IMPRESSIONS CACHE IN REDIS', () => {
await c.track([o1, o2, o3]);
expect(await c.count()).toBe(3);
await c.drop();
expect(await c.count()).toBe(0); // storage should be empty after droping it
expect(await c.count()).toBe(0); // storage should be empty after dropping it

await connection.del(impressionsKey);
await connection.quit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('PLUGGABLE EVENTS CACHE', () => {
await Promise.all([cache.track(fakeEvent1), cache.track(fakeEvent2), cache.track(fakeEvent3)]);
expect(await cache.count()).toBe(3);
await cache.drop();
expect(await cache.count()).toBe(0); // storage should be empty after droping it
expect(await cache.count()).toBe(0); // storage should be empty after dropping it

wrapperMock.mockClear();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => {
await cache.track([o1, o2, o3]);
expect(await cache.count()).toBe(3);
await cache.drop();
expect(await cache.count()).toBe(0); // storage should be empty after droping it
expect(await cache.count()).toBe(0); // storage should be empty after dropping it

});

Expand Down
7 changes: 3 additions & 4 deletions src/storages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ export interface IRecorderCacheProducerSync<T> {
isEmpty(): boolean
/* Clears cache data */
clear(): void
/* Gets cache data */
state(): T
/* Pops cache data */
pop(toMerge?: T): T
}


Expand Down Expand Up @@ -352,8 +352,7 @@ export interface IImpressionCountsCacheSync extends IRecorderCacheProducerSync<R

// Used by impressions count submitter in standalone and producer mode
isEmpty(): boolean // check if cache is empty. Return true if the cache was just created or cleared.
clear(): void // clear cache data
state(): Record<string, number> // get cache data
pop(toMerge?: Record<string, number> ): Record<string, number> // pop cache data
}


Expand Down
Loading