@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.
@@ -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("testWebhook posts to servicer endpoint", async () => {
615
+ test("testDestination posts to servicer endpoint", async () => {
384
616
  const mock = new MockAdapter(axios);
385
- mock.onPost("https://api.chirpier.co/v1.0/webhooks/whk_123/test").reply(200);
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: "chp_client_webhook_key" });
623
+ const client: Client = createClient({ key: "chp_client_destination_key" });
388
624
  try {
389
- await client.testWebhook("whk_123");
390
- expect(mock.history.post[0].url).toBe("https://api.chirpier.co/v1.0/webhooks/whk_123/test");
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 webhook_id?: string;
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
- return (
257
- axiosRetry.isNetworkError(error) ||
258
- axiosRetry.isRetryableError(error) ||
259
- (error.response && error.response.status) === 429
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
- await this.axiosInstance.post(this.apiEndpoint, logs);
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 createPolicy(payload: Omit<Policy, "policy_id">): Promise<Policy> {
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 testWebhook(webhookID: string): Promise<void> {
499
- await this.axiosInstance.post(`${this.servicerEndpoint}/webhooks/${webhookID}/test`);
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;