autotel-subscribers 4.0.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 +669 -0
- package/dist/amplitude.cjs +2486 -0
- package/dist/amplitude.cjs.map +1 -0
- package/dist/amplitude.d.cts +49 -0
- package/dist/amplitude.d.ts +49 -0
- package/dist/amplitude.js +2463 -0
- package/dist/amplitude.js.map +1 -0
- package/dist/event-subscriber-base-CnF3V56W.d.cts +182 -0
- package/dist/event-subscriber-base-CnF3V56W.d.ts +182 -0
- package/dist/factories.cjs +16660 -0
- package/dist/factories.cjs.map +1 -0
- package/dist/factories.d.cts +304 -0
- package/dist/factories.d.ts +304 -0
- package/dist/factories.js +16624 -0
- package/dist/factories.js.map +1 -0
- package/dist/index.cjs +16575 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +179 -0
- package/dist/index.d.ts +179 -0
- package/dist/index.js +16539 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.cjs +220 -0
- package/dist/middleware.cjs.map +1 -0
- package/dist/middleware.d.cts +227 -0
- package/dist/middleware.d.ts +227 -0
- package/dist/middleware.js +208 -0
- package/dist/middleware.js.map +1 -0
- package/dist/mixpanel.cjs +2940 -0
- package/dist/mixpanel.cjs.map +1 -0
- package/dist/mixpanel.d.cts +47 -0
- package/dist/mixpanel.d.ts +47 -0
- package/dist/mixpanel.js +2932 -0
- package/dist/mixpanel.js.map +1 -0
- package/dist/posthog.cjs +4115 -0
- package/dist/posthog.cjs.map +1 -0
- package/dist/posthog.d.cts +299 -0
- package/dist/posthog.d.ts +299 -0
- package/dist/posthog.js +4113 -0
- package/dist/posthog.js.map +1 -0
- package/dist/segment.cjs +6822 -0
- package/dist/segment.cjs.map +1 -0
- package/dist/segment.d.cts +49 -0
- package/dist/segment.d.ts +49 -0
- package/dist/segment.js +6794 -0
- package/dist/segment.js.map +1 -0
- package/dist/slack.cjs +368 -0
- package/dist/slack.cjs.map +1 -0
- package/dist/slack.d.cts +126 -0
- package/dist/slack.d.ts +126 -0
- package/dist/slack.js +366 -0
- package/dist/slack.js.map +1 -0
- package/dist/webhook.cjs +100 -0
- package/dist/webhook.cjs.map +1 -0
- package/dist/webhook.d.cts +53 -0
- package/dist/webhook.d.ts +53 -0
- package/dist/webhook.js +98 -0
- package/dist/webhook.js.map +1 -0
- package/examples/quickstart-custom-subscriber.ts +144 -0
- package/examples/subscriber-bigquery.ts +219 -0
- package/examples/subscriber-databricks.ts +280 -0
- package/examples/subscriber-kafka.ts +326 -0
- package/examples/subscriber-kinesis.ts +307 -0
- package/examples/subscriber-posthog.ts +421 -0
- package/examples/subscriber-pubsub.ts +336 -0
- package/examples/subscriber-snowflake.ts +232 -0
- package/package.json +141 -0
- package/src/amplitude.test.ts +231 -0
- package/src/amplitude.ts +148 -0
- package/src/event-subscriber-base.ts +325 -0
- package/src/factories.ts +197 -0
- package/src/index.ts +50 -0
- package/src/middleware.ts +489 -0
- package/src/mixpanel.test.ts +194 -0
- package/src/mixpanel.ts +134 -0
- package/src/mock-event-subscriber.ts +333 -0
- package/src/posthog.test.ts +629 -0
- package/src/posthog.ts +530 -0
- package/src/segment.test.ts +228 -0
- package/src/segment.ts +148 -0
- package/src/slack.ts +383 -0
- package/src/streaming-event-subscriber.ts +323 -0
- package/src/testing/index.ts +37 -0
- package/src/testing/mock-webhook-server.ts +242 -0
- package/src/testing/subscriber-test-harness.ts +365 -0
- package/src/webhook.test.ts +264 -0
- package/src/webhook.ts +158 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing utilities for subscriber authors.
|
|
3
|
+
*
|
|
4
|
+
* Use these to validate your custom subscribers work correctly.
|
|
5
|
+
*
|
|
6
|
+
* @example Test your subscriber
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { SubscriberTestHarness } from 'autotel-subscribers/testing';
|
|
9
|
+
*
|
|
10
|
+
* const harness = new SubscriberTestHarness(new MySubscriber());
|
|
11
|
+
* const results = await harness.runAll();
|
|
12
|
+
* SubscriberTestHarness.printResults(results);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @example Test webhook subscriber
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { MockWebhookServer } from 'autotel-subscribers/testing';
|
|
18
|
+
*
|
|
19
|
+
* const server = new MockWebhookServer();
|
|
20
|
+
* const url = await server.start();
|
|
21
|
+
* const subscriber = new WebhookSubscriber({ url });
|
|
22
|
+
*
|
|
23
|
+
* await subscriber.trackEvent('test', {});
|
|
24
|
+
* expect(server.getRequestCount()).toBe(1);
|
|
25
|
+
*
|
|
26
|
+
* await server.stop();
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export { SubscriberTestHarness } from './subscriber-test-harness';
|
|
31
|
+
export type { TestResult, TestSuiteResult } from './subscriber-test-harness';
|
|
32
|
+
|
|
33
|
+
export { MockWebhookServer } from './mock-webhook-server';
|
|
34
|
+
export type { RecordedRequest, MockServerOptions } from './mock-webhook-server';
|
|
35
|
+
|
|
36
|
+
// Re-export MockEventSubscriber for convenience
|
|
37
|
+
export { MockEventSubscriber } from '../mock-event-subscriber';
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockWebhookServer - In-Memory HTTP Server for Testing
|
|
3
|
+
*
|
|
4
|
+
* Perfect for testing webhook subscribers without real HTTP calls.
|
|
5
|
+
* Records all requests for easy assertions in tests.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { MockWebhookServer } from 'autotel-subscribers/testing';
|
|
10
|
+
*
|
|
11
|
+
* const server = new MockWebhookServer();
|
|
12
|
+
* const url = await server.start();
|
|
13
|
+
*
|
|
14
|
+
* // Test your webhook subscriber
|
|
15
|
+
* const subscriber = new WebhookSubscriber({ url });
|
|
16
|
+
* await subscriber.trackEvent('test.event', { foo: 'bar' });
|
|
17
|
+
*
|
|
18
|
+
* // Assert
|
|
19
|
+
* const requests = server.getRequests();
|
|
20
|
+
* expect(requests).toHaveLength(1);
|
|
21
|
+
* expect(requests[0].body.event).toBe('test.event');
|
|
22
|
+
*
|
|
23
|
+
* await server.stop();
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import * as http from 'node:http';
|
|
28
|
+
import type { AddressInfo } from 'node:net';
|
|
29
|
+
|
|
30
|
+
export interface RecordedRequest {
|
|
31
|
+
method: string;
|
|
32
|
+
path: string;
|
|
33
|
+
headers: http.IncomingHttpHeaders;
|
|
34
|
+
body: any;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MockServerOptions {
|
|
39
|
+
/** Port to listen on (0 = random port) */
|
|
40
|
+
port?: number;
|
|
41
|
+
/** Response status code (default: 200) */
|
|
42
|
+
responseStatus?: number;
|
|
43
|
+
/** Response delay in ms (default: 0) */
|
|
44
|
+
responseDelay?: number;
|
|
45
|
+
/** Response body (default: 'OK') */
|
|
46
|
+
responseBody?: string;
|
|
47
|
+
/** Log requests to console (default: false) */
|
|
48
|
+
logRequests?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* In-memory HTTP server for testing webhook subscribers.
|
|
53
|
+
*
|
|
54
|
+
* Records all incoming requests so you can assert on them in tests.
|
|
55
|
+
*/
|
|
56
|
+
export class MockWebhookServer {
|
|
57
|
+
private server?: http.Server;
|
|
58
|
+
private requests: RecordedRequest[] = [];
|
|
59
|
+
private options: Required<MockServerOptions>;
|
|
60
|
+
|
|
61
|
+
constructor(options: MockServerOptions = {}) {
|
|
62
|
+
this.options = {
|
|
63
|
+
port: options.port ?? 0,
|
|
64
|
+
responseStatus: options.responseStatus ?? 200,
|
|
65
|
+
responseDelay: options.responseDelay ?? 0,
|
|
66
|
+
responseBody: options.responseBody ?? 'OK',
|
|
67
|
+
logRequests: options.logRequests ?? false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Start the mock server and return its URL.
|
|
73
|
+
*
|
|
74
|
+
* @returns Promise resolving to the server URL (e.g., "http://localhost:3000")
|
|
75
|
+
*/
|
|
76
|
+
async start(): Promise<string> {
|
|
77
|
+
if (this.server) {
|
|
78
|
+
throw new Error('Server already started');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.server = http.createServer((req, res) => {
|
|
82
|
+
let body = '';
|
|
83
|
+
|
|
84
|
+
req.on('data', (chunk) => {
|
|
85
|
+
body += chunk.toString();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
req.on('end', () => {
|
|
89
|
+
const parsedBody = this.parseBody(body, req.headers['content-type']);
|
|
90
|
+
|
|
91
|
+
const request: RecordedRequest = {
|
|
92
|
+
method: req.method || 'GET',
|
|
93
|
+
path: req.url || '/',
|
|
94
|
+
headers: req.headers,
|
|
95
|
+
body: parsedBody,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
this.requests.push(request);
|
|
100
|
+
|
|
101
|
+
if (this.options.logRequests) {
|
|
102
|
+
console.log('[MockWebhookServer]', request.method, request.path, parsedBody);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Simulate response delay
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
res.writeHead(this.options.responseStatus, {
|
|
108
|
+
'Content-Type': 'text/plain',
|
|
109
|
+
});
|
|
110
|
+
res.end(this.options.responseBody);
|
|
111
|
+
}, this.options.responseDelay);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await new Promise<void>((resolve) => {
|
|
116
|
+
this.server!.listen(this.options.port, () => {
|
|
117
|
+
resolve();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const address = this.server.address() as AddressInfo;
|
|
122
|
+
return `http://localhost:${address.port}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Stop the mock server.
|
|
127
|
+
*/
|
|
128
|
+
async stop(): Promise<void> {
|
|
129
|
+
if (!this.server) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await new Promise<void>((resolve, reject) => {
|
|
134
|
+
this.server!.close((err) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
reject(err);
|
|
137
|
+
} else {
|
|
138
|
+
resolve();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.server = undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all recorded requests.
|
|
148
|
+
*/
|
|
149
|
+
getRequests(): RecordedRequest[] {
|
|
150
|
+
return [...this.requests];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get requests matching a filter.
|
|
155
|
+
*/
|
|
156
|
+
getRequestsWhere(filter: Partial<RecordedRequest>): RecordedRequest[] {
|
|
157
|
+
return this.requests.filter((req) => {
|
|
158
|
+
return Object.entries(filter).every(([key, value]) => {
|
|
159
|
+
return (req as any)[key] === value;
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get the last recorded request.
|
|
166
|
+
*/
|
|
167
|
+
getLastRequest(): RecordedRequest | undefined {
|
|
168
|
+
return this.requests.at(-1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the first recorded request.
|
|
173
|
+
*/
|
|
174
|
+
getFirstRequest(): RecordedRequest | undefined {
|
|
175
|
+
return this.requests[0];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Clear all recorded requests.
|
|
180
|
+
*/
|
|
181
|
+
reset(): void {
|
|
182
|
+
this.requests = [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the number of recorded requests.
|
|
187
|
+
*/
|
|
188
|
+
getRequestCount(): number {
|
|
189
|
+
return this.requests.length;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Wait for a specific number of requests.
|
|
194
|
+
*
|
|
195
|
+
* Useful when testing async subscribers.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* await subscriber.trackEvent('event1', {});
|
|
200
|
+
* await subscriber.trackEvent('event2', {});
|
|
201
|
+
* await server.waitForRequests(2, 1000); // Wait max 1 second
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
async waitForRequests(count: number, timeoutMs: number = 5000): Promise<void> {
|
|
205
|
+
const startTime = Date.now();
|
|
206
|
+
|
|
207
|
+
while (this.requests.length < count) {
|
|
208
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Timeout waiting for ${count} requests (got ${this.requests.length})`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Wait 10ms before checking again
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse request body based on Content-Type.
|
|
221
|
+
*/
|
|
222
|
+
private parseBody(body: string, contentType?: string): any {
|
|
223
|
+
if (!body) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
if (contentType?.includes('application/json')) {
|
|
229
|
+
return JSON.parse(body);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (contentType?.includes('application/x-www-form-urlencoded')) {
|
|
233
|
+
return Object.fromEntries(new URLSearchParams(body));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return body;
|
|
237
|
+
} catch {
|
|
238
|
+
// If parsing fails, return raw string
|
|
239
|
+
return body;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubscriberTestHarness - Validate Your Custom Subscriber
|
|
3
|
+
*
|
|
4
|
+
* Use this to test that your custom subscriber implements the EventSubscriber
|
|
5
|
+
* interface correctly. It runs a suite of tests covering:
|
|
6
|
+
* - Basic tracking (all 4 methods)
|
|
7
|
+
* - Concurrent requests
|
|
8
|
+
* - Error handling
|
|
9
|
+
* - Graceful shutdown
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { SubscriberTestHarness } from 'autotel-subscribers/testing';
|
|
14
|
+
* import { MyCustomSubscriber } from './my-adapter';
|
|
15
|
+
*
|
|
16
|
+
* const harness = new SubscriberTestHarness(new MyCustomSubscriber());
|
|
17
|
+
* const results = await harness.runAll();
|
|
18
|
+
*
|
|
19
|
+
* if (results.passed) {
|
|
20
|
+
* console.log('ā
All tests passed!');
|
|
21
|
+
* } else {
|
|
22
|
+
* console.error('ā Tests failed:', results.failures);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
EventSubscriber,
|
|
29
|
+
FunnelStatus,
|
|
30
|
+
OutcomeStatus,
|
|
31
|
+
} from '../event-subscriber-base';
|
|
32
|
+
|
|
33
|
+
export interface TestResult {
|
|
34
|
+
name: string;
|
|
35
|
+
passed: boolean;
|
|
36
|
+
duration: number;
|
|
37
|
+
error?: Error;
|
|
38
|
+
details?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TestSuiteResult {
|
|
42
|
+
passed: boolean;
|
|
43
|
+
total: number;
|
|
44
|
+
passed_count: number;
|
|
45
|
+
failed_count: number;
|
|
46
|
+
duration: number;
|
|
47
|
+
results: TestResult[];
|
|
48
|
+
failures: TestResult[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Test harness for validating custom subscribers.
|
|
53
|
+
*
|
|
54
|
+
* Runs a comprehensive suite of tests to ensure your subscriber:
|
|
55
|
+
* 1. Implements all required methods
|
|
56
|
+
* 2. Handles concurrent requests
|
|
57
|
+
* 3. Deals with errors gracefully
|
|
58
|
+
* 4. Shuts down cleanly
|
|
59
|
+
*/
|
|
60
|
+
export class SubscriberTestHarness {
|
|
61
|
+
constructor(private subscriber: EventSubscriber) {}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run all tests and return a comprehensive report.
|
|
65
|
+
*/
|
|
66
|
+
async runAll(): Promise<TestSuiteResult> {
|
|
67
|
+
const startTime = performance.now();
|
|
68
|
+
const results: TestResult[] = [];
|
|
69
|
+
|
|
70
|
+
// Run all test methods
|
|
71
|
+
const testResults = await Promise.all([
|
|
72
|
+
this.testBasicTracking(),
|
|
73
|
+
this.testFunnelTracking(),
|
|
74
|
+
this.testOutcomeTracking(),
|
|
75
|
+
this.testValueTracking(),
|
|
76
|
+
this.testConcurrency(),
|
|
77
|
+
this.testErrorHandling(),
|
|
78
|
+
this.testShutdown(),
|
|
79
|
+
]);
|
|
80
|
+
results.push(...testResults);
|
|
81
|
+
|
|
82
|
+
const duration = performance.now() - startTime;
|
|
83
|
+
const passed_count = results.filter((r) => r.passed).length;
|
|
84
|
+
const failed_count = results.filter((r) => !r.passed).length;
|
|
85
|
+
const failures = results.filter((r) => !r.passed);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
passed: failed_count === 0,
|
|
89
|
+
total: results.length,
|
|
90
|
+
passed_count,
|
|
91
|
+
failed_count,
|
|
92
|
+
duration,
|
|
93
|
+
results,
|
|
94
|
+
failures,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Test basic event tracking.
|
|
100
|
+
*/
|
|
101
|
+
async testBasicTracking(): Promise<TestResult> {
|
|
102
|
+
const startTime = performance.now();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await this.subscriber.trackEvent('test.event', {
|
|
106
|
+
userId: 'test-user',
|
|
107
|
+
testAttribute: 'test-value',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
name: 'Basic Event Tracking',
|
|
112
|
+
passed: true,
|
|
113
|
+
duration: performance.now() - startTime,
|
|
114
|
+
details: 'Successfully tracked event',
|
|
115
|
+
};
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
name: 'Basic Event Tracking',
|
|
119
|
+
passed: false,
|
|
120
|
+
duration: performance.now() - startTime,
|
|
121
|
+
error: error as Error,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Test funnel step tracking.
|
|
128
|
+
*/
|
|
129
|
+
async testFunnelTracking(): Promise<TestResult> {
|
|
130
|
+
const startTime = performance.now();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await this.subscriber.trackFunnelStep('test_funnel', 'started' as FunnelStatus, {
|
|
134
|
+
cartValue: 99.99,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await this.subscriber.trackFunnelStep('test_funnel', 'completed' as FunnelStatus, {
|
|
138
|
+
cartValue: 99.99,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
name: 'Funnel Tracking',
|
|
143
|
+
passed: true,
|
|
144
|
+
duration: performance.now() - startTime,
|
|
145
|
+
details: 'Successfully tracked funnel steps',
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
name: 'Funnel Tracking',
|
|
150
|
+
passed: false,
|
|
151
|
+
duration: performance.now() - startTime,
|
|
152
|
+
error: error as Error,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Test outcome tracking.
|
|
159
|
+
*/
|
|
160
|
+
async testOutcomeTracking(): Promise<TestResult> {
|
|
161
|
+
const startTime = performance.now();
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await this.subscriber.trackOutcome('test_operation', 'success' as OutcomeStatus, {
|
|
165
|
+
duration: 100,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await this.subscriber.trackOutcome('test_operation', 'failure' as OutcomeStatus, {
|
|
169
|
+
error: 'Test error',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
name: 'Outcome Tracking',
|
|
174
|
+
passed: true,
|
|
175
|
+
duration: performance.now() - startTime,
|
|
176
|
+
details: 'Successfully tracked outcomes',
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
name: 'Outcome Tracking',
|
|
181
|
+
passed: false,
|
|
182
|
+
duration: performance.now() - startTime,
|
|
183
|
+
error: error as Error,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Test value tracking.
|
|
190
|
+
*/
|
|
191
|
+
async testValueTracking(): Promise<TestResult> {
|
|
192
|
+
const startTime = performance.now();
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await this.subscriber.trackValue('test_metric', 42, {
|
|
196
|
+
unit: 'ms',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await this.subscriber.trackValue('revenue', 99.99, {
|
|
200
|
+
currency: 'USD',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
name: 'Value Tracking',
|
|
205
|
+
passed: true,
|
|
206
|
+
duration: performance.now() - startTime,
|
|
207
|
+
details: 'Successfully tracked values',
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return {
|
|
211
|
+
name: 'Value Tracking',
|
|
212
|
+
passed: false,
|
|
213
|
+
duration: performance.now() - startTime,
|
|
214
|
+
error: error as Error,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Test concurrent requests (sends 50 events simultaneously).
|
|
221
|
+
*/
|
|
222
|
+
async testConcurrency(): Promise<TestResult> {
|
|
223
|
+
const startTime = performance.now();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const promises = Array.from({ length: 50 }, (_, i) =>
|
|
227
|
+
this.subscriber.trackEvent(`concurrent_event_${i}`, {
|
|
228
|
+
index: i,
|
|
229
|
+
timestamp: Date.now(),
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
await Promise.all(promises);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
name: 'Concurrency',
|
|
237
|
+
passed: true,
|
|
238
|
+
duration: performance.now() - startTime,
|
|
239
|
+
details: 'Successfully handled 50 concurrent requests',
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
name: 'Concurrency',
|
|
244
|
+
passed: false,
|
|
245
|
+
duration: performance.now() - startTime,
|
|
246
|
+
error: error as Error,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Test error handling (passes invalid data).
|
|
253
|
+
*/
|
|
254
|
+
async testErrorHandling(): Promise<TestResult> {
|
|
255
|
+
const startTime = performance.now();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Test with empty event name
|
|
259
|
+
await this.subscriber.trackEvent('', {});
|
|
260
|
+
|
|
261
|
+
// Test with undefined attributes
|
|
262
|
+
await this.subscriber.trackEvent('test');
|
|
263
|
+
|
|
264
|
+
// Test with null-ish values
|
|
265
|
+
await this.subscriber.trackValue('test', 0, {});
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
name: 'Error Handling',
|
|
269
|
+
passed: true,
|
|
270
|
+
duration: performance.now() - startTime,
|
|
271
|
+
details: 'Handled edge cases gracefully',
|
|
272
|
+
};
|
|
273
|
+
} catch {
|
|
274
|
+
// Some subscribers might throw on invalid input - that's OK
|
|
275
|
+
return {
|
|
276
|
+
name: 'Error Handling',
|
|
277
|
+
passed: true,
|
|
278
|
+
duration: performance.now() - startTime,
|
|
279
|
+
details: 'Subscriber throws on invalid input (acceptable)',
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Test graceful shutdown.
|
|
286
|
+
*/
|
|
287
|
+
async testShutdown(): Promise<TestResult> {
|
|
288
|
+
const startTime = performance.now();
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Start some long-running operations
|
|
292
|
+
const promises = [
|
|
293
|
+
this.subscriber.trackEvent('shutdown_test_1', {}),
|
|
294
|
+
this.subscriber.trackEvent('shutdown_test_2', {}),
|
|
295
|
+
this.subscriber.trackEvent('shutdown_test_3', {}),
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
// Call shutdown
|
|
299
|
+
await this.subscriber.shutdown?.();
|
|
300
|
+
|
|
301
|
+
// Wait for operations to complete
|
|
302
|
+
const results = await Promise.allSettled(promises);
|
|
303
|
+
|
|
304
|
+
const allSettled = results.every(
|
|
305
|
+
(r) => r.status === 'fulfilled' || r.status === 'rejected'
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
if (!allSettled) {
|
|
309
|
+
throw new Error('Some promises never settled');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
name: 'Graceful Shutdown',
|
|
314
|
+
passed: true,
|
|
315
|
+
duration: performance.now() - startTime,
|
|
316
|
+
details: 'Shutdown completed, all requests settled',
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return {
|
|
320
|
+
name: 'Graceful Shutdown',
|
|
321
|
+
passed: false,
|
|
322
|
+
duration: performance.now() - startTime,
|
|
323
|
+
error: error as Error,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Pretty-print test results to console.
|
|
330
|
+
*/
|
|
331
|
+
static printResults(results: TestSuiteResult): void {
|
|
332
|
+
console.log('\n' + '='.repeat(60));
|
|
333
|
+
console.log('š Subscriber Test Results');
|
|
334
|
+
console.log('='.repeat(60));
|
|
335
|
+
console.log(`\nTotal Tests: ${results.total}`);
|
|
336
|
+
console.log(`ā
Passed: ${results.passed_count}`);
|
|
337
|
+
console.log(`ā Failed: ${results.failed_count}`);
|
|
338
|
+
console.log(`ā±ļø Duration: ${results.duration.toFixed(2)}ms`);
|
|
339
|
+
console.log('\n' + '-'.repeat(60));
|
|
340
|
+
|
|
341
|
+
for (const result of results.results) {
|
|
342
|
+
const icon = result.passed ? 'ā
' : 'ā';
|
|
343
|
+
const duration = result.duration.toFixed(2);
|
|
344
|
+
console.log(`${icon} ${result.name} (${duration}ms)`);
|
|
345
|
+
|
|
346
|
+
if (result.details) {
|
|
347
|
+
console.log(` ${result.details}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (result.error) {
|
|
351
|
+
console.log(` Error: ${result.error.message}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log('='.repeat(60));
|
|
356
|
+
|
|
357
|
+
if (results.passed) {
|
|
358
|
+
console.log('\nš All tests passed! Your subscriber is ready to use.');
|
|
359
|
+
} else {
|
|
360
|
+
console.log('\nā ļø Some tests failed. Review the errors above.');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
console.log('\n');
|
|
364
|
+
}
|
|
365
|
+
}
|