-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
1,105 additions
and
439 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import createFetchMock from "vitest-fetch-mock"; | ||
import { vi } from "vitest"; | ||
|
||
const fetchMocker = createFetchMock(vi); | ||
|
||
fetchMocker.enableMocks(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import "vitest-fetch-mock"; | ||
import { EventDispatcher } from "./dispatcher"; | ||
import { beforeEach, describe, expect, it } from "vitest"; | ||
|
||
const createEvent = (eventName: string) => ({ | ||
timestamp: new Date().toISOString(), | ||
sessionId: "123", | ||
eventName, | ||
systemProps: { | ||
isDebug: false, | ||
locale: "en-US", | ||
osName: "iOS", | ||
osVersion: "14.3", | ||
appVersion: "1.0.0", | ||
sdkVersion: "1.0.0", | ||
}, | ||
}); | ||
|
||
const expectRequestCount = (count: number) => { | ||
expect(fetchMock.requests().length).toEqual(count); | ||
}; | ||
|
||
const expectEventsCount = async ( | ||
requestIndex: number, | ||
expectedNumOfEvents: number | ||
) => { | ||
const body = await fetchMock.requests()[requestIndex].json(); | ||
expect(body.length).toEqual(expectedNumOfEvents); | ||
}; | ||
|
||
describe("EventDispatcher", () => { | ||
let dispatcher: EventDispatcher; | ||
|
||
beforeEach(() => { | ||
dispatcher = new EventDispatcher("https://localhost:3000", "A-DEV-000"); | ||
fetchMock.resetMocks(); | ||
}); | ||
|
||
it("should not send a request if queue is empty", async () => { | ||
await dispatcher.flush(); | ||
|
||
expectRequestCount(0); | ||
}); | ||
|
||
it("should dispatch single event", async () => { | ||
fetchMock.mockResponseOnce("{}"); | ||
|
||
dispatcher.enqueue(createEvent("app_started")); | ||
await dispatcher.flush(); | ||
|
||
expectRequestCount(1); | ||
await expectEventsCount(0, 1); | ||
}); | ||
|
||
it("should not dispatch event if it's already been sent", async () => { | ||
fetchMock.mockResponseOnce("{}"); | ||
|
||
dispatcher.enqueue(createEvent("app_started")); | ||
await dispatcher.flush(); | ||
expectRequestCount(1); | ||
|
||
await dispatcher.flush(); | ||
expectRequestCount(1); | ||
}); | ||
|
||
it("should dispatch multiple events", async () => { | ||
fetchMock.mockResponseOnce("{}"); | ||
|
||
dispatcher.enqueue(createEvent("app_started")); | ||
dispatcher.enqueue(createEvent("app_exited")); | ||
await dispatcher.flush(); | ||
|
||
expectRequestCount(1); | ||
await expectEventsCount(0, 2); | ||
}); | ||
|
||
it("should send many events in chunks of 25 items", async () => { | ||
fetchMock.mockResponseOnce("{}"); | ||
|
||
for (let i = 0; i < 60; i++) { | ||
dispatcher.enqueue(createEvent("hello_world")); | ||
} | ||
await dispatcher.flush(); | ||
|
||
expectRequestCount(3); | ||
await expectEventsCount(0, 25); | ||
await expectEventsCount(1, 25); | ||
await expectEventsCount(2, 10); | ||
}); | ||
|
||
it("should retry failed requests in a subsequent flush", async () => { | ||
fetchMock.mockResponseOnce("{}", { status: 500 }); | ||
|
||
dispatcher.enqueue(createEvent("hello_world")); | ||
await dispatcher.flush(); | ||
|
||
expectRequestCount(1); | ||
await expectEventsCount(0, 1); | ||
|
||
fetchMock.mockResponseOnce("{}", { status: 200 }); | ||
await dispatcher.flush(); | ||
|
||
expectRequestCount(2); | ||
await expectEventsCount(1, 1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
export type Event = { | ||
timestamp: string; | ||
sessionId: string; | ||
eventName: string; | ||
systemProps: { | ||
isDebug: boolean; | ||
locale: string; | ||
osName: string; | ||
osVersion: string; | ||
appVersion: string; | ||
sdkVersion: string; | ||
}; | ||
props?: Record<string, string | number | boolean>; | ||
}; | ||
|
||
export class EventDispatcher { | ||
private _events: Event[] = []; | ||
private MAX_BATCH_SIZE = 25; | ||
|
||
constructor(private apiUrl: string, private appKey: string) {} | ||
|
||
public enqueue(evt: Event | Event[]) { | ||
if (Array.isArray(evt)) { | ||
this._events.push(...evt); | ||
return; | ||
} | ||
|
||
this._events.push(evt); | ||
} | ||
|
||
public async flush(): Promise<void> { | ||
if (this._events.length === 0) { | ||
return Promise.resolve(); | ||
} | ||
|
||
let failedEvents: Event[] = []; | ||
do { | ||
const eventsToSend = this._events.splice(0, this.MAX_BATCH_SIZE); | ||
try { | ||
await this._sendEvents(eventsToSend); | ||
} catch { | ||
failedEvents = [...failedEvents, ...eventsToSend]; | ||
} | ||
} while (this._events.length > 0); | ||
|
||
if (failedEvents.length > 0) { | ||
this.enqueue(failedEvents); | ||
} | ||
} | ||
|
||
private async _sendEvents(events: Event[]): Promise<void> { | ||
const res = await fetch(this.apiUrl, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
"App-Key": this.appKey, | ||
}, | ||
credentials: "omit", | ||
body: JSON.stringify(events), | ||
}); | ||
|
||
if (res.status >= 300) { | ||
const reason = await res.text(); | ||
console.warn( | ||
`Aptabase: Failed to send ${events.length} events. Reason: ${res.status} ${reason}` | ||
); | ||
} | ||
|
||
if (res.status >= 500) { | ||
throw new Error("Failed to send events"); | ||
} | ||
|
||
return Promise.resolve(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters