@chirpier/chirpier-js 0.2.0 → 0.3.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/src/index.ts CHANGED
@@ -6,11 +6,11 @@ import {
6
6
  DEFAULT_TIMEOUT,
7
7
  DEFAULT_BATCH_SIZE,
8
8
  DEFAULT_FLUSH_DELAY,
9
- MAX_QUEUE_SIZE,
10
9
  DEFAULT_API_ENDPOINT,
11
10
  DEFAULT_SERVICER_ENDPOINT,
12
11
  } from "./constants";
13
12
  import AsyncLock from "async-lock";
13
+ import { v7 as uuidv7 } from "uuid";
14
14
 
15
15
  // Define logging levels as const enum for better tree-shaking
16
16
  export const enum LogLevel {
@@ -38,29 +38,36 @@ export interface Config {
38
38
  export type Options = Config;
39
39
 
40
40
  export interface Log {
41
- agent_id?: string;
41
+ log_id?: string;
42
+ agent?: string;
42
43
  event: string;
43
44
  value: number;
44
45
  meta?: unknown;
45
46
  occurred_at?: string | Date;
46
47
  }
47
48
 
48
- export interface EventDefinition {
49
+ export interface Event {
49
50
  readonly event_id: string;
50
- readonly agent_id?: string;
51
+ readonly agent?: string;
51
52
  readonly event: string;
52
53
  readonly title?: string;
53
54
  readonly public: boolean;
54
55
  readonly description?: string;
55
56
  readonly unit?: string;
56
- readonly semantic_class: string;
57
- readonly default_aggregate: string;
58
- readonly enabled: boolean;
59
- readonly origin: string;
60
- readonly archived_at?: string;
57
+ readonly timezone: string;
61
58
  readonly created_at?: string;
62
59
  }
63
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
+
64
71
  export interface Policy {
65
72
  readonly policy_id: string;
66
73
  readonly event_id: string;
@@ -75,11 +82,15 @@ export interface Policy {
75
82
  readonly enabled: boolean;
76
83
  }
77
84
 
85
+ export type CreatePolicyPayload = Omit<Policy, "policy_id">;
86
+
87
+ export type UpdatePolicyPayload = Partial<Omit<Policy, "policy_id">>;
88
+
78
89
  export interface Alert {
79
90
  readonly alert_id: string;
80
91
  readonly policy_id: string;
81
92
  readonly event_id: string;
82
- readonly agent_id?: string;
93
+ readonly agent?: string;
83
94
  readonly event: string;
84
95
  readonly title: string;
85
96
  readonly period: string;
@@ -100,7 +111,7 @@ export interface Alert {
100
111
  export interface AlertDelivery {
101
112
  readonly attempt_id: string;
102
113
  readonly alert_id: string;
103
- readonly webhook_id?: string;
114
+ readonly destination_id?: string;
104
115
  readonly channel: string;
105
116
  readonly target: string;
106
117
  readonly status: string;
@@ -109,9 +120,23 @@ export interface AlertDelivery {
109
120
  readonly created_at: string;
110
121
  }
111
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
+
112
137
  export interface EventLogPoint {
113
138
  readonly event_id: string;
114
- readonly agent_id?: string;
139
+ readonly agent?: string;
115
140
  readonly event: string;
116
141
  readonly period: string;
117
142
  readonly occurred_at: string;
@@ -122,6 +147,43 @@ export interface EventLogPoint {
122
147
  readonly max: number;
123
148
  }
124
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
+
125
187
  export interface PaginationOptions {
126
188
  period?: "minute" | "hour" | "day";
127
189
  limit?: number;
@@ -145,7 +207,6 @@ export class ChirpierError extends Error {
145
207
  interface QueuedLog {
146
208
  readonly log: Log;
147
209
  readonly timestamp: number;
148
- retryCount: number;
149
210
  }
150
211
 
151
212
  export class Client {
@@ -158,7 +219,6 @@ export class Client {
158
219
  private logQueue: QueuedLog[] = [];
159
220
  private readonly batchSize: number;
160
221
  private readonly flushDelay: number;
161
- private readonly maxQueueSize: number;
162
222
  private flushTimeoutId: NodeJS.Timeout | null = null;
163
223
  private readonly queueLock: AsyncLock;
164
224
  private readonly flushLock: AsyncLock;
@@ -174,7 +234,7 @@ export class Client {
174
234
  timeout = DEFAULT_TIMEOUT,
175
235
  batchSize = DEFAULT_BATCH_SIZE,
176
236
  flushDelay = DEFAULT_FLUSH_DELAY,
177
- maxQueueSize = MAX_QUEUE_SIZE,
237
+ maxQueueSize,
178
238
  } = options;
179
239
 
180
240
  const key = resolveAPIKey(providedKey);
@@ -226,10 +286,6 @@ export class Client {
226
286
  if (flushDelay < 0) {
227
287
  throw new ChirpierError("Flush delay must be non-negative", "INVALID_FLUSH_DELAY");
228
288
  }
229
- if (maxQueueSize <= 0 || !Number.isInteger(maxQueueSize)) {
230
- throw new ChirpierError("Max queue size must be a positive integer", "INVALID_QUEUE_SIZE");
231
- }
232
-
233
289
  this.apiEndpoint = apiEndpoint ?? DEFAULT_API_ENDPOINT;
234
290
  this.servicerEndpoint = servicerEndpoint ?? DEFAULT_SERVICER_ENDPOINT;
235
291
  this.apiKey = key;
@@ -237,11 +293,11 @@ export class Client {
237
293
  this.timeout = timeout;
238
294
  this.batchSize = batchSize;
239
295
  this.flushDelay = flushDelay;
240
- this.maxQueueSize = maxQueueSize;
241
296
  this.logLevel = logLevel;
242
297
 
243
- this.queueLock = new AsyncLock({ maxPending: this.maxQueueSize });
244
- this.flushLock = new AsyncLock({ maxPending: this.maxQueueSize });
298
+ void maxQueueSize;
299
+ this.queueLock = new AsyncLock();
300
+ this.flushLock = new AsyncLock();
245
301
 
246
302
  this.axiosInstance = axios.create({
247
303
  headers: { Authorization: `Bearer ${this.apiKey}` },
@@ -280,11 +336,22 @@ export class Client {
280
336
  return false;
281
337
  }
282
338
 
339
+ if (log.log_id !== undefined) {
340
+ if (typeof log.log_id !== "string") {
341
+ return false;
342
+ }
343
+
344
+ const trimmedLogID = log.log_id.trim();
345
+ if (trimmedLogID.length > 0 && !isUUID(trimmedLogID)) {
346
+ return false;
347
+ }
348
+ }
349
+
283
350
  if (typeof log.value !== "number" || !Number.isFinite(log.value)) {
284
351
  return false;
285
352
  }
286
353
 
287
- if (log.agent_id !== undefined && typeof log.agent_id !== "string") {
354
+ if (log.agent !== undefined && typeof log.agent !== "string") {
288
355
  return false;
289
356
  }
290
357
 
@@ -319,14 +386,15 @@ export class Client {
319
386
 
320
387
  private normalizeLog(log: Log): Log {
321
388
  const normalizedLog: Log = {
389
+ log_id: resolveLogID(log.log_id),
322
390
  event: log.event.trim(),
323
391
  value: log.value,
324
392
  };
325
393
 
326
- if (typeof log.agent_id === "string") {
327
- const trimmedAgentID = log.agent_id.trim();
328
- if (trimmedAgentID.length > 0) {
329
- normalizedLog.agent_id = trimmedAgentID;
394
+ if (typeof log.agent === "string") {
395
+ const trimmedAgent = log.agent.trim();
396
+ if (trimmedAgent.length > 0) {
397
+ normalizedLog.agent = trimmedAgent;
330
398
  }
331
399
  }
332
400
 
@@ -346,31 +414,17 @@ export class Client {
346
414
  public async log(log: Log): Promise<void> {
347
415
  if (!this.isValidLog(log)) {
348
416
  throw new ChirpierError(
349
- "Invalid log format: event must not be empty, value must be a finite number, agent_id must be a string when provided, meta must be JSON-encodable, and occurred_at must be within the last 30 days and no more than 1 day in the future",
417
+ "Invalid log format: log_id must be a UUID when provided, event must not be empty, value must be a finite number, agent must be a string when provided, meta must be JSON-encodable, and occurred_at must be within the last 30 days and no more than 1 day in the future",
350
418
  "INVALID_LOG"
351
419
  );
352
420
  }
353
421
 
354
422
  const normalizedLog = this.normalizeLog(log);
355
423
 
356
- let queueFull = false;
357
-
358
424
  await this.queueLock.acquire("queue", async () => {
359
- if (this.logQueue.length >= this.maxQueueSize) {
360
- queueFull = true;
361
- return;
362
- }
363
-
364
- this.logQueue.push({ log: normalizedLog, timestamp: Date.now(), retryCount: 0 });
425
+ this.logQueue.push({ log: normalizedLog, timestamp: Date.now() });
365
426
  });
366
427
 
367
- if (queueFull) {
368
- throw new ChirpierError(
369
- `Log queue is full (max size: ${this.maxQueueSize})`,
370
- "QUEUE_FULL"
371
- );
372
- }
373
-
374
428
  if (this.logQueue.length >= this.batchSize) {
375
429
  await this.flushQueue();
376
430
  } else if (!this.flushTimeoutId) {
@@ -412,24 +466,8 @@ export class Client {
412
466
  console.error("Failed to send logs:", error);
413
467
  }
414
468
 
415
- const retryableLogs: QueuedLog[] = [];
416
- for (const queuedLog of logsToSend) {
417
- if (queuedLog.retryCount >= this.retries) {
418
- if (this.logLevel >= LogLevel.Error) {
419
- console.error(
420
- `Dropping log after ${this.retries} retries:`,
421
- queuedLog.log
422
- );
423
- }
424
- continue;
425
- }
426
-
427
- queuedLog.retryCount++;
428
- retryableLogs.push(queuedLog);
429
- }
430
-
431
469
  await this.queueLock.acquire("logQueue", async () => {
432
- this.logQueue = [...retryableLogs, ...this.logQueue];
470
+ this.logQueue = [...logsToSend, ...this.logQueue];
433
471
  });
434
472
  }
435
473
  });
@@ -456,21 +494,26 @@ export class Client {
456
494
  await this.shutdown();
457
495
  }
458
496
 
459
- public async listEvents(): Promise<EventDefinition[]> {
460
- const response = await this.axiosInstance.get<EventDefinition[]>(`${this.servicerEndpoint}/events`);
497
+ public async listEvents(): Promise<Event[]> {
498
+ const response = await this.axiosInstance.get<Event[]>(`${this.servicerEndpoint}/events`);
499
+ return response.data;
500
+ }
501
+
502
+ public async createEvent(payload: CreateEventPayload): Promise<Event> {
503
+ const response = await this.axiosInstance.post<Event>(`${this.servicerEndpoint}/events`, payload);
461
504
  return response.data;
462
505
  }
463
506
 
464
- public async getEvent(eventID: string): Promise<EventDefinition> {
465
- const response = await this.axiosInstance.get<EventDefinition>(`${this.servicerEndpoint}/events/${eventID}`);
507
+ public async getEvent(eventID: string): Promise<Event> {
508
+ const response = await this.axiosInstance.get<Event>(`${this.servicerEndpoint}/events/${eventID}`);
466
509
  return response.data;
467
510
  }
468
511
 
469
512
  public async updateEvent(
470
513
  eventID: string,
471
- payload: Partial<Omit<EventDefinition, "event_id" | "created_at">>
472
- ): Promise<EventDefinition> {
473
- const response = await this.axiosInstance.put<EventDefinition>(
514
+ payload: Partial<Omit<Event, "event_id" | "created_at">>
515
+ ): Promise<Event> {
516
+ const response = await this.axiosInstance.put<Event>(
474
517
  `${this.servicerEndpoint}/events/${eventID}`,
475
518
  payload
476
519
  );
@@ -482,11 +525,21 @@ export class Client {
482
525
  return response.data;
483
526
  }
484
527
 
485
- public async createPolicy(payload: Omit<Policy, "policy_id">): Promise<Policy> {
528
+ public async getPolicy(policyID: string): Promise<Policy> {
529
+ const response = await this.axiosInstance.get<Policy>(`${this.servicerEndpoint}/policies/${policyID}`);
530
+ return response.data;
531
+ }
532
+
533
+ public async createPolicy(payload: CreatePolicyPayload): Promise<Policy> {
486
534
  const response = await this.axiosInstance.post<Policy>(`${this.servicerEndpoint}/policies`, payload);
487
535
  return response.data;
488
536
  }
489
537
 
538
+ public async updatePolicy(policyID: string, payload: UpdatePolicyPayload): Promise<Policy> {
539
+ const response = await this.axiosInstance.put<Policy>(`${this.servicerEndpoint}/policies/${policyID}`, payload);
540
+ return response.data;
541
+ }
542
+
490
543
  public async listAlerts(status?: string): Promise<Alert[]> {
491
544
  const endpoint = status
492
545
  ? `${this.servicerEndpoint}/alerts?status=${encodeURIComponent(status)}`
@@ -495,6 +548,11 @@ export class Client {
495
548
  return response.data;
496
549
  }
497
550
 
551
+ public async getAlert(alertID: string): Promise<Alert> {
552
+ const response = await this.axiosInstance.get<Alert>(`${this.servicerEndpoint}/alerts/${alertID}`);
553
+ return response.data;
554
+ }
555
+
498
556
  public async getAlertDeliveries(alertID: string, options: { limit?: number; offset?: number; kind?: DeliveryKind } = {}): Promise<AlertDelivery[]> {
499
557
  const params = new URLSearchParams();
500
558
  if (options.kind) {
@@ -521,8 +579,29 @@ export class Client {
521
579
  return response.data;
522
580
  }
523
581
 
524
- public async testWebhook(webhookID: string): Promise<void> {
525
- await this.axiosInstance.post(`${this.servicerEndpoint}/webhooks/${webhookID}/test`);
582
+ public async listDestinations(): Promise<Destination[]> {
583
+ const response = await this.axiosInstance.get<Destination[]>(`${this.servicerEndpoint}/destinations`);
584
+ return response.data;
585
+ }
586
+
587
+ public async createDestination(payload: CreateDestinationPayload): Promise<Destination> {
588
+ const response = await this.axiosInstance.post<Destination>(`${this.servicerEndpoint}/destinations`, payload);
589
+ return response.data;
590
+ }
591
+
592
+ public async getDestination(destinationID: string): Promise<Destination> {
593
+ const response = await this.axiosInstance.get<Destination>(`${this.servicerEndpoint}/destinations/${destinationID}`);
594
+ return response.data;
595
+ }
596
+
597
+ public async updateDestination(destinationID: string, payload: UpdateDestinationPayload): Promise<Destination> {
598
+ const response = await this.axiosInstance.put<Destination>(`${this.servicerEndpoint}/destinations/${destinationID}`, payload);
599
+ return response.data;
600
+ }
601
+
602
+ public async testDestination(destinationID: string): Promise<DestinationTestResult> {
603
+ const response = await this.axiosInstance.post<DestinationTestResult>(`${this.servicerEndpoint}/destinations/${destinationID}/test`);
604
+ return response.data;
526
605
  }
527
606
 
528
607
  public async getEventLogs(eventID: string, options: PaginationOptions = {}): Promise<EventLogPoint[]> {
@@ -541,6 +620,15 @@ export class Client {
541
620
  return response.data;
542
621
  }
543
622
 
623
+ public async getEventAnalytics(eventID: string, query: AnalyticsWindowQuery): Promise<AnalyticsWindowResponse> {
624
+ const params = new URLSearchParams();
625
+ params.set("view", query.view);
626
+ params.set("period", query.period);
627
+ params.set("previous", query.previous);
628
+ const response = await this.axiosInstance.get<AnalyticsWindowResponse>(`${this.servicerEndpoint}/events/${eventID}/analytics?${params.toString()}`);
629
+ return response.data;
630
+ }
631
+
544
632
  public async resolveAlert(alertID: string): Promise<Alert> {
545
633
  const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/resolve`);
546
634
  return response.data;
@@ -562,6 +650,21 @@ function isValidAPIKey(token: string): boolean {
562
650
  return token.startsWith("chp_") && token.length > "chp_".length;
563
651
  }
564
652
 
653
+ function isUUID(value: string): boolean {
654
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
655
+ }
656
+
657
+ function resolveLogID(logID?: string): string {
658
+ if (typeof logID === "string") {
659
+ const trimmedLogID = logID.trim();
660
+ if (trimmedLogID.length > 0) {
661
+ return trimmedLogID;
662
+ }
663
+ }
664
+
665
+ return uuidv7();
666
+ }
667
+
565
668
  function loadDotEnvKey(): string | undefined {
566
669
  if (!isNodeEnvironment()) {
567
670
  return undefined;