@mostly-good-metrics/javascript 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/README.md +319 -0
- package/dist/cjs/client.js +416 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/index.js +65 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/logger.js +64 -0
- package/dist/cjs/logger.js.map +1 -0
- package/dist/cjs/network.js +192 -0
- package/dist/cjs/network.js.map +1 -0
- package/dist/cjs/storage.js +227 -0
- package/dist/cjs/storage.js.map +1 -0
- package/dist/cjs/types.js +70 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils.js +249 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/esm/client.js +412 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/index.js +40 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/logger.js +55 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/network.js +187 -0
- package/dist/esm/network.js.map +1 -0
- package/dist/esm/storage.js +221 -0
- package/dist/esm/storage.js.map +1 -0
- package/dist/esm/types.js +66 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils.js +236 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/types/client.d.ts +126 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/logger.d.ts +37 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/network.d.ts +28 -0
- package/dist/types/network.d.ts.map +1 -0
- package/dist/types/storage.d.ts +76 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/types.d.ts +279 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils.d.ts +48 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +68 -0
- package/src/client.test.ts +346 -0
- package/src/client.ts +510 -0
- package/src/index.ts +79 -0
- package/src/logger.ts +63 -0
- package/src/network.ts +230 -0
- package/src/storage.test.ts +175 -0
- package/src/storage.ts +249 -0
- package/src/types.ts +347 -0
- package/src/utils.test.ts +239 -0
- package/src/utils.ts +315 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { MostlyGoodMetrics } from './client';
|
|
2
|
+
import { InMemoryEventStorage } from './storage';
|
|
3
|
+
import { INetworkClient, MGMEventsPayload, ResolvedConfiguration, SendResult } from './types';
|
|
4
|
+
|
|
5
|
+
class MockNetworkClient implements INetworkClient {
|
|
6
|
+
public sentPayloads: MGMEventsPayload[] = [];
|
|
7
|
+
public sendResult: SendResult = { success: true };
|
|
8
|
+
private rateLimited = false;
|
|
9
|
+
|
|
10
|
+
async sendEvents(payload: MGMEventsPayload, _config: ResolvedConfiguration): Promise<SendResult> {
|
|
11
|
+
this.sentPayloads.push(payload);
|
|
12
|
+
return this.sendResult;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
isRateLimited(): boolean {
|
|
16
|
+
return this.rateLimited;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getRetryAfterTime(): Date | null {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setRateLimited(limited: boolean): void {
|
|
24
|
+
this.rateLimited = limited;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('MostlyGoodMetrics', () => {
|
|
29
|
+
let storage: InMemoryEventStorage;
|
|
30
|
+
let networkClient: MockNetworkClient;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
// Reset singleton
|
|
34
|
+
MostlyGoodMetrics.reset();
|
|
35
|
+
|
|
36
|
+
storage = new InMemoryEventStorage(100);
|
|
37
|
+
networkClient = new MockNetworkClient();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
MostlyGoodMetrics.reset();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('configure', () => {
|
|
45
|
+
it('should create a singleton instance', () => {
|
|
46
|
+
const instance = MostlyGoodMetrics.configure({
|
|
47
|
+
apiKey: 'test-key',
|
|
48
|
+
storage,
|
|
49
|
+
networkClient,
|
|
50
|
+
trackAppLifecycleEvents: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(instance).toBeDefined();
|
|
54
|
+
expect(MostlyGoodMetrics.shared).toBe(instance);
|
|
55
|
+
expect(MostlyGoodMetrics.isConfigured).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw if apiKey is missing', () => {
|
|
59
|
+
expect(() => {
|
|
60
|
+
MostlyGoodMetrics.configure({ apiKey: '' });
|
|
61
|
+
}).toThrow('API key is required');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return existing instance on multiple configure calls', () => {
|
|
65
|
+
const instance1 = MostlyGoodMetrics.configure({
|
|
66
|
+
apiKey: 'test-key',
|
|
67
|
+
storage,
|
|
68
|
+
networkClient,
|
|
69
|
+
trackAppLifecycleEvents: false,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const instance2 = MostlyGoodMetrics.configure({
|
|
73
|
+
apiKey: 'different-key',
|
|
74
|
+
storage,
|
|
75
|
+
networkClient,
|
|
76
|
+
trackAppLifecycleEvents: false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(instance1).toBe(instance2);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('track', () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
MostlyGoodMetrics.configure({
|
|
86
|
+
apiKey: 'test-key',
|
|
87
|
+
storage,
|
|
88
|
+
networkClient,
|
|
89
|
+
trackAppLifecycleEvents: false,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should store events', async () => {
|
|
94
|
+
MostlyGoodMetrics.track('test_event');
|
|
95
|
+
|
|
96
|
+
// Wait for async storage
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
98
|
+
|
|
99
|
+
const count = await storage.eventCount();
|
|
100
|
+
expect(count).toBe(1);
|
|
101
|
+
|
|
102
|
+
const events = await storage.fetchEvents(1);
|
|
103
|
+
expect(events[0].name).toBe('test_event');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should include properties', async () => {
|
|
107
|
+
MostlyGoodMetrics.track('button_clicked', {
|
|
108
|
+
button_id: 'submit',
|
|
109
|
+
page: '/checkout',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
113
|
+
|
|
114
|
+
const events = await storage.fetchEvents(1);
|
|
115
|
+
expect(events[0].properties?.button_id).toBe('submit');
|
|
116
|
+
expect(events[0].properties?.page).toBe('/checkout');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should include system properties', async () => {
|
|
120
|
+
MostlyGoodMetrics.track('test_event');
|
|
121
|
+
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
123
|
+
|
|
124
|
+
const events = await storage.fetchEvents(1);
|
|
125
|
+
expect(events[0].properties?.$device_type).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should not track events with invalid names', async () => {
|
|
129
|
+
MostlyGoodMetrics.track('invalid-name');
|
|
130
|
+
MostlyGoodMetrics.track('123_starts_with_number');
|
|
131
|
+
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
133
|
+
|
|
134
|
+
const count = await storage.eventCount();
|
|
135
|
+
expect(count).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should include environment and platform', async () => {
|
|
139
|
+
MostlyGoodMetrics.track('test_event');
|
|
140
|
+
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
142
|
+
|
|
143
|
+
const events = await storage.fetchEvents(1);
|
|
144
|
+
expect(events[0].environment).toBe('production');
|
|
145
|
+
expect(events[0].platform).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('identify', () => {
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
MostlyGoodMetrics.configure({
|
|
152
|
+
apiKey: 'test-key',
|
|
153
|
+
storage,
|
|
154
|
+
networkClient,
|
|
155
|
+
trackAppLifecycleEvents: false,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should set userId', () => {
|
|
160
|
+
MostlyGoodMetrics.identify('user_123');
|
|
161
|
+
expect(MostlyGoodMetrics.shared?.userId).toBe('user_123');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should include userId in events', async () => {
|
|
165
|
+
MostlyGoodMetrics.identify('user_456');
|
|
166
|
+
MostlyGoodMetrics.track('test_event');
|
|
167
|
+
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
169
|
+
|
|
170
|
+
const events = await storage.fetchEvents(1);
|
|
171
|
+
expect(events[0].userId).toBe('user_456');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should not set empty userId', () => {
|
|
175
|
+
MostlyGoodMetrics.identify('user_123');
|
|
176
|
+
MostlyGoodMetrics.identify('');
|
|
177
|
+
expect(MostlyGoodMetrics.shared?.userId).toBe('user_123');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('resetIdentity', () => {
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
MostlyGoodMetrics.configure({
|
|
184
|
+
apiKey: 'test-key',
|
|
185
|
+
storage,
|
|
186
|
+
networkClient,
|
|
187
|
+
trackAppLifecycleEvents: false,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should clear userId', () => {
|
|
192
|
+
MostlyGoodMetrics.identify('user_123');
|
|
193
|
+
MostlyGoodMetrics.resetIdentity();
|
|
194
|
+
expect(MostlyGoodMetrics.shared?.userId).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('flush', () => {
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
MostlyGoodMetrics.configure({
|
|
201
|
+
apiKey: 'test-key',
|
|
202
|
+
storage,
|
|
203
|
+
networkClient,
|
|
204
|
+
trackAppLifecycleEvents: false,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should send events to network client', async () => {
|
|
209
|
+
MostlyGoodMetrics.track('event1');
|
|
210
|
+
MostlyGoodMetrics.track('event2');
|
|
211
|
+
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
213
|
+
await MostlyGoodMetrics.flush();
|
|
214
|
+
|
|
215
|
+
expect(networkClient.sentPayloads).toHaveLength(1);
|
|
216
|
+
expect(networkClient.sentPayloads[0].events).toHaveLength(2);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should clear events after successful send', async () => {
|
|
220
|
+
MostlyGoodMetrics.track('test_event');
|
|
221
|
+
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
223
|
+
await MostlyGoodMetrics.flush();
|
|
224
|
+
|
|
225
|
+
const count = await storage.eventCount();
|
|
226
|
+
expect(count).toBe(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should not send when rate limited', async () => {
|
|
230
|
+
networkClient.setRateLimited(true);
|
|
231
|
+
|
|
232
|
+
MostlyGoodMetrics.track('test_event');
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
234
|
+
await MostlyGoodMetrics.flush();
|
|
235
|
+
|
|
236
|
+
expect(networkClient.sentPayloads).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should keep events on retryable error', async () => {
|
|
240
|
+
networkClient.sendResult = {
|
|
241
|
+
success: false,
|
|
242
|
+
error: { name: 'MGMError', message: 'Server error', type: 'SERVER_ERROR' } as never,
|
|
243
|
+
shouldRetry: true,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
MostlyGoodMetrics.track('test_event');
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
248
|
+
await MostlyGoodMetrics.flush();
|
|
249
|
+
|
|
250
|
+
const count = await storage.eventCount();
|
|
251
|
+
expect(count).toBe(1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should drop events on non-retryable error', async () => {
|
|
255
|
+
networkClient.sendResult = {
|
|
256
|
+
success: false,
|
|
257
|
+
error: { name: 'MGMError', message: 'Bad request', type: 'BAD_REQUEST' } as never,
|
|
258
|
+
shouldRetry: false,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
MostlyGoodMetrics.track('test_event');
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
263
|
+
await MostlyGoodMetrics.flush();
|
|
264
|
+
|
|
265
|
+
const count = await storage.eventCount();
|
|
266
|
+
expect(count).toBe(0);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('startNewSession', () => {
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
MostlyGoodMetrics.configure({
|
|
273
|
+
apiKey: 'test-key',
|
|
274
|
+
storage,
|
|
275
|
+
networkClient,
|
|
276
|
+
trackAppLifecycleEvents: false,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should generate a new session ID', () => {
|
|
281
|
+
const originalSessionId = MostlyGoodMetrics.shared?.sessionId;
|
|
282
|
+
MostlyGoodMetrics.startNewSession();
|
|
283
|
+
const newSessionId = MostlyGoodMetrics.shared?.sessionId;
|
|
284
|
+
|
|
285
|
+
expect(newSessionId).toBeDefined();
|
|
286
|
+
expect(newSessionId).not.toBe(originalSessionId);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('clearPendingEvents', () => {
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
MostlyGoodMetrics.configure({
|
|
293
|
+
apiKey: 'test-key',
|
|
294
|
+
storage,
|
|
295
|
+
networkClient,
|
|
296
|
+
trackAppLifecycleEvents: false,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should clear all pending events', async () => {
|
|
301
|
+
MostlyGoodMetrics.track('event1');
|
|
302
|
+
MostlyGoodMetrics.track('event2');
|
|
303
|
+
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
305
|
+
expect(await storage.eventCount()).toBe(2);
|
|
306
|
+
|
|
307
|
+
await MostlyGoodMetrics.clearPendingEvents();
|
|
308
|
+
expect(await storage.eventCount()).toBe(0);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('getPendingEventCount', () => {
|
|
313
|
+
beforeEach(() => {
|
|
314
|
+
MostlyGoodMetrics.configure({
|
|
315
|
+
apiKey: 'test-key',
|
|
316
|
+
storage,
|
|
317
|
+
networkClient,
|
|
318
|
+
trackAppLifecycleEvents: false,
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should return correct count', async () => {
|
|
323
|
+
expect(await MostlyGoodMetrics.getPendingEventCount()).toBe(0);
|
|
324
|
+
|
|
325
|
+
MostlyGoodMetrics.track('event1');
|
|
326
|
+
MostlyGoodMetrics.track('event2');
|
|
327
|
+
MostlyGoodMetrics.track('event3');
|
|
328
|
+
|
|
329
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
330
|
+
expect(await MostlyGoodMetrics.getPendingEventCount()).toBe(3);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('static methods without configuration', () => {
|
|
335
|
+
it('should not throw when SDK is not configured', async () => {
|
|
336
|
+
expect(() => MostlyGoodMetrics.track('test')).not.toThrow();
|
|
337
|
+
expect(() => MostlyGoodMetrics.identify('user')).not.toThrow();
|
|
338
|
+
expect(() => MostlyGoodMetrics.resetIdentity()).not.toThrow();
|
|
339
|
+
expect(() => MostlyGoodMetrics.startNewSession()).not.toThrow();
|
|
340
|
+
|
|
341
|
+
await expect(MostlyGoodMetrics.flush()).resolves.not.toThrow();
|
|
342
|
+
await expect(MostlyGoodMetrics.clearPendingEvents()).resolves.not.toThrow();
|
|
343
|
+
await expect(MostlyGoodMetrics.getPendingEventCount()).resolves.toBe(0);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|