@bananalytics/react-native 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/context/app.d.ts +12 -0
- package/dist/context/app.js +34 -0
- package/dist/context/device.d.ts +12 -0
- package/dist/context/device.js +38 -0
- package/dist/context/session.d.ts +49 -0
- package/dist/context/session.js +99 -0
- package/dist/core/client.d.ts +112 -0
- package/dist/core/client.js +283 -0
- package/dist/core/config.d.ts +27 -0
- package/dist/core/config.js +41 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.js +40 -0
- package/dist/hooks/BananalyticsProvider.d.ts +22 -0
- package/dist/hooks/BananalyticsProvider.js +72 -0
- package/dist/hooks/RochadeProvider.d.ts +22 -0
- package/dist/hooks/RochadeProvider.js +72 -0
- package/dist/hooks/useBananalytics.d.ts +16 -0
- package/dist/hooks/useBananalytics.js +26 -0
- package/dist/hooks/useRochade.d.ts +16 -0
- package/dist/hooks/useRochade.js +26 -0
- package/dist/hooks/useTrackScreen.d.ts +14 -0
- package/dist/hooks/useTrackScreen.js +24 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +97 -0
- package/dist/privacy/consent.d.ts +39 -0
- package/dist/privacy/consent.js +57 -0
- package/dist/privacy/sanitizer.d.ts +16 -0
- package/dist/privacy/sanitizer.js +40 -0
- package/dist/tracking/event-builder.d.ts +47 -0
- package/dist/tracking/event-builder.js +73 -0
- package/dist/tracking/lifecycle-tracker.d.ts +24 -0
- package/dist/tracking/lifecycle-tracker.js +51 -0
- package/dist/tracking/screen-tracker.d.ts +20 -0
- package/dist/tracking/screen-tracker.js +34 -0
- package/dist/tracking/user-identity.d.ts +40 -0
- package/dist/tracking/user-identity.js +75 -0
- package/dist/transport/batcher.d.ts +44 -0
- package/dist/transport/batcher.js +91 -0
- package/dist/transport/persister.d.ts +49 -0
- package/dist/transport/persister.js +148 -0
- package/dist/transport/queue.d.ts +41 -0
- package/dist/transport/queue.js +67 -0
- package/dist/transport/retry.d.ts +15 -0
- package/dist/transport/retry.js +52 -0
- package/dist/transport/transport.d.ts +23 -0
- package/dist/transport/transport.js +51 -0
- package/dist/types/common.d.ts +6 -0
- package/dist/types/common.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/events.d.ts +53 -0
- package/dist/types/events.js +2 -0
- package/dist/utils/id.d.ts +13 -0
- package/dist/utils/id.js +18 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/network.d.ts +7 -0
- package/dist/utils/network.js +33 -0
- package/dist/utils/time.d.ts +13 -0
- package/dist/utils/time.js +17 -0
- package/dist/utils/validation.d.ts +26 -0
- package/dist/utils/validation.js +60 -0
- package/package.json +64 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UserIdentity = void 0;
|
|
4
|
+
const id_1 = require("../utils/id");
|
|
5
|
+
/**
|
|
6
|
+
* Manages user identity — anonymous ID, user ID, and identity lifecycle.
|
|
7
|
+
*/
|
|
8
|
+
class UserIdentity {
|
|
9
|
+
constructor(persister, logger) {
|
|
10
|
+
this.userId = null;
|
|
11
|
+
this.persister = persister;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
this.anonymousId = (0, id_1.generateId)();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Initializes identity, loading persisted anonymous and user IDs.
|
|
17
|
+
*/
|
|
18
|
+
async initialize() {
|
|
19
|
+
const storedAnonId = await this.persister.loadAnonymousId();
|
|
20
|
+
if (storedAnonId) {
|
|
21
|
+
this.anonymousId = storedAnonId;
|
|
22
|
+
this.logger.debug('Restored anonymous ID', this.anonymousId);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await this.persister.saveAnonymousId(this.anonymousId);
|
|
26
|
+
this.logger.debug('Generated new anonymous ID', this.anonymousId);
|
|
27
|
+
}
|
|
28
|
+
const storedUserId = await this.persister.loadUserId();
|
|
29
|
+
if (storedUserId) {
|
|
30
|
+
this.userId = storedUserId;
|
|
31
|
+
this.logger.debug('Restored user ID', this.userId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Sets the user ID for all subsequent events.
|
|
36
|
+
*
|
|
37
|
+
* @param userId - The user identifier
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* identity.identify('user-123');
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
async identify(userId) {
|
|
45
|
+
this.userId = userId;
|
|
46
|
+
await this.persister.saveUserId(userId);
|
|
47
|
+
this.logger.debug('User identified', userId);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Clears the user ID and generates a new anonymous ID.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* identity.reset();
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
async reset() {
|
|
58
|
+
this.userId = null;
|
|
59
|
+
this.anonymousId = (0, id_1.generateId)();
|
|
60
|
+
await Promise.all([
|
|
61
|
+
this.persister.saveUserId(null),
|
|
62
|
+
this.persister.saveAnonymousId(this.anonymousId),
|
|
63
|
+
]);
|
|
64
|
+
this.logger.debug('Identity reset, new anonymous ID', this.anonymousId);
|
|
65
|
+
}
|
|
66
|
+
/** Returns the current anonymous ID. */
|
|
67
|
+
getAnonymousId() {
|
|
68
|
+
return this.anonymousId;
|
|
69
|
+
}
|
|
70
|
+
/** Returns the current user ID, or null if not identified. */
|
|
71
|
+
getUserId() {
|
|
72
|
+
return this.userId;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.UserIdentity = UserIdentity;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { EventPayload } from '../types/events';
|
|
2
|
+
import { Logger } from '../utils/logger';
|
|
3
|
+
import { EventQueue } from './queue';
|
|
4
|
+
import { Transport } from './transport';
|
|
5
|
+
/**
|
|
6
|
+
* Manages automatic batching and flushing of events.
|
|
7
|
+
* Flushes on a timer interval or when the queue reaches the threshold.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Batcher {
|
|
10
|
+
private readonly queue;
|
|
11
|
+
private readonly transport;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
private readonly flushInterval;
|
|
14
|
+
private readonly flushAt;
|
|
15
|
+
private readonly maxRetries;
|
|
16
|
+
private timer;
|
|
17
|
+
private flushing;
|
|
18
|
+
constructor(queue: EventQueue, transport: Transport, logger: Logger, flushInterval: number, flushAt: number, maxRetries: number);
|
|
19
|
+
/**
|
|
20
|
+
* Starts the automatic flush timer.
|
|
21
|
+
*/
|
|
22
|
+
start(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Stops the automatic flush timer.
|
|
25
|
+
*/
|
|
26
|
+
stop(): void;
|
|
27
|
+
/**
|
|
28
|
+
* Adds an event and triggers a flush if the threshold is reached.
|
|
29
|
+
*
|
|
30
|
+
* @param event - Event payload to enqueue
|
|
31
|
+
*/
|
|
32
|
+
enqueue(event: EventPayload): void;
|
|
33
|
+
/**
|
|
34
|
+
* Flushes all queued events to the backend.
|
|
35
|
+
*
|
|
36
|
+
* @returns Promise that resolves when the flush completes
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* await batcher.flush();
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
flush(): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Batcher = void 0;
|
|
4
|
+
const network_1 = require("../utils/network");
|
|
5
|
+
const retry_1 = require("./retry");
|
|
6
|
+
/**
|
|
7
|
+
* Manages automatic batching and flushing of events.
|
|
8
|
+
* Flushes on a timer interval or when the queue reaches the threshold.
|
|
9
|
+
*/
|
|
10
|
+
class Batcher {
|
|
11
|
+
constructor(queue, transport, logger, flushInterval, flushAt, maxRetries) {
|
|
12
|
+
this.timer = null;
|
|
13
|
+
this.flushing = false;
|
|
14
|
+
this.queue = queue;
|
|
15
|
+
this.transport = transport;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
this.flushInterval = flushInterval;
|
|
18
|
+
this.flushAt = flushAt;
|
|
19
|
+
this.maxRetries = maxRetries;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Starts the automatic flush timer.
|
|
23
|
+
*/
|
|
24
|
+
start() {
|
|
25
|
+
if (this.timer)
|
|
26
|
+
return;
|
|
27
|
+
this.timer = setInterval(() => {
|
|
28
|
+
this.flush().catch((err) => {
|
|
29
|
+
this.logger.error('Auto-flush failed', err);
|
|
30
|
+
});
|
|
31
|
+
}, this.flushInterval);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Stops the automatic flush timer.
|
|
35
|
+
*/
|
|
36
|
+
stop() {
|
|
37
|
+
if (this.timer) {
|
|
38
|
+
clearInterval(this.timer);
|
|
39
|
+
this.timer = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Adds an event and triggers a flush if the threshold is reached.
|
|
44
|
+
*
|
|
45
|
+
* @param event - Event payload to enqueue
|
|
46
|
+
*/
|
|
47
|
+
enqueue(event) {
|
|
48
|
+
this.queue.push(event);
|
|
49
|
+
if (this.queue.length >= this.flushAt) {
|
|
50
|
+
this.flush().catch((err) => {
|
|
51
|
+
this.logger.error('Threshold flush failed', err);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Flushes all queued events to the backend.
|
|
57
|
+
*
|
|
58
|
+
* @returns Promise that resolves when the flush completes
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* await batcher.flush();
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
async flush() {
|
|
66
|
+
if (this.flushing)
|
|
67
|
+
return;
|
|
68
|
+
if (this.queue.length === 0)
|
|
69
|
+
return;
|
|
70
|
+
// Skip flush if offline — events stay in queue for next attempt
|
|
71
|
+
const online = await (0, network_1.isOnline)();
|
|
72
|
+
if (!online) {
|
|
73
|
+
this.logger.debug('Skipping flush — device is offline');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.flushing = true;
|
|
77
|
+
const events = this.queue.flush();
|
|
78
|
+
try {
|
|
79
|
+
await (0, retry_1.withRetry)(() => this.transport.send(events), this.maxRetries, this.logger);
|
|
80
|
+
this.logger.debug(`Flushed ${events.length} events`);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
this.logger.error('Flush failed after retries, re-queueing events', err);
|
|
84
|
+
this.queue.unshift(events);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
this.flushing = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.Batcher = Batcher;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EventPayload } from '../types/events';
|
|
2
|
+
import { Logger } from '../utils/logger';
|
|
3
|
+
/** AsyncStorage interface — matches @react-native-async-storage/async-storage. */
|
|
4
|
+
export interface AsyncStorageInterface {
|
|
5
|
+
getItem(key: string): Promise<string | null>;
|
|
6
|
+
setItem(key: string, value: string): Promise<void>;
|
|
7
|
+
removeItem(key: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Handles persistence of SDK state to AsyncStorage.
|
|
11
|
+
* Persists the event queue, anonymous ID, user ID, opt-out state, and session.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Persister {
|
|
14
|
+
private readonly storage;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
constructor(storage: AsyncStorageInterface, logger: Logger);
|
|
17
|
+
/** Maximum bytes to persist in AsyncStorage. Older events are dropped if exceeded. */
|
|
18
|
+
private static readonly MAX_PERSIST_BYTES;
|
|
19
|
+
/** Saves the event queue to storage, trimming oldest events if it exceeds the size limit. */
|
|
20
|
+
saveQueue(events: ReadonlyArray<EventPayload>): Promise<void>;
|
|
21
|
+
/** Loads the event queue from storage. */
|
|
22
|
+
loadQueue(): Promise<EventPayload[]>;
|
|
23
|
+
/** Clears the persisted queue. */
|
|
24
|
+
clearQueue(): Promise<void>;
|
|
25
|
+
/** Saves the anonymous ID. */
|
|
26
|
+
saveAnonymousId(id: string): Promise<void>;
|
|
27
|
+
/** Loads the anonymous ID. */
|
|
28
|
+
loadAnonymousId(): Promise<string | null>;
|
|
29
|
+
/** Saves the user ID. */
|
|
30
|
+
saveUserId(id: string | null): Promise<void>;
|
|
31
|
+
/** Loads the user ID. */
|
|
32
|
+
loadUserId(): Promise<string | null>;
|
|
33
|
+
/** Saves the opt-out state. */
|
|
34
|
+
saveOptOut(optedOut: boolean): Promise<void>;
|
|
35
|
+
/** Loads the opt-out state. */
|
|
36
|
+
loadOptOut(): Promise<boolean>;
|
|
37
|
+
/** Saves session state. */
|
|
38
|
+
saveSession(session: {
|
|
39
|
+
id: string;
|
|
40
|
+
startedAt: string;
|
|
41
|
+
lastActivity: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
/** Loads session state. */
|
|
44
|
+
loadSession(): Promise<{
|
|
45
|
+
id: string;
|
|
46
|
+
startedAt: string;
|
|
47
|
+
lastActivity: string;
|
|
48
|
+
} | null>;
|
|
49
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Persister = void 0;
|
|
4
|
+
const QUEUE_STORAGE_KEY = '@bananalytics/queue';
|
|
5
|
+
const ANONYMOUS_ID_KEY = '@bananalytics/anonymous_id';
|
|
6
|
+
const USER_ID_KEY = '@bananalytics/user_id';
|
|
7
|
+
const OPT_OUT_KEY = '@bananalytics/opt_out';
|
|
8
|
+
const SESSION_KEY = '@bananalytics/session';
|
|
9
|
+
/**
|
|
10
|
+
* Handles persistence of SDK state to AsyncStorage.
|
|
11
|
+
* Persists the event queue, anonymous ID, user ID, opt-out state, and session.
|
|
12
|
+
*/
|
|
13
|
+
class Persister {
|
|
14
|
+
constructor(storage, logger) {
|
|
15
|
+
this.storage = storage;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
}
|
|
18
|
+
/** Saves the event queue to storage, trimming oldest events if it exceeds the size limit. */
|
|
19
|
+
async saveQueue(events) {
|
|
20
|
+
try {
|
|
21
|
+
let toSave = [...events];
|
|
22
|
+
let serialized = JSON.stringify(toSave);
|
|
23
|
+
// Drop oldest events until we're under the size limit
|
|
24
|
+
while (serialized.length > Persister.MAX_PERSIST_BYTES && toSave.length > 0) {
|
|
25
|
+
const dropped = toSave.length;
|
|
26
|
+
toSave = toSave.slice(Math.ceil(toSave.length / 2));
|
|
27
|
+
this.logger.warn(`Persisted queue too large (${serialized.length} bytes), dropped ${dropped - toSave.length} oldest events`);
|
|
28
|
+
serialized = JSON.stringify(toSave);
|
|
29
|
+
}
|
|
30
|
+
await this.storage.setItem(QUEUE_STORAGE_KEY, serialized);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
this.logger.error('Failed to persist queue', err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Loads the event queue from storage. */
|
|
37
|
+
async loadQueue() {
|
|
38
|
+
try {
|
|
39
|
+
const data = await this.storage.getItem(QUEUE_STORAGE_KEY);
|
|
40
|
+
if (data) {
|
|
41
|
+
return JSON.parse(data);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
this.logger.error('Failed to load persisted queue', err);
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
/** Clears the persisted queue. */
|
|
50
|
+
async clearQueue() {
|
|
51
|
+
try {
|
|
52
|
+
await this.storage.removeItem(QUEUE_STORAGE_KEY);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
this.logger.error('Failed to clear persisted queue', err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Saves the anonymous ID. */
|
|
59
|
+
async saveAnonymousId(id) {
|
|
60
|
+
try {
|
|
61
|
+
await this.storage.setItem(ANONYMOUS_ID_KEY, id);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
this.logger.error('Failed to persist anonymous ID', err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Loads the anonymous ID. */
|
|
68
|
+
async loadAnonymousId() {
|
|
69
|
+
try {
|
|
70
|
+
return await this.storage.getItem(ANONYMOUS_ID_KEY);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.logger.error('Failed to load anonymous ID', err);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Saves the user ID. */
|
|
78
|
+
async saveUserId(id) {
|
|
79
|
+
try {
|
|
80
|
+
if (id === null) {
|
|
81
|
+
await this.storage.removeItem(USER_ID_KEY);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await this.storage.setItem(USER_ID_KEY, id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
this.logger.error('Failed to persist user ID', err);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Loads the user ID. */
|
|
92
|
+
async loadUserId() {
|
|
93
|
+
try {
|
|
94
|
+
return await this.storage.getItem(USER_ID_KEY);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
this.logger.error('Failed to load user ID', err);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Saves the opt-out state. */
|
|
102
|
+
async saveOptOut(optedOut) {
|
|
103
|
+
try {
|
|
104
|
+
await this.storage.setItem(OPT_OUT_KEY, JSON.stringify(optedOut));
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
this.logger.error('Failed to persist opt-out state', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Loads the opt-out state. */
|
|
111
|
+
async loadOptOut() {
|
|
112
|
+
try {
|
|
113
|
+
const data = await this.storage.getItem(OPT_OUT_KEY);
|
|
114
|
+
if (data !== null) {
|
|
115
|
+
return JSON.parse(data);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
this.logger.error('Failed to load opt-out state', err);
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/** Saves session state. */
|
|
124
|
+
async saveSession(session) {
|
|
125
|
+
try {
|
|
126
|
+
await this.storage.setItem(SESSION_KEY, JSON.stringify(session));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
this.logger.error('Failed to persist session', err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Loads session state. */
|
|
133
|
+
async loadSession() {
|
|
134
|
+
try {
|
|
135
|
+
const data = await this.storage.getItem(SESSION_KEY);
|
|
136
|
+
if (data) {
|
|
137
|
+
return JSON.parse(data);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.logger.error('Failed to load session', err);
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.Persister = Persister;
|
|
147
|
+
/** Maximum bytes to persist in AsyncStorage. Older events are dropped if exceeded. */
|
|
148
|
+
Persister.MAX_PERSIST_BYTES = 512 * 1024; // 512KB
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EventPayload } from '../types/events';
|
|
2
|
+
import { Logger } from '../utils/logger';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory event queue with a configurable max size.
|
|
5
|
+
* Events exceeding the max size are dropped (oldest first).
|
|
6
|
+
*/
|
|
7
|
+
export declare class EventQueue {
|
|
8
|
+
private items;
|
|
9
|
+
private readonly maxSize;
|
|
10
|
+
private readonly logger;
|
|
11
|
+
constructor(maxSize: number, logger: Logger);
|
|
12
|
+
/**
|
|
13
|
+
* Adds an event to the queue.
|
|
14
|
+
*
|
|
15
|
+
* @param event - The event payload to enqueue
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* queue.push(eventPayload);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
push(event: EventPayload): void;
|
|
23
|
+
/**
|
|
24
|
+
* Adds events to the front of the queue (used for retry).
|
|
25
|
+
*
|
|
26
|
+
* @param events - Events to prepend
|
|
27
|
+
*/
|
|
28
|
+
unshift(events: EventPayload[]): void;
|
|
29
|
+
/**
|
|
30
|
+
* Takes all events from the queue, clearing it.
|
|
31
|
+
*
|
|
32
|
+
* @returns All queued events
|
|
33
|
+
*/
|
|
34
|
+
flush(): EventPayload[];
|
|
35
|
+
/** Returns the current number of events in the queue. */
|
|
36
|
+
get length(): number;
|
|
37
|
+
/** Returns all events without removing them. */
|
|
38
|
+
peek(): ReadonlyArray<EventPayload>;
|
|
39
|
+
/** Clears all events from the queue. */
|
|
40
|
+
clear(): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EventQueue = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* In-memory event queue with a configurable max size.
|
|
6
|
+
* Events exceeding the max size are dropped (oldest first).
|
|
7
|
+
*/
|
|
8
|
+
class EventQueue {
|
|
9
|
+
constructor(maxSize, logger) {
|
|
10
|
+
this.items = [];
|
|
11
|
+
this.maxSize = maxSize;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Adds an event to the queue.
|
|
16
|
+
*
|
|
17
|
+
* @param event - The event payload to enqueue
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* queue.push(eventPayload);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
push(event) {
|
|
25
|
+
if (this.items.length >= this.maxSize) {
|
|
26
|
+
this.items.shift();
|
|
27
|
+
this.logger.warn(`Queue full (max ${this.maxSize}), dropping oldest event`);
|
|
28
|
+
}
|
|
29
|
+
this.items.push(event);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Adds events to the front of the queue (used for retry).
|
|
33
|
+
*
|
|
34
|
+
* @param events - Events to prepend
|
|
35
|
+
*/
|
|
36
|
+
unshift(events) {
|
|
37
|
+
const available = this.maxSize - this.items.length;
|
|
38
|
+
const toAdd = events.slice(0, available);
|
|
39
|
+
this.items.unshift(...toAdd);
|
|
40
|
+
if (events.length > available) {
|
|
41
|
+
this.logger.warn(`Dropped ${events.length - available} events during retry (queue full)`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Takes all events from the queue, clearing it.
|
|
46
|
+
*
|
|
47
|
+
* @returns All queued events
|
|
48
|
+
*/
|
|
49
|
+
flush() {
|
|
50
|
+
const events = this.items;
|
|
51
|
+
this.items = [];
|
|
52
|
+
return events;
|
|
53
|
+
}
|
|
54
|
+
/** Returns the current number of events in the queue. */
|
|
55
|
+
get length() {
|
|
56
|
+
return this.items.length;
|
|
57
|
+
}
|
|
58
|
+
/** Returns all events without removing them. */
|
|
59
|
+
peek() {
|
|
60
|
+
return this.items;
|
|
61
|
+
}
|
|
62
|
+
/** Clears all events from the queue. */
|
|
63
|
+
clear() {
|
|
64
|
+
this.items = [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.EventQueue = EventQueue;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger';
|
|
2
|
+
/**
|
|
3
|
+
* Executes a function with exponential backoff retry logic.
|
|
4
|
+
*
|
|
5
|
+
* @param fn - The async function to retry
|
|
6
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
7
|
+
* @param logger - Logger instance
|
|
8
|
+
* @returns The result of the function if successful
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* await withRetry(() => transport.send(events), 3, logger);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function withRetry(fn: () => Promise<void>, maxRetries: number, logger: Logger): Promise<void>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withRetry = withRetry;
|
|
4
|
+
const errors_1 = require("../core/errors");
|
|
5
|
+
const BASE_DELAY_MS = 1000;
|
|
6
|
+
/**
|
|
7
|
+
* Executes a function with exponential backoff retry logic.
|
|
8
|
+
*
|
|
9
|
+
* @param fn - The async function to retry
|
|
10
|
+
* @param maxRetries - Maximum number of retry attempts
|
|
11
|
+
* @param logger - Logger instance
|
|
12
|
+
* @returns The result of the function if successful
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* await withRetry(() => transport.send(events), 3, logger);
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
async function withRetry(fn, maxRetries, logger) {
|
|
20
|
+
let lastError;
|
|
21
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
await fn();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
28
|
+
// Don't retry client errors (400, 401, 403)
|
|
29
|
+
if (err instanceof errors_1.NetworkError && !err.isRetryable) {
|
|
30
|
+
logger.warn(`Non-retryable error (status ${err.statusCode}), dropping events: ${err.message}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (attempt < maxRetries) {
|
|
34
|
+
const delay = addJitter(BASE_DELAY_MS * Math.pow(2, attempt));
|
|
35
|
+
logger.debug(`Retry attempt ${attempt + 1}/${maxRetries} in ${delay}ms`);
|
|
36
|
+
await sleep(delay);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw lastError;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Adds random jitter (0-50% of base) to prevent thundering herd.
|
|
44
|
+
* When many clients retry at the same time after an outage,
|
|
45
|
+
* jitter spreads them out so the server isn't overwhelmed.
|
|
46
|
+
*/
|
|
47
|
+
function addJitter(baseMs) {
|
|
48
|
+
return Math.floor(baseMs + Math.random() * baseMs * 0.5);
|
|
49
|
+
}
|
|
50
|
+
function sleep(ms) {
|
|
51
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { EventPayload } from '../types/events';
|
|
2
|
+
import { Logger } from '../utils/logger';
|
|
3
|
+
/**
|
|
4
|
+
* HTTP transport that sends event batches to the ingestion endpoint.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Transport {
|
|
7
|
+
private readonly endpoint;
|
|
8
|
+
private readonly apiKey;
|
|
9
|
+
private readonly logger;
|
|
10
|
+
constructor(endpoint: string, apiKey: string, logger: Logger);
|
|
11
|
+
/**
|
|
12
|
+
* Sends a batch of events to the ingestion endpoint.
|
|
13
|
+
*
|
|
14
|
+
* @param events - Array of event payloads to send
|
|
15
|
+
* @throws NetworkError on failure
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* await transport.send(eventBatch);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
send(events: EventPayload[]): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Transport = void 0;
|
|
4
|
+
const errors_1 = require("../core/errors");
|
|
5
|
+
/**
|
|
6
|
+
* HTTP transport that sends event batches to the ingestion endpoint.
|
|
7
|
+
*/
|
|
8
|
+
class Transport {
|
|
9
|
+
constructor(endpoint, apiKey, logger) {
|
|
10
|
+
this.endpoint = endpoint;
|
|
11
|
+
this.apiKey = apiKey;
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sends a batch of events to the ingestion endpoint.
|
|
16
|
+
*
|
|
17
|
+
* @param events - Array of event payloads to send
|
|
18
|
+
* @throws NetworkError on failure
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* await transport.send(eventBatch);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
async send(events) {
|
|
26
|
+
const url = `${this.endpoint}/v1/ingest`;
|
|
27
|
+
const body = JSON.stringify({ batch: events });
|
|
28
|
+
this.logger.debug(`Sending ${events.length} events to ${url}`);
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch(url, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
35
|
+
},
|
|
36
|
+
body,
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new errors_1.NetworkError(`Ingestion failed with status ${response.status}`, response.status);
|
|
40
|
+
}
|
|
41
|
+
this.logger.debug(`Successfully sent ${events.length} events`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err instanceof errors_1.NetworkError) {
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
throw new errors_1.NetworkError(`Network request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.Transport = Transport;
|