@chirpier/chirpier-js 0.1.6 → 0.2.1
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 +184 -108
- package/dist/__tests__/chirpier.test.js +435 -100
- package/dist/constants.d.ts +7 -6
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +5 -4
- package/dist/index.d.ts +129 -56
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +483 -203
- package/package.json +5 -5
- package/src/__tests__/chirpier.test.ts +296 -92
- package/src/constants.ts +7 -6
- package/src/index.ts +492 -206
package/src/index.ts
CHANGED
|
@@ -1,126 +1,258 @@
|
|
|
1
|
-
// Import necessary dependencies
|
|
2
1
|
import axios, { AxiosInstance } from "axios";
|
|
3
2
|
import axiosRetry from "axios-retry";
|
|
4
|
-
import
|
|
3
|
+
import dotenv from "dotenv";
|
|
5
4
|
import {
|
|
6
5
|
DEFAULT_RETRIES,
|
|
7
6
|
DEFAULT_TIMEOUT,
|
|
8
7
|
DEFAULT_BATCH_SIZE,
|
|
9
8
|
DEFAULT_FLUSH_DELAY,
|
|
10
|
-
|
|
9
|
+
DEFAULT_API_ENDPOINT,
|
|
10
|
+
DEFAULT_SERVICER_ENDPOINT,
|
|
11
11
|
} from "./constants";
|
|
12
12
|
import AsyncLock from "async-lock";
|
|
13
|
+
import { v7 as uuidv7 } from "uuid";
|
|
13
14
|
|
|
14
|
-
// Define logging levels
|
|
15
|
-
export enum LogLevel {
|
|
15
|
+
// Define logging levels as const enum for better tree-shaking
|
|
16
|
+
export const enum LogLevel {
|
|
16
17
|
None = 0,
|
|
17
18
|
Error = 1,
|
|
18
19
|
Info = 2,
|
|
19
20
|
Debug = 3,
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export interface Config {
|
|
24
|
+
key?: string;
|
|
25
|
+
apiEndpoint?: string;
|
|
26
|
+
servicerEndpoint?: string;
|
|
26
27
|
logLevel?: LogLevel;
|
|
28
|
+
retries?: number;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
batchSize?: number;
|
|
31
|
+
flushDelay?: number;
|
|
32
|
+
/** @deprecated Queues are unbounded in memory. */
|
|
33
|
+
maxQueueSize?: number;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Use Config.
|
|
38
|
+
*/
|
|
39
|
+
export type Options = Config;
|
|
40
|
+
|
|
41
|
+
export interface Log {
|
|
42
|
+
log_id?: string;
|
|
43
|
+
agent?: string;
|
|
44
|
+
event: string;
|
|
33
45
|
value: number;
|
|
46
|
+
meta?: unknown;
|
|
47
|
+
occurred_at?: string | Date;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Event {
|
|
51
|
+
readonly event_id: string;
|
|
52
|
+
readonly agent?: string;
|
|
53
|
+
readonly event: string;
|
|
54
|
+
readonly title?: string;
|
|
55
|
+
readonly public: boolean;
|
|
56
|
+
readonly description?: string;
|
|
57
|
+
readonly unit?: string;
|
|
58
|
+
readonly timezone: string;
|
|
59
|
+
readonly created_at?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface Policy {
|
|
63
|
+
readonly policy_id: string;
|
|
64
|
+
readonly event_id: string;
|
|
65
|
+
readonly title: string;
|
|
66
|
+
readonly description?: string;
|
|
67
|
+
readonly channel: string;
|
|
68
|
+
readonly period: string;
|
|
69
|
+
readonly aggregate: string;
|
|
70
|
+
readonly condition: string;
|
|
71
|
+
readonly threshold: number;
|
|
72
|
+
readonly severity: string;
|
|
73
|
+
readonly enabled: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Alert {
|
|
77
|
+
readonly alert_id: string;
|
|
78
|
+
readonly policy_id: string;
|
|
79
|
+
readonly event_id: string;
|
|
80
|
+
readonly agent?: string;
|
|
81
|
+
readonly event: string;
|
|
82
|
+
readonly title: string;
|
|
83
|
+
readonly period: string;
|
|
84
|
+
readonly aggregate: string;
|
|
85
|
+
readonly condition: string;
|
|
86
|
+
readonly threshold: number;
|
|
87
|
+
readonly severity: string;
|
|
88
|
+
readonly status: string;
|
|
89
|
+
readonly value: number;
|
|
90
|
+
readonly count: number;
|
|
91
|
+
readonly min: number;
|
|
92
|
+
readonly max: number;
|
|
93
|
+
readonly triggered_at?: string;
|
|
94
|
+
readonly acknowledged_at?: string;
|
|
95
|
+
readonly resolved_at?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface AlertDelivery {
|
|
99
|
+
readonly attempt_id: string;
|
|
100
|
+
readonly alert_id: string;
|
|
101
|
+
readonly webhook_id?: string;
|
|
102
|
+
readonly channel: string;
|
|
103
|
+
readonly target: string;
|
|
104
|
+
readonly status: string;
|
|
105
|
+
readonly response_status?: number;
|
|
106
|
+
readonly error_message?: string;
|
|
107
|
+
readonly created_at: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface EventLogPoint {
|
|
111
|
+
readonly event_id: string;
|
|
112
|
+
readonly agent?: string;
|
|
113
|
+
readonly event: string;
|
|
114
|
+
readonly period: string;
|
|
115
|
+
readonly occurred_at: string;
|
|
116
|
+
readonly count: number;
|
|
117
|
+
readonly value: number;
|
|
118
|
+
readonly squares: number;
|
|
119
|
+
readonly min: number;
|
|
120
|
+
readonly max: number;
|
|
34
121
|
}
|
|
35
122
|
|
|
123
|
+
export interface PaginationOptions {
|
|
124
|
+
period?: "minute" | "hour" | "day";
|
|
125
|
+
limit?: number;
|
|
126
|
+
offset?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type DeliveryKind = "alert" | "test" | "all";
|
|
130
|
+
|
|
36
131
|
// Custom error class for Chirpier-specific errors
|
|
37
132
|
export class ChirpierError extends Error {
|
|
38
|
-
constructor(
|
|
133
|
+
constructor(
|
|
134
|
+
message: string,
|
|
135
|
+
public readonly code?: string
|
|
136
|
+
) {
|
|
39
137
|
super(message);
|
|
40
138
|
this.name = "ChirpierError";
|
|
41
139
|
Object.setPrototypeOf(this, ChirpierError.prototype);
|
|
42
140
|
}
|
|
43
141
|
}
|
|
44
142
|
|
|
45
|
-
interface
|
|
46
|
-
|
|
47
|
-
timestamp: number;
|
|
48
|
-
retryCount: number;
|
|
143
|
+
interface QueuedLog {
|
|
144
|
+
readonly log: Log;
|
|
145
|
+
readonly timestamp: number;
|
|
49
146
|
}
|
|
50
147
|
|
|
51
|
-
|
|
52
|
-
* Main Chirpier class for monitoring events.
|
|
53
|
-
*/
|
|
54
|
-
export class Chirpier {
|
|
55
|
-
private static instance: Chirpier | null = null;
|
|
148
|
+
export class Client {
|
|
56
149
|
private readonly apiKey: string;
|
|
57
150
|
private readonly apiEndpoint: string;
|
|
151
|
+
private readonly servicerEndpoint: string;
|
|
58
152
|
private readonly retries: number;
|
|
59
153
|
private readonly timeout: number;
|
|
60
154
|
private readonly axiosInstance: AxiosInstance;
|
|
61
|
-
private
|
|
155
|
+
private logQueue: QueuedLog[] = [];
|
|
62
156
|
private readonly batchSize: number;
|
|
63
157
|
private readonly flushDelay: number;
|
|
64
158
|
private flushTimeoutId: NodeJS.Timeout | null = null;
|
|
65
|
-
private readonly queueLock
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
private readonly flushLock = new AsyncLock({
|
|
69
|
-
maxPending: MAX_QUEUE_SIZE,
|
|
70
|
-
});
|
|
159
|
+
private readonly queueLock: AsyncLock;
|
|
160
|
+
private readonly flushLock: AsyncLock;
|
|
71
161
|
private readonly logLevel: LogLevel;
|
|
72
162
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
163
|
+
constructor(options: Config = {}) {
|
|
164
|
+
const {
|
|
165
|
+
key: providedKey,
|
|
166
|
+
apiEndpoint = DEFAULT_API_ENDPOINT,
|
|
167
|
+
servicerEndpoint = DEFAULT_SERVICER_ENDPOINT,
|
|
168
|
+
logLevel = LogLevel.None,
|
|
169
|
+
retries = DEFAULT_RETRIES,
|
|
170
|
+
timeout = DEFAULT_TIMEOUT,
|
|
171
|
+
batchSize = DEFAULT_BATCH_SIZE,
|
|
172
|
+
flushDelay = DEFAULT_FLUSH_DELAY,
|
|
173
|
+
maxQueueSize,
|
|
174
|
+
} = options;
|
|
175
|
+
|
|
176
|
+
const key = resolveAPIKey(providedKey);
|
|
177
|
+
|
|
178
|
+
if (!key) {
|
|
179
|
+
throw new ChirpierError("API key is required", "INVALID_KEY");
|
|
180
|
+
}
|
|
79
181
|
|
|
80
|
-
if (!key
|
|
81
|
-
throw new ChirpierError("API key
|
|
182
|
+
if (!isValidAPIKey(key)) {
|
|
183
|
+
throw new ChirpierError("Invalid API key: must start with 'chp_'", "INVALID_KEY");
|
|
82
184
|
}
|
|
83
185
|
|
|
84
|
-
if (
|
|
85
|
-
typeof
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
186
|
+
if (apiEndpoint !== undefined) {
|
|
187
|
+
if (typeof apiEndpoint !== "string" || apiEndpoint.trim().length === 0) {
|
|
188
|
+
throw new ChirpierError(
|
|
189
|
+
"apiEndpoint must be a non-empty string",
|
|
190
|
+
"INVALID_API_ENDPOINT"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let parsedURL: URL;
|
|
195
|
+
try {
|
|
196
|
+
parsedURL = new URL(apiEndpoint);
|
|
197
|
+
} catch {
|
|
198
|
+
throw new ChirpierError(
|
|
199
|
+
"apiEndpoint must be a valid absolute URL",
|
|
200
|
+
"INVALID_API_ENDPOINT"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (parsedURL.protocol !== "https:" && parsedURL.protocol !== "http:") {
|
|
205
|
+
throw new ChirpierError(
|
|
206
|
+
"apiEndpoint must use http or https",
|
|
207
|
+
"INVALID_API_ENDPOINT"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
91
210
|
}
|
|
92
211
|
|
|
93
|
-
|
|
212
|
+
// Validate numeric options
|
|
213
|
+
if (retries < 0 || !Number.isInteger(retries)) {
|
|
214
|
+
throw new ChirpierError("Retries must be a non-negative integer", "INVALID_RETRIES");
|
|
215
|
+
}
|
|
216
|
+
if (timeout <= 0) {
|
|
217
|
+
throw new ChirpierError("Timeout must be positive", "INVALID_TIMEOUT");
|
|
218
|
+
}
|
|
219
|
+
if (batchSize <= 0 || !Number.isInteger(batchSize)) {
|
|
220
|
+
throw new ChirpierError("Batch size must be a positive integer", "INVALID_BATCH_SIZE");
|
|
221
|
+
}
|
|
222
|
+
if (flushDelay < 0) {
|
|
223
|
+
throw new ChirpierError("Flush delay must be non-negative", "INVALID_FLUSH_DELAY");
|
|
224
|
+
}
|
|
225
|
+
this.apiEndpoint = apiEndpoint ?? DEFAULT_API_ENDPOINT;
|
|
226
|
+
this.servicerEndpoint = servicerEndpoint ?? DEFAULT_SERVICER_ENDPOINT;
|
|
94
227
|
this.apiKey = key;
|
|
95
|
-
this.retries =
|
|
96
|
-
this.timeout =
|
|
97
|
-
this.batchSize =
|
|
98
|
-
this.flushDelay =
|
|
228
|
+
this.retries = retries;
|
|
229
|
+
this.timeout = timeout;
|
|
230
|
+
this.batchSize = batchSize;
|
|
231
|
+
this.flushDelay = flushDelay;
|
|
99
232
|
this.logLevel = logLevel;
|
|
100
233
|
|
|
101
|
-
|
|
234
|
+
void maxQueueSize;
|
|
235
|
+
this.queueLock = new AsyncLock();
|
|
236
|
+
this.flushLock = new AsyncLock();
|
|
237
|
+
|
|
102
238
|
this.axiosInstance = axios.create({
|
|
103
239
|
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
104
240
|
timeout: this.timeout,
|
|
105
241
|
});
|
|
106
242
|
|
|
107
|
-
// Add the interceptor here
|
|
108
243
|
this.axiosInstance.interceptors.response.use(
|
|
109
244
|
(response) => response,
|
|
110
|
-
(error) =>
|
|
111
|
-
// Don't handle the error here; let axios-retry handle it
|
|
112
|
-
return Promise.reject(error);
|
|
113
|
-
}
|
|
245
|
+
(error) => Promise.reject(error)
|
|
114
246
|
);
|
|
115
247
|
|
|
116
|
-
// Apply axios-retry to your Axios instance
|
|
117
248
|
axiosRetry(this.axiosInstance, {
|
|
118
249
|
retries: this.retries,
|
|
119
250
|
retryDelay: (retryCount) => {
|
|
120
|
-
|
|
251
|
+
const baseDelay = Math.pow(2, retryCount) * 1000;
|
|
252
|
+
const jitter = Math.random() * 0.3 * baseDelay;
|
|
253
|
+
return baseDelay + jitter;
|
|
121
254
|
},
|
|
122
255
|
retryCondition: (error) => {
|
|
123
|
-
// Retry on network errors, 5xx errors, and 429 (Too Many Requests)
|
|
124
256
|
return (
|
|
125
257
|
axiosRetry.isNetworkError(error) ||
|
|
126
258
|
axiosRetry.isRetryableError(error) ||
|
|
@@ -131,60 +263,105 @@ export class Chirpier {
|
|
|
131
263
|
});
|
|
132
264
|
}
|
|
133
265
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Chirpier.instance = new Chirpier(options);
|
|
266
|
+
private isValidLog(log: Log): boolean {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const oldestAllowed = now - 30 * 24 * 60 * 60 * 1000;
|
|
269
|
+
const newestAllowed = now + 24 * 60 * 60 * 1000;
|
|
270
|
+
|
|
271
|
+
if (typeof log.event !== "string" || log.event.trim().length === 0) {
|
|
272
|
+
return false;
|
|
142
273
|
}
|
|
143
|
-
return Chirpier.instance;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Validates the event structure.
|
|
148
|
-
* @param event - The event to validate.
|
|
149
|
-
* @returns True if valid, false otherwise.
|
|
150
|
-
*/
|
|
151
|
-
private isValidEvent(event: Event): boolean {
|
|
152
|
-
return (
|
|
153
|
-
typeof event.group_id === "string" &&
|
|
154
|
-
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
|
155
|
-
event.group_id
|
|
156
|
-
) &&
|
|
157
|
-
event.group_id.trim().length > 0 &&
|
|
158
|
-
typeof event.stream_name === "string" &&
|
|
159
|
-
event.stream_name.trim().length > 0 &&
|
|
160
|
-
typeof event.value === "number"
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
274
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
275
|
+
if (log.log_id !== undefined) {
|
|
276
|
+
if (typeof log.log_id !== "string") {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const trimmedLogID = log.log_id.trim();
|
|
281
|
+
if (trimmedLogID.length > 0 && !isUUID(trimmedLogID)) {
|
|
282
|
+
return false;
|
|
172
283
|
}
|
|
173
|
-
return; // Silently drop the event
|
|
174
284
|
}
|
|
175
285
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
286
|
+
if (typeof log.value !== "number" || !Number.isFinite(log.value)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (log.agent !== undefined && typeof log.agent !== "string") {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (log.meta !== undefined) {
|
|
295
|
+
try {
|
|
296
|
+
const serializedMeta = JSON.stringify(log.meta);
|
|
297
|
+
if (serializedMeta === undefined) {
|
|
298
|
+
return false;
|
|
180
299
|
}
|
|
181
|
-
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (log.occurred_at !== undefined) {
|
|
306
|
+
const occurredAtMillis =
|
|
307
|
+
log.occurred_at instanceof Date
|
|
308
|
+
? log.occurred_at.getTime()
|
|
309
|
+
: new Date(log.occurred_at).getTime();
|
|
310
|
+
|
|
311
|
+
if (!Number.isFinite(occurredAtMillis)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (occurredAtMillis < oldestAllowed || occurredAtMillis > newestAllowed) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private normalizeLog(log: Log): Log {
|
|
324
|
+
const normalizedLog: Log = {
|
|
325
|
+
log_id: resolveLogID(log.log_id),
|
|
326
|
+
event: log.event.trim(),
|
|
327
|
+
value: log.value,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (typeof log.agent === "string") {
|
|
331
|
+
const trimmedAgent = log.agent.trim();
|
|
332
|
+
if (trimmedAgent.length > 0) {
|
|
333
|
+
normalizedLog.agent = trimmedAgent;
|
|
182
334
|
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (log.meta !== undefined) {
|
|
338
|
+
normalizedLog.meta = log.meta;
|
|
339
|
+
}
|
|
183
340
|
|
|
184
|
-
|
|
341
|
+
if (log.occurred_at !== undefined) {
|
|
342
|
+
const occurredAtDate =
|
|
343
|
+
log.occurred_at instanceof Date ? log.occurred_at : new Date(log.occurred_at);
|
|
344
|
+
normalizedLog.occurred_at = occurredAtDate.toISOString();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return normalizedLog;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
public async log(log: Log): Promise<void> {
|
|
351
|
+
if (!this.isValidLog(log)) {
|
|
352
|
+
throw new ChirpierError(
|
|
353
|
+
"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",
|
|
354
|
+
"INVALID_LOG"
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const normalizedLog = this.normalizeLog(log);
|
|
359
|
+
|
|
360
|
+
await this.queueLock.acquire("queue", async () => {
|
|
361
|
+
this.logQueue.push({ log: normalizedLog, timestamp: Date.now() });
|
|
185
362
|
});
|
|
186
363
|
|
|
187
|
-
if (this.
|
|
364
|
+
if (this.logQueue.length >= this.batchSize) {
|
|
188
365
|
await this.flushQueue();
|
|
189
366
|
} else if (!this.flushTimeoutId) {
|
|
190
367
|
this.flushTimeoutId = setTimeout(
|
|
@@ -194,142 +371,237 @@ export class Chirpier {
|
|
|
194
371
|
}
|
|
195
372
|
}
|
|
196
373
|
|
|
197
|
-
/**
|
|
198
|
-
* Flushes the event queue by sending all events to the API.
|
|
199
|
-
*/
|
|
200
374
|
private async flushQueue(): Promise<void> {
|
|
201
|
-
// Acquire the flush lock
|
|
202
375
|
await this.flushLock.acquire("flush", async () => {
|
|
203
|
-
let
|
|
376
|
+
let logsToSend: QueuedLog[] = [];
|
|
204
377
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
this.eventQueue = [];
|
|
378
|
+
await this.queueLock.acquire("logQueue", async () => {
|
|
379
|
+
if (this.logQueue.length > 0) {
|
|
380
|
+
logsToSend = [...this.logQueue];
|
|
381
|
+
this.logQueue = [];
|
|
210
382
|
}
|
|
211
383
|
});
|
|
212
384
|
|
|
213
|
-
if (
|
|
385
|
+
if (logsToSend.length === 0) {
|
|
214
386
|
return;
|
|
215
387
|
}
|
|
216
388
|
|
|
217
389
|
try {
|
|
218
|
-
// Clear any pending flush timeout
|
|
219
390
|
if (this.flushTimeoutId) {
|
|
220
391
|
clearTimeout(this.flushTimeoutId);
|
|
221
392
|
this.flushTimeoutId = null;
|
|
222
393
|
}
|
|
223
394
|
|
|
224
|
-
|
|
225
|
-
await this.sendEvents(eventsToSend.map((qe) => qe.event));
|
|
395
|
+
await this.sendLogs(logsToSend.map((queuedLog) => queuedLog.log));
|
|
226
396
|
|
|
227
397
|
if (this.logLevel >= LogLevel.Info) {
|
|
228
|
-
console.info(`Successfully sent ${
|
|
398
|
+
console.info(`Successfully sent ${logsToSend.length} logs`);
|
|
229
399
|
}
|
|
230
400
|
} catch (error) {
|
|
231
|
-
// Log failure
|
|
232
401
|
if (this.logLevel >= LogLevel.Error) {
|
|
233
|
-
console.error("Failed to send
|
|
402
|
+
console.error("Failed to send logs:", error);
|
|
234
403
|
}
|
|
235
404
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
for (const queuedEvent of eventsToSend) {
|
|
239
|
-
if (queuedEvent.retryCount >= this.retries) {
|
|
240
|
-
if (this.logLevel >= LogLevel.Error) {
|
|
241
|
-
console.error(
|
|
242
|
-
`Dropping event after ${this.retries} retries:`,
|
|
243
|
-
queuedEvent.event
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
continue; // Skip adding this event back to the queue
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Increment retry count and add back to the queue
|
|
250
|
-
queuedEvent.retryCount++;
|
|
251
|
-
retryableEvents.push(queuedEvent);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Requeue remaining retryable events
|
|
255
|
-
await this.queueLock.acquire("eventQueue", async () => {
|
|
256
|
-
this.eventQueue = [...retryableEvents, ...this.eventQueue];
|
|
405
|
+
await this.queueLock.acquire("logQueue", async () => {
|
|
406
|
+
this.logQueue = [...logsToSend, ...this.logQueue];
|
|
257
407
|
});
|
|
258
408
|
}
|
|
259
409
|
});
|
|
260
410
|
}
|
|
261
411
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
* @param events - The array of events to send.
|
|
265
|
-
*/
|
|
266
|
-
private async sendEvents(events: Event[]): Promise<void> {
|
|
267
|
-
await this.axiosInstance.post(this.apiEndpoint, events);
|
|
412
|
+
private async sendLogs(logs: Log[]): Promise<void> {
|
|
413
|
+
await this.axiosInstance.post(this.apiEndpoint, logs);
|
|
268
414
|
}
|
|
269
415
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
clearTimeout(
|
|
277
|
-
|
|
416
|
+
public async flush(): Promise<void> {
|
|
417
|
+
await this.flushQueue();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
public async shutdown(): Promise<void> {
|
|
421
|
+
if (this.flushTimeoutId) {
|
|
422
|
+
clearTimeout(this.flushTimeoutId);
|
|
423
|
+
this.flushTimeoutId = null;
|
|
278
424
|
}
|
|
279
|
-
|
|
280
|
-
await
|
|
281
|
-
|
|
282
|
-
|
|
425
|
+
|
|
426
|
+
await this.flushQueue();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
public async close(): Promise<void> {
|
|
430
|
+
await this.shutdown();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public async listEvents(): Promise<Event[]> {
|
|
434
|
+
const response = await this.axiosInstance.get<Event[]>(`${this.servicerEndpoint}/events`);
|
|
435
|
+
return response.data;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
public async getEvent(eventID: string): Promise<Event> {
|
|
439
|
+
const response = await this.axiosInstance.get<Event>(`${this.servicerEndpoint}/events/${eventID}`);
|
|
440
|
+
return response.data;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
public async updateEvent(
|
|
444
|
+
eventID: string,
|
|
445
|
+
payload: Partial<Omit<Event, "event_id" | "created_at">>
|
|
446
|
+
): Promise<Event> {
|
|
447
|
+
const response = await this.axiosInstance.put<Event>(
|
|
448
|
+
`${this.servicerEndpoint}/events/${eventID}`,
|
|
449
|
+
payload
|
|
450
|
+
);
|
|
451
|
+
return response.data;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public async listPolicies(): Promise<Policy[]> {
|
|
455
|
+
const response = await this.axiosInstance.get<Policy[]>(`${this.servicerEndpoint}/policies`);
|
|
456
|
+
return response.data;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public async createPolicy(payload: Omit<Policy, "policy_id">): Promise<Policy> {
|
|
460
|
+
const response = await this.axiosInstance.post<Policy>(`${this.servicerEndpoint}/policies`, payload);
|
|
461
|
+
return response.data;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
public async listAlerts(status?: string): Promise<Alert[]> {
|
|
465
|
+
const endpoint = status
|
|
466
|
+
? `${this.servicerEndpoint}/alerts?status=${encodeURIComponent(status)}`
|
|
467
|
+
: `${this.servicerEndpoint}/alerts`;
|
|
468
|
+
const response = await this.axiosInstance.get<Alert[]>(endpoint);
|
|
469
|
+
return response.data;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
public async getAlertDeliveries(alertID: string, options: { limit?: number; offset?: number; kind?: DeliveryKind } = {}): Promise<AlertDelivery[]> {
|
|
473
|
+
const params = new URLSearchParams();
|
|
474
|
+
if (options.kind) {
|
|
475
|
+
params.set("kind", options.kind);
|
|
476
|
+
}
|
|
477
|
+
if (typeof options.limit === "number") {
|
|
478
|
+
params.set("limit", String(options.limit));
|
|
479
|
+
}
|
|
480
|
+
if (typeof options.offset === "number") {
|
|
481
|
+
params.set("offset", String(options.offset));
|
|
482
|
+
}
|
|
483
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
484
|
+
const response = await this.axiosInstance.get<AlertDelivery[]>(`${this.servicerEndpoint}/alerts/${alertID}/deliveries${suffix}`);
|
|
485
|
+
return response.data;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public async acknowledgeAlert(alertID: string): Promise<Alert> {
|
|
489
|
+
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/acknowledge`);
|
|
490
|
+
return response.data;
|
|
283
491
|
}
|
|
492
|
+
|
|
493
|
+
public async archiveAlert(alertID: string): Promise<Alert> {
|
|
494
|
+
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/archive`);
|
|
495
|
+
return response.data;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
public async testWebhook(webhookID: string): Promise<void> {
|
|
499
|
+
await this.axiosInstance.post(`${this.servicerEndpoint}/webhooks/${webhookID}/test`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
public async getEventLogs(eventID: string, options: PaginationOptions = {}): Promise<EventLogPoint[]> {
|
|
503
|
+
const params = new URLSearchParams();
|
|
504
|
+
if (options.period) {
|
|
505
|
+
params.set("period", options.period);
|
|
506
|
+
}
|
|
507
|
+
if (typeof options.limit === "number") {
|
|
508
|
+
params.set("limit", String(options.limit));
|
|
509
|
+
}
|
|
510
|
+
if (typeof options.offset === "number") {
|
|
511
|
+
params.set("offset", String(options.offset));
|
|
512
|
+
}
|
|
513
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
514
|
+
const response = await this.axiosInstance.get<EventLogPoint[]>(`${this.servicerEndpoint}/events/${eventID}/logs${suffix}`);
|
|
515
|
+
return response.data;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
public async resolveAlert(alertID: string): Promise<Alert> {
|
|
519
|
+
const response = await this.axiosInstance.post<Alert>(`${this.servicerEndpoint}/alerts/${alertID}/resolve`);
|
|
520
|
+
return response.data;
|
|
521
|
+
}
|
|
522
|
+
|
|
284
523
|
}
|
|
285
524
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
*/
|
|
291
|
-
function base64UrlDecode(str: string): string {
|
|
292
|
-
// Replace '-' with '+' and '_' with '/'
|
|
293
|
-
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
294
|
-
// Pad the base64 string
|
|
295
|
-
const padding = base64.length % 4;
|
|
296
|
-
if (padding !== 0) {
|
|
297
|
-
base64 += "=".repeat(4 - padding);
|
|
298
|
-
}
|
|
299
|
-
return Base64.decode(base64);
|
|
525
|
+
let instance: Client | null = null;
|
|
526
|
+
|
|
527
|
+
export function createClient(config: Config = {}): Client {
|
|
528
|
+
return new Client(config);
|
|
300
529
|
}
|
|
301
530
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
531
|
+
function isNodeEnvironment(): boolean {
|
|
532
|
+
return typeof process !== "undefined" && !!(process.versions && process.versions.node);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function isValidAPIKey(token: string): boolean {
|
|
536
|
+
return token.startsWith("chp_") && token.length > "chp_".length;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isUUID(value: string): boolean {
|
|
540
|
+
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);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function resolveLogID(logID?: string): string {
|
|
544
|
+
if (typeof logID === "string") {
|
|
545
|
+
const trimmedLogID = logID.trim();
|
|
546
|
+
if (trimmedLogID.length > 0) {
|
|
547
|
+
return trimmedLogID;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return uuidv7();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function loadDotEnvKey(): string | undefined {
|
|
555
|
+
if (!isNodeEnvironment()) {
|
|
556
|
+
return undefined;
|
|
311
557
|
}
|
|
558
|
+
|
|
312
559
|
try {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
560
|
+
dotenv.config({ path: ".env", override: false });
|
|
561
|
+
} catch {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const envKey = process.env.CHIRPIER_API_KEY;
|
|
566
|
+
if (typeof envKey !== "string") {
|
|
567
|
+
return undefined;
|
|
319
568
|
}
|
|
569
|
+
|
|
570
|
+
const trimmedKey = envKey.trim();
|
|
571
|
+
return trimmedKey.length > 0 ? trimmedKey : undefined;
|
|
320
572
|
}
|
|
321
573
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
574
|
+
function resolveAPIKey(providedKey?: string): string | undefined {
|
|
575
|
+
if (typeof providedKey === "string" && providedKey.trim().length > 0) {
|
|
576
|
+
return providedKey.trim();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (typeof process !== "undefined" && process.env && typeof process.env.CHIRPIER_API_KEY === "string") {
|
|
580
|
+
const envKey = process.env.CHIRPIER_API_KEY.trim();
|
|
581
|
+
if (envKey.length > 0) {
|
|
582
|
+
return envKey;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return loadDotEnvKey();
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function initialize(options: Config = {}): void {
|
|
590
|
+
const resolvedKey = resolveAPIKey(options.key);
|
|
591
|
+
if (!resolvedKey) {
|
|
592
|
+
throw new ChirpierError("API key is required", "INVALID_KEY");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (!isValidAPIKey(resolvedKey)) {
|
|
596
|
+
throw new ChirpierError("Invalid API key: must start with 'chp_'", "INVALID_KEY");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (instance) {
|
|
600
|
+
return;
|
|
329
601
|
}
|
|
330
602
|
|
|
331
603
|
try {
|
|
332
|
-
|
|
604
|
+
instance = new Client({ ...options, key: resolvedKey });
|
|
333
605
|
} catch (error) {
|
|
334
606
|
if (error instanceof ChirpierError) {
|
|
335
607
|
if (options.logLevel && options.logLevel >= LogLevel.Error) {
|
|
@@ -347,19 +619,33 @@ export function initialize(options: Options): void {
|
|
|
347
619
|
}
|
|
348
620
|
}
|
|
349
621
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
622
|
+
export async function logEvent(log: Log): Promise<void> {
|
|
623
|
+
if (!instance) {
|
|
624
|
+
throw new ChirpierError(
|
|
625
|
+
"Chirpier SDK is not initialized. Please call initialize() first.",
|
|
626
|
+
"NOT_INITIALIZED"
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
await instance.log(log);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export async function stop(): Promise<void> {
|
|
634
|
+
if (!instance) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
await instance.shutdown();
|
|
639
|
+
instance = null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export async function flush(): Promise<void> {
|
|
356
643
|
if (!instance) {
|
|
357
644
|
throw new ChirpierError(
|
|
358
|
-
"Chirpier SDK is not initialized. Please call initialize() first."
|
|
645
|
+
"Chirpier SDK is not initialized. Please call initialize() first.",
|
|
646
|
+
"NOT_INITIALIZED"
|
|
359
647
|
);
|
|
360
648
|
}
|
|
361
649
|
|
|
362
|
-
instance.
|
|
363
|
-
console.error("Error in monitor function:", error);
|
|
364
|
-
});
|
|
650
|
+
await instance.flush();
|
|
365
651
|
}
|