@chirpier/chirpier-js 0.2.1 → 0.4.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 +42 -10
- package/dist/__tests__/chirpier.test.js +410 -7
- package/dist/index.d.ts +67 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +227 -14
- package/package.json +1 -1
- package/src/__tests__/chirpier.test.ts +267 -6
- package/src/index.ts +232 -16
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import axios from "axios";
|
|
4
|
+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
|
5
5
|
import MockAdapter from "axios-mock-adapter";
|
|
6
6
|
import {
|
|
7
7
|
Client,
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
|
|
21
21
|
describe("Chirpier SDK", () => {
|
|
22
22
|
afterEach(async () => {
|
|
23
|
+
jest.useRealTimers();
|
|
24
|
+
jest.restoreAllMocks();
|
|
23
25
|
await stop();
|
|
24
26
|
});
|
|
25
27
|
|
|
@@ -341,6 +343,203 @@ describe("Chirpier SDK", () => {
|
|
|
341
343
|
expect(JSON.parse(mock.history.post[0].data)[0].event).toBe("queued.before.flush");
|
|
342
344
|
});
|
|
343
345
|
|
|
346
|
+
test("retries network errors", async () => {
|
|
347
|
+
jest.useFakeTimers();
|
|
348
|
+
const client: Client = createClient({
|
|
349
|
+
key: "chp_network_retry_key",
|
|
350
|
+
retries: 2,
|
|
351
|
+
flushDelay: 10000,
|
|
352
|
+
});
|
|
353
|
+
const axiosBackedClient = client as unknown as { axiosInstance: AxiosInstance };
|
|
354
|
+
let attempts = 0;
|
|
355
|
+
axiosBackedClient.axiosInstance.defaults.adapter = async (
|
|
356
|
+
config: AxiosRequestConfig
|
|
357
|
+
): Promise<AxiosResponse> => {
|
|
358
|
+
attempts += 1;
|
|
359
|
+
if (attempts < 3) {
|
|
360
|
+
throw Object.assign(new Error("Network Error"), {
|
|
361
|
+
code: "ECONNRESET",
|
|
362
|
+
config,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
data: { success: true },
|
|
368
|
+
status: 200,
|
|
369
|
+
statusText: "OK",
|
|
370
|
+
headers: {},
|
|
371
|
+
config,
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
await client.log({ event: "network.retry", value: 1 });
|
|
377
|
+
const flushPromise = client.flush();
|
|
378
|
+
await jest.runAllTimersAsync();
|
|
379
|
+
await expect(flushPromise).resolves.toBeUndefined();
|
|
380
|
+
expect(attempts).toBe(3);
|
|
381
|
+
} finally {
|
|
382
|
+
await client.shutdown();
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("retries 429 responses", async () => {
|
|
387
|
+
jest.useFakeTimers();
|
|
388
|
+
const mock = new MockAdapter(axios);
|
|
389
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(429, { error: "rate limited" }, { "retry-after": "1" });
|
|
390
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(429, { error: "rate limited" }, { "retry-after": "1" });
|
|
391
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(200, { success: true });
|
|
392
|
+
|
|
393
|
+
const client: Client = createClient({
|
|
394
|
+
key: "chp_rate_limit_key",
|
|
395
|
+
retries: 2,
|
|
396
|
+
flushDelay: 10000,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
await client.log({ event: "rate.limit", value: 1 });
|
|
401
|
+
const flushPromise = client.flush();
|
|
402
|
+
await jest.runAllTimersAsync();
|
|
403
|
+
await expect(flushPromise).resolves.toBeUndefined();
|
|
404
|
+
expect(mock.history.post.length).toBe(3);
|
|
405
|
+
} finally {
|
|
406
|
+
await client.shutdown();
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("does not retry 401 and logs the Chirpier response message", async () => {
|
|
411
|
+
const mock = new MockAdapter(axios);
|
|
412
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
413
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(401, { error: "invalid api key" });
|
|
414
|
+
|
|
415
|
+
const client: Client = createClient({
|
|
416
|
+
key: "chp_unauthorized_key",
|
|
417
|
+
retries: 3,
|
|
418
|
+
flushDelay: 10000,
|
|
419
|
+
logLevel: LogLevel.Error,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
await client.log({ event: "auth.failed", value: 1 });
|
|
424
|
+
await client.flush();
|
|
425
|
+
await client.flush();
|
|
426
|
+
|
|
427
|
+
expect(mock.history.post.length).toBe(1);
|
|
428
|
+
expect(errorSpy).toHaveBeenCalledWith("Chirpier API returned 401: invalid api key");
|
|
429
|
+
} finally {
|
|
430
|
+
await client.shutdown();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("does not retry 403 and logs the Chirpier response message", async () => {
|
|
435
|
+
const mock = new MockAdapter(axios);
|
|
436
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation(() => undefined);
|
|
437
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(403, { message: "project is disabled" });
|
|
438
|
+
|
|
439
|
+
const client: Client = createClient({
|
|
440
|
+
key: "chp_forbidden_key",
|
|
441
|
+
retries: 3,
|
|
442
|
+
flushDelay: 10000,
|
|
443
|
+
logLevel: LogLevel.Error,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await client.log({ event: "auth.forbidden", value: 1 });
|
|
448
|
+
await client.flush();
|
|
449
|
+
await client.flush();
|
|
450
|
+
|
|
451
|
+
expect(mock.history.post.length).toBe(1);
|
|
452
|
+
expect(errorSpy).toHaveBeenCalledWith("Chirpier API returned 403: project is disabled");
|
|
453
|
+
} finally {
|
|
454
|
+
await client.shutdown();
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test.each([404, 500, 503])( "does not retry non-retryable status %s", async (status) => {
|
|
459
|
+
const mock = new MockAdapter(axios);
|
|
460
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(status, { error: `http ${status}` });
|
|
461
|
+
|
|
462
|
+
const client: Client = createClient({
|
|
463
|
+
key: `chp_non_retryable_${status}`,
|
|
464
|
+
retries: 3,
|
|
465
|
+
flushDelay: 10000,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
await client.log({ event: `non.retryable.${status}`, value: 1 });
|
|
470
|
+
await client.flush();
|
|
471
|
+
await client.flush();
|
|
472
|
+
|
|
473
|
+
expect(mock.history.post.length).toBe(1);
|
|
474
|
+
} finally {
|
|
475
|
+
await client.shutdown();
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test("retries 502 responses", async () => {
|
|
480
|
+
jest.useFakeTimers();
|
|
481
|
+
const mock = new MockAdapter(axios);
|
|
482
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(502, { error: "bad gateway" });
|
|
483
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(502, { error: "bad gateway" });
|
|
484
|
+
mock.onPost(DEFAULT_API_ENDPOINT).replyOnce(200, { success: true });
|
|
485
|
+
|
|
486
|
+
const client: Client = createClient({
|
|
487
|
+
key: "chp_bad_gateway_key",
|
|
488
|
+
retries: 2,
|
|
489
|
+
flushDelay: 10000,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await client.log({ event: "bad.gateway", value: 1 });
|
|
494
|
+
const flushPromise = client.flush();
|
|
495
|
+
await jest.runAllTimersAsync();
|
|
496
|
+
await expect(flushPromise).resolves.toBeUndefined();
|
|
497
|
+
expect(mock.history.post.length).toBe(3);
|
|
498
|
+
} finally {
|
|
499
|
+
await client.shutdown();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test("flush requeues only retryable failures", async () => {
|
|
504
|
+
const retryableMock = new MockAdapter(axios);
|
|
505
|
+
retryableMock.onPost(DEFAULT_API_ENDPOINT).replyOnce(502, { error: "bad gateway" });
|
|
506
|
+
retryableMock.onPost(DEFAULT_API_ENDPOINT).replyOnce(200, { success: true });
|
|
507
|
+
|
|
508
|
+
const retryableClient: Client = createClient({
|
|
509
|
+
key: "chp_retryable_requeue_key",
|
|
510
|
+
retries: 0,
|
|
511
|
+
flushDelay: 10000,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
await retryableClient.log({ event: "retryable.requeue", value: 1 });
|
|
516
|
+
await retryableClient.flush();
|
|
517
|
+
await retryableClient.flush();
|
|
518
|
+
expect(retryableMock.history.post.length).toBe(2);
|
|
519
|
+
} finally {
|
|
520
|
+
await retryableClient.shutdown();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const nonRetryableMock = new MockAdapter(axios);
|
|
524
|
+
nonRetryableMock.onPost(DEFAULT_API_ENDPOINT).replyOnce(404, { error: "not found" });
|
|
525
|
+
|
|
526
|
+
const nonRetryableClient: Client = createClient({
|
|
527
|
+
key: "chp_non_retryable_requeue_key",
|
|
528
|
+
retries: 0,
|
|
529
|
+
flushDelay: 10000,
|
|
530
|
+
logLevel: LogLevel.None,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
await nonRetryableClient.log({ event: "non.retryable.drop", value: 1 });
|
|
535
|
+
await nonRetryableClient.flush();
|
|
536
|
+
await nonRetryableClient.flush();
|
|
537
|
+
expect(nonRetryableMock.history.post.length).toBe(1);
|
|
538
|
+
} finally {
|
|
539
|
+
await nonRetryableClient.shutdown();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
344
543
|
test("getEventLogs uses servicer endpoint with period, limit, and offset", async () => {
|
|
345
544
|
const mock = new MockAdapter(axios);
|
|
346
545
|
mock.onGet("https://api.chirpier.co/v1.0/events/evt_123/logs?period=hour&limit=25&offset=10").reply(200, []);
|
|
@@ -354,6 +553,39 @@ describe("Chirpier SDK", () => {
|
|
|
354
553
|
}
|
|
355
554
|
});
|
|
356
555
|
|
|
556
|
+
test("event, policy, alert, and destination helpers use servicer endpoints", async () => {
|
|
557
|
+
const mock = new MockAdapter(axios);
|
|
558
|
+
mock.onPost("https://api.chirpier.co/v1.0/events").reply(200, { event_id: "evt_123", event: "tool.errors.count", public: false, timezone: "UTC" });
|
|
559
|
+
mock.onGet("https://api.chirpier.co/v1.0/events/evt_123").reply(200, { event_id: "evt_123", event: "tool.errors.count", public: false, timezone: "UTC" });
|
|
560
|
+
mock.onGet("https://api.chirpier.co/v1.0/policies/pol_123").reply(200, { policy_id: "pol_123", event_id: "evt_123", title: "Policy", channel: "ops", period: "hour", aggregate: "sum", condition: "gt", threshold: 1, severity: "warning", enabled: true });
|
|
561
|
+
mock.onPut("https://api.chirpier.co/v1.0/policies/pol_123").reply(200, { policy_id: "pol_123", event_id: "evt_123", title: "Updated", channel: "ops", period: "hour", aggregate: "sum", condition: "gt", threshold: 1, severity: "warning", enabled: true });
|
|
562
|
+
mock.onGet("https://api.chirpier.co/v1.0/alerts/alrt_123").reply(200, { alert_id: "alrt_123", policy_id: "pol_123", event_id: "evt_123", event: "tool.errors.count", title: "Alert", channel: "ops", period: "hour", aggregate: "sum", condition: "gt", threshold: 1, severity: "warning", status: "triggered", value: 2, count: 2, min: 1, max: 1 });
|
|
563
|
+
mock.onGet("https://api.chirpier.co/v1.0/destinations").reply(200, []);
|
|
564
|
+
mock.onPost("https://api.chirpier.co/v1.0/destinations").reply(200, { destination_id: "dst_123", channel: "slack", scope: "all", enabled: true });
|
|
565
|
+
mock.onGet("https://api.chirpier.co/v1.0/destinations/dst_123").reply(200, { destination_id: "dst_123", channel: "slack", scope: "all", enabled: true });
|
|
566
|
+
mock.onPut("https://api.chirpier.co/v1.0/destinations/dst_123").reply(200, { destination_id: "dst_123", channel: "slack", scope: "all", enabled: false });
|
|
567
|
+
|
|
568
|
+
const client: Client = createClient({ key: "chp_client_route_key" });
|
|
569
|
+
try {
|
|
570
|
+
const createdEvent = await client.createEvent({ event: "tool.errors.count" });
|
|
571
|
+
expect(createdEvent.event_id).toBe("evt_123");
|
|
572
|
+
await client.getEvent("evt_123");
|
|
573
|
+
const policy = await client.getPolicy("pol_123");
|
|
574
|
+
expect(policy.policy_id).toBe("pol_123");
|
|
575
|
+
await client.updatePolicy("pol_123", { title: "Updated" });
|
|
576
|
+
const alert = await client.getAlert("alrt_123");
|
|
577
|
+
expect(alert.alert_id).toBe("alrt_123");
|
|
578
|
+
await client.listDestinations();
|
|
579
|
+
const destination = await client.createDestination({ channel: "slack", scope: "all", enabled: true });
|
|
580
|
+
expect(destination.destination_id).toBe("dst_123");
|
|
581
|
+
await client.getDestination("dst_123");
|
|
582
|
+
const updatedDestination = await client.updateDestination("dst_123", { enabled: false });
|
|
583
|
+
expect(updatedDestination.enabled).toBe(false);
|
|
584
|
+
} finally {
|
|
585
|
+
await client.shutdown();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
357
589
|
test("getAlertDeliveries uses pagination params", async () => {
|
|
358
590
|
const mock = new MockAdapter(axios);
|
|
359
591
|
mock.onGet("https://api.chirpier.co/v1.0/alerts/alrt_123/deliveries?kind=test&limit=20&offset=5").reply(200, []);
|
|
@@ -380,17 +612,46 @@ describe("Chirpier SDK", () => {
|
|
|
380
612
|
}
|
|
381
613
|
});
|
|
382
614
|
|
|
383
|
-
test("
|
|
615
|
+
test("testDestination posts to servicer endpoint", async () => {
|
|
384
616
|
const mock = new MockAdapter(axios);
|
|
385
|
-
|
|
617
|
+
mock.onPost("https://api.chirpier.co/v1.0/destinations/whk_123/test").reply(200, {
|
|
618
|
+
alert_id: "alrt_123",
|
|
619
|
+
destination_id: "whk_123",
|
|
620
|
+
status: "sent",
|
|
621
|
+
});
|
|
386
622
|
|
|
387
|
-
const client: Client = createClient({ key: "
|
|
623
|
+
const client: Client = createClient({ key: "chp_client_destination_key" });
|
|
388
624
|
try {
|
|
389
|
-
|
|
390
|
-
|
|
625
|
+
const result = await client.testDestination("whk_123");
|
|
626
|
+
expect(result.alert_id).toBe("alrt_123");
|
|
627
|
+
expect(mock.history.post[0].url).toBe("https://api.chirpier.co/v1.0/destinations/whk_123/test");
|
|
391
628
|
} finally {
|
|
392
629
|
await client.shutdown();
|
|
393
630
|
}
|
|
394
631
|
});
|
|
632
|
+
|
|
633
|
+
test("getEventAnalytics uses analytics endpoint", async () => {
|
|
634
|
+
const mock = new MockAdapter(axios);
|
|
635
|
+
mock.onGet("https://api.chirpier.co/v1.0/events/evt_123/analytics?view=window&period=1h&previous=previous_window").reply(200, {
|
|
636
|
+
event_id: "evt_123",
|
|
637
|
+
view: "window",
|
|
638
|
+
period: "1h",
|
|
639
|
+
previous: "previous_window",
|
|
640
|
+
data: null,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const client: Client = createClient({ key: "chp_client_analytics_key" });
|
|
644
|
+
try {
|
|
645
|
+
const analytics = await client.getEventAnalytics("evt_123", {
|
|
646
|
+
view: "window",
|
|
647
|
+
period: "1h",
|
|
648
|
+
previous: "previous_window",
|
|
649
|
+
});
|
|
650
|
+
expect(analytics.event_id).toBe("evt_123");
|
|
651
|
+
expect(mock.history.get[0].url).toBe("https://api.chirpier.co/v1.0/events/evt_123/analytics?view=window&period=1h&previous=previous_window");
|
|
652
|
+
} finally {
|
|
653
|
+
await client.shutdown();
|
|
654
|
+
}
|
|
655
|
+
});
|
|
395
656
|
});
|
|
396
657
|
});
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,6 @@ export interface Config {
|
|
|
29
29
|
timeout?: number;
|
|
30
30
|
batchSize?: number;
|
|
31
31
|
flushDelay?: number;
|
|
32
|
-
/** @deprecated Queues are unbounded in memory. */
|
|
33
32
|
maxQueueSize?: number;
|
|
34
33
|
}
|
|
35
34
|
|
|
@@ -59,6 +58,16 @@ export interface Event {
|
|
|
59
58
|
readonly created_at?: string;
|
|
60
59
|
}
|
|
61
60
|
|
|
61
|
+
export interface CreateEventPayload {
|
|
62
|
+
agent?: string;
|
|
63
|
+
event: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
public?: boolean;
|
|
66
|
+
description?: string;
|
|
67
|
+
unit?: string;
|
|
68
|
+
timezone?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
export interface Policy {
|
|
63
72
|
readonly policy_id: string;
|
|
64
73
|
readonly event_id: string;
|
|
@@ -73,6 +82,10 @@ export interface Policy {
|
|
|
73
82
|
readonly enabled: boolean;
|
|
74
83
|
}
|
|
75
84
|
|
|
85
|
+
export type CreatePolicyPayload = Omit<Policy, "policy_id">;
|
|
86
|
+
|
|
87
|
+
export type UpdatePolicyPayload = Partial<Omit<Policy, "policy_id">>;
|
|
88
|
+
|
|
76
89
|
export interface Alert {
|
|
77
90
|
readonly alert_id: string;
|
|
78
91
|
readonly policy_id: string;
|
|
@@ -98,7 +111,7 @@ export interface Alert {
|
|
|
98
111
|
export interface AlertDelivery {
|
|
99
112
|
readonly attempt_id: string;
|
|
100
113
|
readonly alert_id: string;
|
|
101
|
-
readonly
|
|
114
|
+
readonly destination_id?: string;
|
|
102
115
|
readonly channel: string;
|
|
103
116
|
readonly target: string;
|
|
104
117
|
readonly status: string;
|
|
@@ -107,6 +120,20 @@ export interface AlertDelivery {
|
|
|
107
120
|
readonly created_at: string;
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
export interface Destination {
|
|
124
|
+
readonly destination_id: string;
|
|
125
|
+
readonly channel: string;
|
|
126
|
+
readonly url?: string;
|
|
127
|
+
readonly credentials?: Record<string, unknown>;
|
|
128
|
+
readonly scope: string;
|
|
129
|
+
readonly policy_ids?: string[];
|
|
130
|
+
readonly enabled: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CreateDestinationPayload = Omit<Destination, "destination_id">;
|
|
134
|
+
|
|
135
|
+
export type UpdateDestinationPayload = Partial<Omit<Destination, "destination_id">>;
|
|
136
|
+
|
|
110
137
|
export interface EventLogPoint {
|
|
111
138
|
readonly event_id: string;
|
|
112
139
|
readonly agent?: string;
|
|
@@ -120,6 +147,43 @@ export interface EventLogPoint {
|
|
|
120
147
|
readonly max: number;
|
|
121
148
|
}
|
|
122
149
|
|
|
150
|
+
export interface AnalyticsWindowQuery {
|
|
151
|
+
view: "window";
|
|
152
|
+
period: "1h" | "1d" | "7d" | "1m";
|
|
153
|
+
previous: "previous_window" | "previous_1d" | "previous_7d" | "previous_1m";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface AnalyticsWindowData {
|
|
157
|
+
readonly current_value: number;
|
|
158
|
+
readonly current_count: number;
|
|
159
|
+
readonly previous_value: number;
|
|
160
|
+
readonly previous_count: number;
|
|
161
|
+
readonly value_delta: number;
|
|
162
|
+
readonly count_delta: number;
|
|
163
|
+
readonly value_pct_change: number;
|
|
164
|
+
readonly count_pct_change: number;
|
|
165
|
+
readonly current_mean: number;
|
|
166
|
+
readonly previous_mean: number;
|
|
167
|
+
readonly mean_delta: number;
|
|
168
|
+
readonly mean_pct_change: number;
|
|
169
|
+
readonly current_stddev: number;
|
|
170
|
+
readonly previous_stddev: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface AnalyticsWindowResponse {
|
|
174
|
+
readonly event_id: string;
|
|
175
|
+
readonly view: "window";
|
|
176
|
+
readonly period: "1h" | "1d" | "7d" | "1m";
|
|
177
|
+
readonly previous: "previous_window" | "previous_1d" | "previous_7d" | "previous_1m";
|
|
178
|
+
readonly data: AnalyticsWindowData | null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface DestinationTestResult {
|
|
182
|
+
readonly alert_id: string;
|
|
183
|
+
readonly destination_id: string;
|
|
184
|
+
readonly status: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
123
187
|
export interface PaginationOptions {
|
|
124
188
|
period?: "minute" | "hour" | "day";
|
|
125
189
|
limit?: number;
|
|
@@ -140,6 +204,99 @@ export class ChirpierError extends Error {
|
|
|
140
204
|
}
|
|
141
205
|
}
|
|
142
206
|
|
|
207
|
+
type LogRetryPolicy = "success" | "retryable" | "retry_after" | "non_retryable";
|
|
208
|
+
|
|
209
|
+
function classifyLogResponseStatus(status?: number): LogRetryPolicy {
|
|
210
|
+
if (status === undefined) {
|
|
211
|
+
return "success";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (status === 429) {
|
|
215
|
+
return "retry_after";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (status >= 500) {
|
|
219
|
+
if (status === 500 || status === 503) {
|
|
220
|
+
return "non_retryable";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return "retryable";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (status >= 400) {
|
|
227
|
+
return "non_retryable";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return "success";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getChirpierResponseMessage(data: unknown): string | undefined {
|
|
234
|
+
if (typeof data === "string") {
|
|
235
|
+
const trimmed = data.trim();
|
|
236
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (data && typeof data === "object") {
|
|
240
|
+
const record = data as Record<string, unknown>;
|
|
241
|
+
if (typeof record.message === "string" && record.message.trim().length > 0) {
|
|
242
|
+
return record.message.trim();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (typeof record.error === "string" && record.error.trim().length > 0) {
|
|
246
|
+
return record.error.trim();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
return JSON.stringify(data);
|
|
251
|
+
} catch {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getRetryDelayMs(retryCount: number, error: unknown): number {
|
|
260
|
+
if (axios.isAxiosError(error) && error.response?.status === 429) {
|
|
261
|
+
const retryAfterHeader = error.response.headers?.["retry-after"];
|
|
262
|
+
const retryAfterSeconds = Number(retryAfterHeader);
|
|
263
|
+
if (Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0) {
|
|
264
|
+
return retryAfterSeconds * 1000;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const baseDelay = Math.pow(2, retryCount) * 1000;
|
|
269
|
+
const jitter = Math.random() * 0.3 * baseDelay;
|
|
270
|
+
return baseDelay + jitter;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeSendLogsError(error: unknown, logLevel: LogLevel): unknown {
|
|
274
|
+
if (!axios.isAxiosError(error)) {
|
|
275
|
+
return error;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const status = error.response?.status;
|
|
279
|
+
if (classifyLogResponseStatus(status) !== "non_retryable") {
|
|
280
|
+
return error;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const responseMessage = getChirpierResponseMessage(error.response?.data);
|
|
284
|
+
if ((status === 401 || status === 403) && logLevel >= LogLevel.Error) {
|
|
285
|
+
if (responseMessage) {
|
|
286
|
+
console.error(`Chirpier API returned ${status}: ${responseMessage}`);
|
|
287
|
+
} else {
|
|
288
|
+
console.error(`Chirpier API returned ${status}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const message = responseMessage ? `HTTP ${status}: ${responseMessage}` : `HTTP ${status}`;
|
|
293
|
+
return new ChirpierError(message, "NON_RETRYABLE_RESPONSE");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function isNonRetryableLogError(error: unknown): error is ChirpierError {
|
|
297
|
+
return error instanceof ChirpierError && error.code === "NON_RETRYABLE_RESPONSE";
|
|
298
|
+
}
|
|
299
|
+
|
|
143
300
|
interface QueuedLog {
|
|
144
301
|
readonly log: Log;
|
|
145
302
|
readonly timestamp: number;
|
|
@@ -247,17 +404,15 @@ export class Client {
|
|
|
247
404
|
|
|
248
405
|
axiosRetry(this.axiosInstance, {
|
|
249
406
|
retries: this.retries,
|
|
250
|
-
retryDelay: (retryCount) =>
|
|
251
|
-
const baseDelay = Math.pow(2, retryCount) * 1000;
|
|
252
|
-
const jitter = Math.random() * 0.3 * baseDelay;
|
|
253
|
-
return baseDelay + jitter;
|
|
254
|
-
},
|
|
407
|
+
retryDelay: (retryCount, error) => getRetryDelayMs(retryCount, error),
|
|
255
408
|
retryCondition: (error) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
409
|
+
if (axiosRetry.isNetworkError(error)) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const status = error.response?.status;
|
|
414
|
+
const retryPolicy = classifyLogResponseStatus(status);
|
|
415
|
+
return retryPolicy === "retryable" || retryPolicy === "retry_after";
|
|
261
416
|
},
|
|
262
417
|
shouldResetTimeout: true,
|
|
263
418
|
});
|
|
@@ -402,6 +557,13 @@ export class Client {
|
|
|
402
557
|
console.error("Failed to send logs:", error);
|
|
403
558
|
}
|
|
404
559
|
|
|
560
|
+
if (isNonRetryableLogError(error)) {
|
|
561
|
+
if (this.logLevel >= LogLevel.Error) {
|
|
562
|
+
console.error("Dropping logs after non-retryable response from API");
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
405
567
|
await this.queueLock.acquire("logQueue", async () => {
|
|
406
568
|
this.logQueue = [...logsToSend, ...this.logQueue];
|
|
407
569
|
});
|
|
@@ -410,7 +572,11 @@ export class Client {
|
|
|
410
572
|
}
|
|
411
573
|
|
|
412
574
|
private async sendLogs(logs: Log[]): Promise<void> {
|
|
413
|
-
|
|
575
|
+
try {
|
|
576
|
+
await this.axiosInstance.post(this.apiEndpoint, logs);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
throw normalizeSendLogsError(error, this.logLevel);
|
|
579
|
+
}
|
|
414
580
|
}
|
|
415
581
|
|
|
416
582
|
public async flush(): Promise<void> {
|
|
@@ -435,6 +601,11 @@ export class Client {
|
|
|
435
601
|
return response.data;
|
|
436
602
|
}
|
|
437
603
|
|
|
604
|
+
public async createEvent(payload: CreateEventPayload): Promise<Event> {
|
|
605
|
+
const response = await this.axiosInstance.post<Event>(`${this.servicerEndpoint}/events`, payload);
|
|
606
|
+
return response.data;
|
|
607
|
+
}
|
|
608
|
+
|
|
438
609
|
public async getEvent(eventID: string): Promise<Event> {
|
|
439
610
|
const response = await this.axiosInstance.get<Event>(`${this.servicerEndpoint}/events/${eventID}`);
|
|
440
611
|
return response.data;
|
|
@@ -456,11 +627,21 @@ export class Client {
|
|
|
456
627
|
return response.data;
|
|
457
628
|
}
|
|
458
629
|
|
|
459
|
-
public async
|
|
630
|
+
public async getPolicy(policyID: string): Promise<Policy> {
|
|
631
|
+
const response = await this.axiosInstance.get<Policy>(`${this.servicerEndpoint}/policies/${policyID}`);
|
|
632
|
+
return response.data;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
public async createPolicy(payload: CreatePolicyPayload): Promise<Policy> {
|
|
460
636
|
const response = await this.axiosInstance.post<Policy>(`${this.servicerEndpoint}/policies`, payload);
|
|
461
637
|
return response.data;
|
|
462
638
|
}
|
|
463
639
|
|
|
640
|
+
public async updatePolicy(policyID: string, payload: UpdatePolicyPayload): Promise<Policy> {
|
|
641
|
+
const response = await this.axiosInstance.put<Policy>(`${this.servicerEndpoint}/policies/${policyID}`, payload);
|
|
642
|
+
return response.data;
|
|
643
|
+
}
|
|
644
|
+
|
|
464
645
|
public async listAlerts(status?: string): Promise<Alert[]> {
|
|
465
646
|
const endpoint = status
|
|
466
647
|
? `${this.servicerEndpoint}/alerts?status=${encodeURIComponent(status)}`
|
|
@@ -469,6 +650,11 @@ export class Client {
|
|
|
469
650
|
return response.data;
|
|
470
651
|
}
|
|
471
652
|
|
|
653
|
+
public async getAlert(alertID: string): Promise<Alert> {
|
|
654
|
+
const response = await this.axiosInstance.get<Alert>(`${this.servicerEndpoint}/alerts/${alertID}`);
|
|
655
|
+
return response.data;
|
|
656
|
+
}
|
|
657
|
+
|
|
472
658
|
public async getAlertDeliveries(alertID: string, options: { limit?: number; offset?: number; kind?: DeliveryKind } = {}): Promise<AlertDelivery[]> {
|
|
473
659
|
const params = new URLSearchParams();
|
|
474
660
|
if (options.kind) {
|
|
@@ -495,8 +681,29 @@ export class Client {
|
|
|
495
681
|
return response.data;
|
|
496
682
|
}
|
|
497
683
|
|
|
498
|
-
public async
|
|
499
|
-
await this.axiosInstance.
|
|
684
|
+
public async listDestinations(): Promise<Destination[]> {
|
|
685
|
+
const response = await this.axiosInstance.get<Destination[]>(`${this.servicerEndpoint}/destinations`);
|
|
686
|
+
return response.data;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
public async createDestination(payload: CreateDestinationPayload): Promise<Destination> {
|
|
690
|
+
const response = await this.axiosInstance.post<Destination>(`${this.servicerEndpoint}/destinations`, payload);
|
|
691
|
+
return response.data;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
public async getDestination(destinationID: string): Promise<Destination> {
|
|
695
|
+
const response = await this.axiosInstance.get<Destination>(`${this.servicerEndpoint}/destinations/${destinationID}`);
|
|
696
|
+
return response.data;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
public async updateDestination(destinationID: string, payload: UpdateDestinationPayload): Promise<Destination> {
|
|
700
|
+
const response = await this.axiosInstance.put<Destination>(`${this.servicerEndpoint}/destinations/${destinationID}`, payload);
|
|
701
|
+
return response.data;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
public async testDestination(destinationID: string): Promise<DestinationTestResult> {
|
|
705
|
+
const response = await this.axiosInstance.post<DestinationTestResult>(`${this.servicerEndpoint}/destinations/${destinationID}/test`);
|
|
706
|
+
return response.data;
|
|
500
707
|
}
|
|
501
708
|
|
|
502
709
|
public async getEventLogs(eventID: string, options: PaginationOptions = {}): Promise<EventLogPoint[]> {
|
|
@@ -515,6 +722,15 @@ export class Client {
|
|
|
515
722
|
return response.data;
|
|
516
723
|
}
|
|
517
724
|
|
|
725
|
+
public async getEventAnalytics(eventID: string, query: AnalyticsWindowQuery): Promise<AnalyticsWindowResponse> {
|
|
726
|
+
const params = new URLSearchParams();
|
|
727
|
+
params.set("view", query.view);
|
|
728
|
+
params.set("period", query.period);
|
|
729
|
+
params.set("previous", query.previous);
|
|
730
|
+
const response = await this.axiosInstance.get<AnalyticsWindowResponse>(`${this.servicerEndpoint}/events/${eventID}/analytics?${params.toString()}`);
|
|
731
|
+
return response.data;
|
|
732
|
+
}
|
|
733
|
+
|
|
518
734
|
public async resolveAlert(alertID: string): Promise<Alert> {
|
|
519
735
|
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/resolve`);
|
|
520
736
|
return response.data;
|