@chirpier/chirpier-js 0.1.6 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chirpier/chirpier-js",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Chirpier SDK for JavaScript",
5
5
  "keywords": [
6
6
  "chirpier",
@@ -30,7 +30,7 @@
30
30
  "async-lock": "^1.4.1",
31
31
  "axios": "^0.24.0",
32
32
  "axios-retry": "^4.5.0",
33
- "js-base64": "^3.7.7",
33
+ "dotenv": "^16.4.7",
34
34
  "ts-node": "^10.9.2",
35
35
  "tslib": "^2.3.0"
36
36
  },
@@ -1,192 +1,364 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import axios from "axios";
5
+ import MockAdapter from "axios-mock-adapter";
1
6
  import {
2
- Chirpier,
7
+ Client,
3
8
  ChirpierError,
4
- Event,
9
+ Log,
5
10
  LogLevel,
11
+ createClient,
12
+ flush,
6
13
  initialize,
7
- monitor,
14
+ logEvent,
15
+ stop,
8
16
  } from "../index";
9
17
  import {
10
18
  DEFAULT_API_ENDPOINT,
11
- DEFAULT_RETRIES,
12
- DEFAULT_TIMEOUT,
13
- DEFAULT_BATCH_SIZE,
14
- DEFAULT_FLUSH_DELAY,
15
19
  } from "../constants";
16
- import MockAdapter from "axios-mock-adapter";
17
- import axios from "axios";
18
20
 
19
21
  describe("Chirpier SDK", () => {
22
+ afterEach(async () => {
23
+ await stop();
24
+ });
25
+
20
26
  describe("Initialization", () => {
21
- test("should throw error if monitor is called before initialize", () => {
22
- const event: Event = {
23
- group_id: "bfd9299d-817a-452f-bc53-6e154f2281fc",
24
- stream_name: "test-stream",
27
+ test("should throw error if logEvent is called before initialize", async () => {
28
+ const log: Log = {
29
+ event: "test-event",
25
30
  value: 1,
26
31
  };
27
32
 
28
- expect(() => monitor(event)).toThrow(ChirpierError);
29
- expect(() => monitor(event)).toThrow(
33
+ await expect(logEvent(log)).rejects.toThrow(ChirpierError);
34
+ await expect(logEvent(log)).rejects.toThrow(
30
35
  "Chirpier SDK is not initialized. Please call initialize() first."
31
36
  );
32
37
  });
33
38
 
34
- test("should initialize with default values", () => {
39
+ test("should initialize with default values", async () => {
40
+ const mock = new MockAdapter(axios);
41
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
42
+
35
43
  initialize({
36
- key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
44
+ key: "chp_test_default_key",
37
45
  logLevel: LogLevel.None,
38
46
  });
39
- const chirpier = Chirpier.getInstance({
40
- key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
41
- });
42
47
 
43
- // Setup mock server
48
+ await logEvent({ event: "sdk.initialized", value: 1 });
49
+ await new Promise((resolve) => setTimeout(resolve, 2000));
50
+ expect(mock.history.post[0].url).toBe(DEFAULT_API_ENDPOINT);
51
+ });
52
+
53
+ test("should initialize with custom apiEndpoint", async () => {
54
+ const customEndpoint = "https://localhost:3001/v1.0/logs";
44
55
  const mock = new MockAdapter(axios);
45
- mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
56
+ mock.onPost(customEndpoint).reply(200, { success: true });
46
57
 
47
- expect(chirpier?.["apiEndpoint"]).toBe(DEFAULT_API_ENDPOINT);
48
- expect(chirpier?.["retries"]).toBe(DEFAULT_RETRIES);
49
- expect(chirpier?.["timeout"]).toBe(DEFAULT_TIMEOUT);
50
- expect(chirpier?.["batchSize"]).toBe(DEFAULT_BATCH_SIZE);
51
- expect(chirpier?.["flushDelay"]).toBe(DEFAULT_FLUSH_DELAY);
58
+ initialize({
59
+ key: "chp_test_custom_endpoint",
60
+ apiEndpoint: customEndpoint,
61
+ logLevel: LogLevel.None,
62
+ });
52
63
 
53
- Chirpier.stop();
64
+ await logEvent({ event: "sdk.custom-endpoint", value: 1 });
65
+ await new Promise((resolve) => setTimeout(resolve, 2000));
66
+ expect(mock.history.post[0].url).toBe(customEndpoint);
54
67
  });
55
68
 
56
- test("should throw error if key is not provided", () => {
69
+ test("should throw error for invalid apiEndpoint", () => {
57
70
  expect(() => {
58
71
  initialize({
59
- key: "api_key",
60
- logLevel: LogLevel.None,
72
+ key: "chp_test_invalid_endpoint",
73
+ apiEndpoint: "not-a-url",
61
74
  });
62
- }).toThrow(ChirpierError);
75
+ }).toThrow("apiEndpoint must be a valid absolute URL");
76
+ });
77
+
78
+ test("should throw error for invalid key prefix", () => {
63
79
  expect(() => {
64
80
  initialize({
65
- key: "api_key",
81
+ key: "invalid_key",
66
82
  logLevel: LogLevel.None,
67
83
  });
68
- }).toThrow("Invalid API key: Not a valid JWT");
84
+ }).toThrow("Invalid API key: must start with 'chp_'");
85
+ });
86
+
87
+ test("should load key from process environment", () => {
88
+ const previousKey = process.env.CHIRPIER_API_KEY;
89
+ process.env.CHIRPIER_API_KEY = "chp_env_key";
90
+
91
+ try {
92
+ initialize({ logLevel: LogLevel.None });
93
+ expect(() => initialize({ logLevel: LogLevel.None })).not.toThrow();
94
+ } finally {
95
+ if (previousKey === undefined) {
96
+ delete process.env.CHIRPIER_API_KEY;
97
+ } else {
98
+ process.env.CHIRPIER_API_KEY = previousKey;
99
+ }
100
+ }
101
+ });
102
+
103
+ test("should load key from .env fallback", () => {
104
+ const previousKey = process.env.CHIRPIER_API_KEY;
105
+ const previousCwd = process.cwd();
106
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "chirpier-js-"));
107
+
108
+ try {
109
+ delete process.env.CHIRPIER_API_KEY;
110
+ fs.writeFileSync(path.join(tempDir, ".env"), "CHIRPIER_API_KEY=chp_dotenv_key\n");
111
+ process.chdir(tempDir);
112
+
113
+ initialize({ logLevel: LogLevel.None });
114
+ expect(() => initialize({ logLevel: LogLevel.None })).not.toThrow();
115
+ } finally {
116
+ process.chdir(previousCwd);
117
+ if (previousKey === undefined) {
118
+ delete process.env.CHIRPIER_API_KEY;
119
+ } else {
120
+ process.env.CHIRPIER_API_KEY = previousKey;
121
+ }
122
+ }
69
123
  });
70
124
  });
71
125
 
72
- describe("monitor", () => {
73
- test("event should be sent", async () => {
74
- // Setup mock server
126
+ describe("logEvent", () => {
127
+ test("log should be sent", async () => {
75
128
  const mock = new MockAdapter(axios);
76
129
  mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
77
130
 
78
131
  initialize({
79
- key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
132
+ key: "chp_log_send_key",
80
133
  logLevel: LogLevel.None,
81
134
  });
82
135
 
83
- const event: Event = {
84
- group_id: "bfd9299d-817a-452f-bc53-6e154f2281fc",
85
- stream_name: "test-stream",
136
+ const log: Log = {
137
+ agent_id: "api.worker",
138
+ event: "request.finished",
86
139
  value: 1,
87
140
  };
88
141
 
89
- monitor(event);
90
-
91
- await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for flush
142
+ await logEvent(log);
143
+ await new Promise((resolve) => setTimeout(resolve, 2000));
92
144
 
93
145
  expect(mock.history.post.length).toBe(1);
94
146
  expect(mock.history.post[0].url).toBe(DEFAULT_API_ENDPOINT);
95
147
  expect(JSON.parse(mock.history.post[0].data)).toEqual([
96
148
  {
97
- group_id: "bfd9299d-817a-452f-bc53-6e154f2281fc",
98
- stream_name: "test-stream",
149
+ agent_id: "api.worker",
150
+ event: "request.finished",
99
151
  value: 1,
100
152
  },
101
153
  ]);
102
-
103
- // Clean up the mock
104
- mock.reset();
105
- Chirpier.stop();
106
154
  });
107
155
 
108
- test("should silently drop invalid event", async () => {
156
+ test("agent_id whitespace should be omitted", async () => {
109
157
  const mock = new MockAdapter(axios);
110
158
  mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
111
159
 
112
160
  initialize({
113
- key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
114
- logLevel: LogLevel.Debug,
161
+ key: "chp_log_whitespace_agent",
162
+ logLevel: LogLevel.None,
115
163
  });
116
164
 
117
- const invalidEvent = {
118
- group_id: "invalid-uuid",
119
- stream_name: "",
120
- value: 0,
121
- } as Event;
165
+ await logEvent({
166
+ agent_id: " ",
167
+ event: "metric.tick",
168
+ value: 42,
169
+ });
122
170
 
123
- const consoleSpy = jest.spyOn(console, "debug");
171
+ await new Promise((resolve) => setTimeout(resolve, 2000));
172
+ const payload = JSON.parse(mock.history.post[0].data);
173
+ expect(payload[0].agent_id).toBeUndefined();
174
+ });
124
175
 
125
- await monitor(invalidEvent);
176
+ test("should support meta payload", async () => {
177
+ const mock = new MockAdapter(axios);
178
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
126
179
 
127
- expect(consoleSpy).toHaveBeenCalledWith(
128
- "Invalid event format, dropping event:",
129
- invalidEvent
130
- );
131
- expect(mock.history.post.length).toBe(0); // No request should be made
180
+ initialize({
181
+ key: "chp_log_meta_key",
182
+ logLevel: LogLevel.None,
183
+ });
184
+
185
+ await logEvent({
186
+ agent_id: "api.worker",
187
+ event: "request.finished",
188
+ value: 200,
189
+ meta: {
190
+ path: "/v1.0/logs",
191
+ status: "ok",
192
+ },
193
+ });
132
194
 
133
- // Clean up
134
- mock.reset();
135
- consoleSpy.mockRestore();
136
- Chirpier.stop();
195
+ await new Promise((resolve) => setTimeout(resolve, 2000));
196
+ const payload = JSON.parse(mock.history.post[0].data);
197
+ expect(payload[0].meta.path).toBe("/v1.0/logs");
137
198
  });
138
199
 
139
- test("should batch events and flush when batch size is reached", async () => {
200
+ test("should support occurred_at timestamp", async () => {
140
201
  const mock = new MockAdapter(axios);
141
202
  mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
142
203
 
143
204
  initialize({
144
- key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
205
+ key: "chp_log_occurred_at_key",
145
206
  logLevel: LogLevel.None,
146
207
  });
147
208
 
148
- const event: Event = {
149
- group_id: "bfd9299d-817a-452f-bc53-6e154f2281fc",
150
- stream_name: "test-stream",
209
+ const occurredAt = new Date(Date.now() - 2 * 60 * 60 * 1000);
210
+
211
+ await logEvent({
212
+ event: "request.finished",
151
213
  value: 1,
152
- };
214
+ occurred_at: occurredAt,
215
+ });
216
+
217
+ await new Promise((resolve) => setTimeout(resolve, 2000));
218
+ const payload = JSON.parse(mock.history.post[0].data);
219
+ expect(payload[0].occurred_at).toBe(occurredAt.toISOString());
220
+ });
153
221
 
154
- monitor(event);
155
- monitor(event);
222
+ test("should throw error for invalid log", async () => {
223
+ const mock = new MockAdapter(axios);
224
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
156
225
 
157
- await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for flush
226
+ initialize({
227
+ key: "chp_invalid_log_key",
228
+ logLevel: LogLevel.Debug,
229
+ });
158
230
 
159
- expect(mock.history.post.length).toBe(1);
160
- expect(JSON.parse(mock.history.post[0].data).length).toBe(2);
231
+ await expect(
232
+ logEvent({
233
+ event: "",
234
+ value: 0,
235
+ })
236
+ ).rejects.toThrow(ChirpierError);
237
+
238
+ await expect(
239
+ logEvent({
240
+ event: "too-old",
241
+ value: 1,
242
+ occurred_at: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000),
243
+ })
244
+ ).rejects.toThrow(ChirpierError);
245
+
246
+ await expect(
247
+ logEvent({
248
+ event: "too-future",
249
+ value: 1,
250
+ occurred_at: new Date(Date.now() + 25 * 60 * 60 * 1000),
251
+ })
252
+ ).rejects.toThrow(ChirpierError);
161
253
 
162
- mock.reset();
163
- Chirpier.stop();
254
+ expect(mock.history.post.length).toBe(0);
164
255
  });
165
256
 
166
- test("should flush events after interval", async () => {
257
+ test("should batch logs and flush when batch size is reached", async () => {
167
258
  const mock = new MockAdapter(axios);
168
259
  mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
169
260
 
170
261
  initialize({
171
- key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
262
+ key: "chp_batch_key",
172
263
  logLevel: LogLevel.None,
173
264
  });
174
265
 
175
- const event: Event = {
176
- group_id: "bfd9299d-817a-452f-bc53-6e154f2281fc",
177
- stream_name: "test-stream",
266
+ await logEvent({ event: "batch.event", value: 1 });
267
+ await logEvent({ event: "batch.event", value: 2 });
268
+
269
+ await new Promise((resolve) => setTimeout(resolve, 2000));
270
+
271
+ expect(mock.history.post.length).toBe(1);
272
+ expect(JSON.parse(mock.history.post[0].data).length).toBe(2);
273
+ });
274
+ });
275
+
276
+ describe("Client API", () => {
277
+ test("createClient supports direct logging", async () => {
278
+ const mock = new MockAdapter(axios);
279
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
280
+
281
+ const client: Client = createClient({ key: "chp_direct_client_key" });
282
+ await client.log({
283
+ event: "direct.client.log",
178
284
  value: 1,
179
- };
285
+ });
286
+
287
+ await new Promise((resolve) => setTimeout(resolve, 2000));
288
+ expect(mock.history.post.length).toBe(1);
289
+ expect(JSON.parse(mock.history.post[0].data)[0].event).toBe("direct.client.log");
180
290
 
181
- monitor(event);
291
+ await client.shutdown();
292
+ });
293
+
294
+ test("flush should force delivery of queued logs", async () => {
295
+ const mock = new MockAdapter(axios);
296
+ mock.onPost(DEFAULT_API_ENDPOINT).reply(200, { success: true });
297
+
298
+ initialize({
299
+ key: "chp_flush_key",
300
+ logLevel: LogLevel.None,
301
+ flushDelay: 10000,
302
+ });
182
303
 
183
- await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for flush
304
+ await logEvent({ event: "queued.before.flush", value: 1 });
305
+ expect(mock.history.post.length).toBe(0);
184
306
 
307
+ await flush();
185
308
  expect(mock.history.post.length).toBe(1);
186
- expect(JSON.parse(mock.history.post[0].data).length).toBe(1);
309
+ expect(JSON.parse(mock.history.post[0].data)[0].event).toBe("queued.before.flush");
310
+ });
311
+
312
+ test("getEventLogs uses servicer endpoint with period, limit, and offset", async () => {
313
+ const mock = new MockAdapter(axios);
314
+ mock.onGet("https://api.chirpier.co/v1.0/events/evt_123/logs?period=hour&limit=25&offset=10").reply(200, []);
315
+
316
+ const client: Client = createClient({ key: "chp_client_logs_key" });
317
+ try {
318
+ await client.getEventLogs("evt_123", { period: "hour", limit: 25, offset: 10 });
319
+ expect(mock.history.get[0].url).toBe("https://api.chirpier.co/v1.0/events/evt_123/logs?period=hour&limit=25&offset=10");
320
+ } finally {
321
+ await client.shutdown();
322
+ }
323
+ });
187
324
 
188
- mock.reset();
189
- Chirpier.stop();
325
+ test("getAlertDeliveries uses pagination params", async () => {
326
+ const mock = new MockAdapter(axios);
327
+ mock.onGet("https://api.chirpier.co/v1.0/alerts/alrt_123/deliveries?kind=test&limit=20&offset=5").reply(200, []);
328
+
329
+ const client: Client = createClient({ key: "chp_client_alert_key" });
330
+ try {
331
+ await client.getAlertDeliveries("alrt_123", { kind: "test", limit: 20, offset: 5 });
332
+ expect(mock.history.get[0].url).toBe("https://api.chirpier.co/v1.0/alerts/alrt_123/deliveries?kind=test&limit=20&offset=5");
333
+ } finally {
334
+ await client.shutdown();
335
+ }
336
+ });
337
+
338
+ test("archiveAlert posts to servicer endpoint", async () => {
339
+ const mock = new MockAdapter(axios);
340
+ mock.onPost("https://api.chirpier.co/v1.0/alerts/alrt_123/archive").reply(200, {});
341
+
342
+ const client: Client = createClient({ key: "chp_client_alert_key" });
343
+ try {
344
+ await client.archiveAlert("alrt_123");
345
+ expect(mock.history.post[0].url).toBe("https://api.chirpier.co/v1.0/alerts/alrt_123/archive");
346
+ } finally {
347
+ await client.shutdown();
348
+ }
349
+ });
350
+
351
+ test("testWebhook posts to servicer endpoint", async () => {
352
+ const mock = new MockAdapter(axios);
353
+ mock.onPost("https://api.chirpier.co/v1.0/webhooks/whk_123/test").reply(200);
354
+
355
+ const client: Client = createClient({ key: "chp_client_webhook_key" });
356
+ try {
357
+ await client.testWebhook("whk_123");
358
+ expect(mock.history.post[0].url).toBe("https://api.chirpier.co/v1.0/webhooks/whk_123/test");
359
+ } finally {
360
+ await client.shutdown();
361
+ }
190
362
  });
191
363
  });
192
364
  });
package/src/constants.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  // constants.ts
2
2
 
3
- export const DEFAULT_RETRIES = 15;
4
- export const DEFAULT_TIMEOUT = 5000
5
- export const DEFAULT_BATCH_SIZE = 350;
6
- export const DEFAULT_FLUSH_DELAY = 500;
7
- export const MAX_QUEUE_SIZE = 50000;
8
- export const DEFAULT_API_ENDPOINT = "https://eu-west.chirpier.co/v1.0/events"
3
+ export const DEFAULT_RETRIES = 15 as const;
4
+ export const DEFAULT_TIMEOUT = 5000 as const;
5
+ export const DEFAULT_BATCH_SIZE = 500 as const;
6
+ export const DEFAULT_FLUSH_DELAY = 500 as const;
7
+ export const MAX_QUEUE_SIZE = 5000 as const;
8
+ export const DEFAULT_API_ENDPOINT = "https://logs.chirpier.co/v1.0/logs" as const;
9
+ export const DEFAULT_SERVICER_ENDPOINT = "https://api.chirpier.co/v1.0" as const;