warning_sdk 1.0.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/dist/core/aggregator.d.ts +28 -0
- package/dist/core/aggregator.js +163 -0
- package/dist/core/bucket.d.ts +41 -0
- package/dist/core/bucket.js +237 -0
- package/dist/core/client.d.ts +22 -0
- package/dist/core/client.js +51 -0
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +46 -0
- package/dist/core/deterministicCrypto.d.ts +8 -0
- package/dist/core/deterministicCrypto.js +153 -0
- package/dist/core/privacy.d.ts +5 -0
- package/dist/core/privacy.js +18 -0
- package/dist/core/transport.d.ts +12 -0
- package/dist/core/transport.js +107 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +27 -0
- package/dist/platforms/express.d.ts +5 -0
- package/dist/platforms/express.js +102 -0
- package/package.json +41 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MetricFields } from './bucket';
|
|
2
|
+
import { WarningSDKConfig } from './config';
|
|
3
|
+
import { Transport } from './transport';
|
|
4
|
+
export declare class Aggregator {
|
|
5
|
+
private config;
|
|
6
|
+
private transport;
|
|
7
|
+
private currentBucket;
|
|
8
|
+
private flushTimer;
|
|
9
|
+
private pendingBuckets;
|
|
10
|
+
private sendLoop;
|
|
11
|
+
private degradedMinute;
|
|
12
|
+
private capFlushMinute;
|
|
13
|
+
private pausedUntilMs;
|
|
14
|
+
private readonly MAX_PENDING_BUCKETS;
|
|
15
|
+
private readonly DEGRADE_THRESHOLD;
|
|
16
|
+
constructor(config: WarningSDKConfig, transport: Transport);
|
|
17
|
+
isPaused(): boolean;
|
|
18
|
+
private pauseForMs;
|
|
19
|
+
private createNewBucket;
|
|
20
|
+
private rotateBucket;
|
|
21
|
+
private enqueueBucket;
|
|
22
|
+
private drainQueue;
|
|
23
|
+
record(fields: MetricFields): void;
|
|
24
|
+
private startFlushInterval;
|
|
25
|
+
flush(): Promise<void>;
|
|
26
|
+
private flushBucket;
|
|
27
|
+
shutdown(): Promise<void>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Aggregator = void 0;
|
|
4
|
+
const bucket_1 = require("./bucket");
|
|
5
|
+
class Aggregator {
|
|
6
|
+
constructor(config, transport) {
|
|
7
|
+
this.flushTimer = null;
|
|
8
|
+
this.pendingBuckets = [];
|
|
9
|
+
this.sendLoop = null;
|
|
10
|
+
this.degradedMinute = null;
|
|
11
|
+
this.capFlushMinute = null;
|
|
12
|
+
this.pausedUntilMs = 0;
|
|
13
|
+
this.MAX_PENDING_BUCKETS = 5;
|
|
14
|
+
this.DEGRADE_THRESHOLD = Math.floor(bucket_1.Bucket.MAX_TOTAL_KEYS * 0.8);
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.transport = transport;
|
|
17
|
+
this.currentBucket = this.createNewBucket();
|
|
18
|
+
this.startFlushInterval();
|
|
19
|
+
}
|
|
20
|
+
isPaused() {
|
|
21
|
+
return Date.now() < this.pausedUntilMs;
|
|
22
|
+
}
|
|
23
|
+
pauseForMs(durationMs) {
|
|
24
|
+
const d = Number.isFinite(durationMs) ? Math.max(0, durationMs) : 0;
|
|
25
|
+
const next = Date.now() + (d || 300000);
|
|
26
|
+
if (next > this.pausedUntilMs)
|
|
27
|
+
this.pausedUntilMs = next;
|
|
28
|
+
// Drop queued data to avoid memory growth and unnecessary work.
|
|
29
|
+
this.pendingBuckets = [];
|
|
30
|
+
this.currentBucket = this.createNewBucket();
|
|
31
|
+
this.degradedMinute = null;
|
|
32
|
+
this.capFlushMinute = null;
|
|
33
|
+
}
|
|
34
|
+
createNewBucket(alignedTime) {
|
|
35
|
+
const time = alignedTime ?? Date.now();
|
|
36
|
+
const bucketTime = Math.floor(time / 60000) * 60000;
|
|
37
|
+
return new bucket_1.Bucket(bucketTime);
|
|
38
|
+
}
|
|
39
|
+
rotateBucket(nextAlignedTime) {
|
|
40
|
+
const bucketToSend = this.currentBucket;
|
|
41
|
+
this.currentBucket = this.createNewBucket(nextAlignedTime);
|
|
42
|
+
if (bucketToSend.timestamp !== nextAlignedTime) {
|
|
43
|
+
this.degradedMinute = null;
|
|
44
|
+
this.capFlushMinute = null;
|
|
45
|
+
}
|
|
46
|
+
return bucketToSend;
|
|
47
|
+
}
|
|
48
|
+
enqueueBucket(bucket) {
|
|
49
|
+
if (bucket.isEmpty())
|
|
50
|
+
return;
|
|
51
|
+
if (this.pendingBuckets.length >= this.MAX_PENDING_BUCKETS) {
|
|
52
|
+
this.pendingBuckets.shift();
|
|
53
|
+
}
|
|
54
|
+
this.pendingBuckets.push(bucket);
|
|
55
|
+
}
|
|
56
|
+
async drainQueue() {
|
|
57
|
+
if (this.sendLoop)
|
|
58
|
+
return this.sendLoop;
|
|
59
|
+
this.sendLoop = (async () => {
|
|
60
|
+
while (this.pendingBuckets.length > 0) {
|
|
61
|
+
const bucket = this.pendingBuckets.shift();
|
|
62
|
+
if (!bucket)
|
|
63
|
+
continue;
|
|
64
|
+
try {
|
|
65
|
+
await this.flushBucket(bucket);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Silently fail, data is dropped to prevent memory leaks/blocking
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})().finally(() => {
|
|
72
|
+
this.sendLoop = null;
|
|
73
|
+
if (this.pendingBuckets.length > 0) {
|
|
74
|
+
void this.drainQueue();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return this.sendLoop;
|
|
78
|
+
}
|
|
79
|
+
record(fields) {
|
|
80
|
+
if (this.isPaused())
|
|
81
|
+
return;
|
|
82
|
+
const eventTime = fields.timestamp ?? Date.now();
|
|
83
|
+
const alignedTime = Math.floor(eventTime / 60000) * 60000;
|
|
84
|
+
if (this.currentBucket.timestamp !== alignedTime) {
|
|
85
|
+
const bucketToSend = this.rotateBucket(alignedTime);
|
|
86
|
+
this.enqueueBucket(bucketToSend);
|
|
87
|
+
void this.drainQueue();
|
|
88
|
+
}
|
|
89
|
+
const degrade = this.degradedMinute === alignedTime;
|
|
90
|
+
if (degrade) {
|
|
91
|
+
this.currentBucket.incrementDegraded(fields);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.currentBucket.increment(fields);
|
|
95
|
+
}
|
|
96
|
+
if (this.degradedMinute === null && this.currentBucket.size() >= this.DEGRADE_THRESHOLD) {
|
|
97
|
+
this.degradedMinute = alignedTime;
|
|
98
|
+
}
|
|
99
|
+
if (this.currentBucket.size() >= bucket_1.Bucket.MAX_TOTAL_KEYS) {
|
|
100
|
+
if (this.capFlushMinute !== alignedTime) {
|
|
101
|
+
this.capFlushMinute = alignedTime;
|
|
102
|
+
this.degradedMinute = alignedTime;
|
|
103
|
+
const bucketToSend = this.rotateBucket(alignedTime);
|
|
104
|
+
this.enqueueBucket(bucketToSend);
|
|
105
|
+
void this.drainQueue();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
startFlushInterval() {
|
|
110
|
+
if (this.flushTimer)
|
|
111
|
+
clearInterval(this.flushTimer);
|
|
112
|
+
this.flushTimer = setInterval(() => {
|
|
113
|
+
void this.flush().catch(() => { });
|
|
114
|
+
}, 10000);
|
|
115
|
+
if (this.flushTimer.unref) {
|
|
116
|
+
this.flushTimer.unref();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async flush() {
|
|
120
|
+
if (this.currentBucket.isEmpty()) {
|
|
121
|
+
const nowAligned = Math.floor(Date.now() / 60000) * 60000;
|
|
122
|
+
if (this.currentBucket.timestamp !== nowAligned) {
|
|
123
|
+
this.currentBucket = new bucket_1.Bucket(nowAligned);
|
|
124
|
+
this.degradedMinute = null;
|
|
125
|
+
this.capFlushMinute = null;
|
|
126
|
+
}
|
|
127
|
+
await this.drainQueue();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const nowAligned = Math.floor(Date.now() / 60000) * 60000;
|
|
131
|
+
const bucketToSend = this.rotateBucket(nowAligned);
|
|
132
|
+
this.enqueueBucket(bucketToSend);
|
|
133
|
+
await this.drainQueue();
|
|
134
|
+
}
|
|
135
|
+
async flushBucket(bucketToSend) {
|
|
136
|
+
if (bucketToSend.isEmpty())
|
|
137
|
+
return;
|
|
138
|
+
if (this.isPaused())
|
|
139
|
+
return;
|
|
140
|
+
const metricsWithTime = bucketToSend.toJSONWithMinute(bucketToSend.timestamp);
|
|
141
|
+
const maxRows = Math.min(2000, metricsWithTime.length);
|
|
142
|
+
for (let i = 0; i < metricsWithTime.length; i += maxRows) {
|
|
143
|
+
const payload = {
|
|
144
|
+
project_id: this.config.projectId,
|
|
145
|
+
project_token: this.config.projectToken,
|
|
146
|
+
sent_at: Date.now(),
|
|
147
|
+
metrics: metricsWithTime.slice(i, i + maxRows)
|
|
148
|
+
};
|
|
149
|
+
const res = await this.transport.send(payload);
|
|
150
|
+
if (res?.paused) {
|
|
151
|
+
this.pauseForMs(res.retryAfterMs || 300000);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async shutdown() {
|
|
157
|
+
if (this.flushTimer) {
|
|
158
|
+
clearInterval(this.flushTimer);
|
|
159
|
+
}
|
|
160
|
+
await this.flush();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.Aggregator = Aggregator;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface MetricFields {
|
|
2
|
+
serviceName?: string;
|
|
3
|
+
method: string;
|
|
4
|
+
route: string;
|
|
5
|
+
status: number;
|
|
6
|
+
apiKeyId?: string;
|
|
7
|
+
ipHash?: string;
|
|
8
|
+
uaHash?: string;
|
|
9
|
+
latencyMs?: number;
|
|
10
|
+
aborted?: boolean;
|
|
11
|
+
timestamp?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class Bucket {
|
|
14
|
+
timestamp: number;
|
|
15
|
+
counts: Map<string, {
|
|
16
|
+
count: number;
|
|
17
|
+
sumLatencyMs?: number;
|
|
18
|
+
minLatencyMs?: number;
|
|
19
|
+
maxLatencyMs?: number;
|
|
20
|
+
latencyHistogram?: Record<string, number>;
|
|
21
|
+
}>;
|
|
22
|
+
private ipTracking;
|
|
23
|
+
private overflowProtection;
|
|
24
|
+
private static MAX_UNIQUE_IPS;
|
|
25
|
+
static readonly MAX_TOTAL_KEYS = 2000;
|
|
26
|
+
private static readonly SEP;
|
|
27
|
+
private static readonly ESC;
|
|
28
|
+
constructor(timestamp: number);
|
|
29
|
+
private static escapePart;
|
|
30
|
+
private static unescapePart;
|
|
31
|
+
private static splitKey;
|
|
32
|
+
private static latencyBucket;
|
|
33
|
+
static generateKeyParts(serviceName: string, method: string, route: string, status: number, apiKeyId?: string, ipHash?: string, uaHash?: string): string;
|
|
34
|
+
private incrementInternal;
|
|
35
|
+
increment(fields: MetricFields): void;
|
|
36
|
+
incrementDegraded(fields: MetricFields): void;
|
|
37
|
+
isEmpty(): boolean;
|
|
38
|
+
size(): number;
|
|
39
|
+
toJSON(): any[];
|
|
40
|
+
toJSONWithMinute(minute: number): any[];
|
|
41
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Bucket = void 0;
|
|
4
|
+
class Bucket {
|
|
5
|
+
constructor(timestamp) {
|
|
6
|
+
this.ipTracking = new Map();
|
|
7
|
+
this.overflowProtection = new Set();
|
|
8
|
+
this.timestamp = timestamp;
|
|
9
|
+
this.counts = new Map();
|
|
10
|
+
}
|
|
11
|
+
static escapePart(value) {
|
|
12
|
+
let out = '';
|
|
13
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
14
|
+
const ch = value[i];
|
|
15
|
+
if (ch === Bucket.ESC || ch === Bucket.SEP) {
|
|
16
|
+
out += Bucket.ESC;
|
|
17
|
+
}
|
|
18
|
+
out += ch;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
static unescapePart(value) {
|
|
23
|
+
let out = '';
|
|
24
|
+
let escaped = false;
|
|
25
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
26
|
+
const ch = value[i];
|
|
27
|
+
if (escaped) {
|
|
28
|
+
out += ch;
|
|
29
|
+
escaped = false;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === Bucket.ESC) {
|
|
33
|
+
escaped = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
out += ch;
|
|
37
|
+
}
|
|
38
|
+
if (escaped)
|
|
39
|
+
return null;
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
static splitKey(key) {
|
|
43
|
+
const parts = [];
|
|
44
|
+
let current = '';
|
|
45
|
+
let escaped = false;
|
|
46
|
+
for (let i = 0; i < key.length; i += 1) {
|
|
47
|
+
const ch = key[i];
|
|
48
|
+
if (escaped) {
|
|
49
|
+
current += Bucket.ESC;
|
|
50
|
+
current += ch;
|
|
51
|
+
escaped = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === Bucket.ESC) {
|
|
55
|
+
escaped = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (ch === Bucket.SEP) {
|
|
59
|
+
parts.push(current);
|
|
60
|
+
current = '';
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
current += ch;
|
|
64
|
+
}
|
|
65
|
+
if (escaped)
|
|
66
|
+
return null;
|
|
67
|
+
parts.push(current);
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
static latencyBucket(ms) {
|
|
71
|
+
// Buckets must match backend parser:
|
|
72
|
+
// - "a-b" or "a+" (see warning_backend/analytics.py:_parse_bucket_range)
|
|
73
|
+
if (ms < 0)
|
|
74
|
+
ms = 0;
|
|
75
|
+
if (ms <= 10)
|
|
76
|
+
return "0-10";
|
|
77
|
+
if (ms <= 25)
|
|
78
|
+
return "10-25";
|
|
79
|
+
if (ms <= 50)
|
|
80
|
+
return "25-50";
|
|
81
|
+
if (ms <= 100)
|
|
82
|
+
return "50-100";
|
|
83
|
+
if (ms <= 200)
|
|
84
|
+
return "100-200";
|
|
85
|
+
if (ms <= 350)
|
|
86
|
+
return "200-350";
|
|
87
|
+
if (ms <= 500)
|
|
88
|
+
return "350-500";
|
|
89
|
+
if (ms <= 750)
|
|
90
|
+
return "500-750";
|
|
91
|
+
if (ms <= 1000)
|
|
92
|
+
return "750-1000";
|
|
93
|
+
if (ms <= 1500)
|
|
94
|
+
return "1000-1500";
|
|
95
|
+
if (ms <= 2000)
|
|
96
|
+
return "1500-2000";
|
|
97
|
+
if (ms <= 3000)
|
|
98
|
+
return "2000-3000";
|
|
99
|
+
if (ms <= 5000)
|
|
100
|
+
return "3000-5000";
|
|
101
|
+
return "5000+";
|
|
102
|
+
}
|
|
103
|
+
static generateKeyParts(serviceName, method, route, status, apiKeyId, ipHash, uaHash) {
|
|
104
|
+
const svc = serviceName || '';
|
|
105
|
+
const ak = apiKeyId || '';
|
|
106
|
+
const ip = ipHash || '';
|
|
107
|
+
const ua = uaHash || '';
|
|
108
|
+
return [
|
|
109
|
+
Bucket.escapePart(svc),
|
|
110
|
+
Bucket.escapePart(method),
|
|
111
|
+
Bucket.escapePart(route),
|
|
112
|
+
Bucket.escapePart(String(status)),
|
|
113
|
+
Bucket.escapePart(ak),
|
|
114
|
+
Bucket.escapePart(ip),
|
|
115
|
+
Bucket.escapePart(ua)
|
|
116
|
+
].join(Bucket.SEP);
|
|
117
|
+
}
|
|
118
|
+
incrementInternal(fields, degraded) {
|
|
119
|
+
let ipHash = fields.ipHash || '';
|
|
120
|
+
if (!degraded && fields.ipHash) {
|
|
121
|
+
const guardKey = `${fields.serviceName || ''}|${fields.method}|${fields.route}|${fields.apiKeyId || 'none'}`;
|
|
122
|
+
if (this.overflowProtection.has(guardKey)) {
|
|
123
|
+
ipHash = 'MANY';
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
let s = this.ipTracking.get(guardKey);
|
|
127
|
+
if (!s) {
|
|
128
|
+
s = new Set();
|
|
129
|
+
this.ipTracking.set(guardKey, s);
|
|
130
|
+
}
|
|
131
|
+
s.add(ipHash);
|
|
132
|
+
if (s.size > Bucket.MAX_UNIQUE_IPS) {
|
|
133
|
+
this.overflowProtection.add(guardKey);
|
|
134
|
+
this.ipTracking.delete(guardKey);
|
|
135
|
+
ipHash = 'MANY';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const k = Bucket.generateKeyParts(fields.serviceName || '', fields.method, fields.route, fields.status, fields.apiKeyId, degraded ? undefined : ipHash, degraded ? undefined : fields.uaHash);
|
|
140
|
+
let current = this.counts.get(k);
|
|
141
|
+
if (!current) {
|
|
142
|
+
if (this.counts.size >= Bucket.MAX_TOTAL_KEYS) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
current = { count: 0 };
|
|
146
|
+
this.counts.set(k, current);
|
|
147
|
+
}
|
|
148
|
+
current.count += 1;
|
|
149
|
+
if (Number.isFinite(fields.latencyMs)) {
|
|
150
|
+
const latency = fields.latencyMs;
|
|
151
|
+
current.sumLatencyMs = (current.sumLatencyMs || 0) + latency;
|
|
152
|
+
current.maxLatencyMs = current.maxLatencyMs === undefined ? latency : Math.max(current.maxLatencyMs, latency);
|
|
153
|
+
current.minLatencyMs = current.minLatencyMs === undefined ? latency : Math.min(current.minLatencyMs, latency);
|
|
154
|
+
const bucket = Bucket.latencyBucket(latency);
|
|
155
|
+
const hist = current.latencyHistogram || (current.latencyHistogram = {});
|
|
156
|
+
hist[bucket] = (hist[bucket] || 0) + 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
increment(fields) {
|
|
160
|
+
this.incrementInternal(fields, false);
|
|
161
|
+
}
|
|
162
|
+
incrementDegraded(fields) {
|
|
163
|
+
this.incrementInternal(fields, true);
|
|
164
|
+
}
|
|
165
|
+
isEmpty() {
|
|
166
|
+
return this.counts.size === 0;
|
|
167
|
+
}
|
|
168
|
+
size() {
|
|
169
|
+
return this.counts.size;
|
|
170
|
+
}
|
|
171
|
+
toJSON() {
|
|
172
|
+
const metrics = [];
|
|
173
|
+
for (const [k, v] of this.counts.entries()) {
|
|
174
|
+
const parts = Bucket.splitKey(k);
|
|
175
|
+
if (!parts || parts.length !== 7)
|
|
176
|
+
continue;
|
|
177
|
+
const unescaped = parts.map(part => Bucket.unescapePart(part));
|
|
178
|
+
if (unescaped.some(part => part === null))
|
|
179
|
+
continue;
|
|
180
|
+
const [serviceName, method, route, statusRaw, apiKeyId, ipHash, uaHash] = unescaped;
|
|
181
|
+
const statusValue = parseInt(statusRaw, 10);
|
|
182
|
+
if (!Number.isFinite(statusValue))
|
|
183
|
+
continue;
|
|
184
|
+
metrics.push({
|
|
185
|
+
service_name: serviceName || undefined,
|
|
186
|
+
method,
|
|
187
|
+
route,
|
|
188
|
+
status: statusValue,
|
|
189
|
+
api_key_id: apiKeyId || undefined,
|
|
190
|
+
ip_hash: ipHash || undefined,
|
|
191
|
+
ua_hash: uaHash || undefined,
|
|
192
|
+
count: v.count,
|
|
193
|
+
sum_latency_ms: v.sumLatencyMs,
|
|
194
|
+
min_latency_ms: v.minLatencyMs,
|
|
195
|
+
max_latency_ms: v.maxLatencyMs,
|
|
196
|
+
latency_histogram: v.latencyHistogram && Object.keys(v.latencyHistogram).length ? v.latencyHistogram : undefined
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return metrics;
|
|
200
|
+
}
|
|
201
|
+
toJSONWithMinute(minute) {
|
|
202
|
+
const metrics = [];
|
|
203
|
+
for (const [k, v] of this.counts.entries()) {
|
|
204
|
+
const parts = Bucket.splitKey(k);
|
|
205
|
+
if (!parts || parts.length !== 7)
|
|
206
|
+
continue;
|
|
207
|
+
const unescaped = parts.map(part => Bucket.unescapePart(part));
|
|
208
|
+
if (unescaped.some(part => part === null))
|
|
209
|
+
continue;
|
|
210
|
+
const [serviceName, method, route, statusRaw, apiKeyId, ipHash, uaHash] = unescaped;
|
|
211
|
+
const statusValue = parseInt(statusRaw, 10);
|
|
212
|
+
if (!Number.isFinite(statusValue))
|
|
213
|
+
continue;
|
|
214
|
+
metrics.push({
|
|
215
|
+
service_name: serviceName || undefined,
|
|
216
|
+
method,
|
|
217
|
+
route,
|
|
218
|
+
status: statusValue,
|
|
219
|
+
api_key_id: apiKeyId || undefined,
|
|
220
|
+
ip_hash: ipHash || undefined,
|
|
221
|
+
ua_hash: uaHash || undefined,
|
|
222
|
+
count: v.count,
|
|
223
|
+
sum_latency_ms: v.sumLatencyMs,
|
|
224
|
+
min_latency_ms: v.minLatencyMs,
|
|
225
|
+
max_latency_ms: v.maxLatencyMs,
|
|
226
|
+
latency_histogram: v.latencyHistogram && Object.keys(v.latencyHistogram).length ? v.latencyHistogram : undefined,
|
|
227
|
+
minute
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return metrics;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
exports.Bucket = Bucket;
|
|
234
|
+
Bucket.MAX_UNIQUE_IPS = 50;
|
|
235
|
+
Bucket.MAX_TOTAL_KEYS = 2000;
|
|
236
|
+
Bucket.SEP = '\u001f';
|
|
237
|
+
Bucket.ESC = '\\';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { WarningSDKConfig } from './config';
|
|
2
|
+
export declare class WarningClient {
|
|
3
|
+
readonly config: WarningSDKConfig;
|
|
4
|
+
private aggregator;
|
|
5
|
+
private initialized;
|
|
6
|
+
constructor(config: Partial<WarningSDKConfig>);
|
|
7
|
+
isPaused(): boolean;
|
|
8
|
+
recordRequest(req: {
|
|
9
|
+
serviceName?: string;
|
|
10
|
+
method: string;
|
|
11
|
+
route: string;
|
|
12
|
+
status: number;
|
|
13
|
+
ip?: string;
|
|
14
|
+
userAgent?: string;
|
|
15
|
+
apiKeyId?: string;
|
|
16
|
+
authToken?: string;
|
|
17
|
+
aborted?: boolean;
|
|
18
|
+
latencyMs?: number;
|
|
19
|
+
timestamp?: number;
|
|
20
|
+
}): void;
|
|
21
|
+
flush(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WarningClient = void 0;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const aggregator_1 = require("./aggregator");
|
|
6
|
+
const transport_1 = require("./transport");
|
|
7
|
+
const privacy_1 = require("./privacy");
|
|
8
|
+
class WarningClient {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.initialized = false;
|
|
11
|
+
this.config = (0, config_1.validateConfig)(config);
|
|
12
|
+
const transport = new transport_1.Transport(this.config);
|
|
13
|
+
this.aggregator = new aggregator_1.Aggregator(this.config, transport);
|
|
14
|
+
this.initialized = true;
|
|
15
|
+
}
|
|
16
|
+
isPaused() {
|
|
17
|
+
return this.aggregator.isPaused();
|
|
18
|
+
}
|
|
19
|
+
recordRequest(req) {
|
|
20
|
+
if (!this.initialized)
|
|
21
|
+
return;
|
|
22
|
+
if (this.aggregator.isPaused())
|
|
23
|
+
return;
|
|
24
|
+
const serviceName = req.serviceName || this.config.serviceName;
|
|
25
|
+
let finalIpHash = req.ip ? privacy_1.Privacy.passThroughIp(req.ip) : undefined;
|
|
26
|
+
let finalUaHash = req.userAgent ? privacy_1.Privacy.passThroughUa(req.userAgent) : undefined;
|
|
27
|
+
let apiKeyId = req.apiKeyId;
|
|
28
|
+
if (!apiKeyId && req.authToken) {
|
|
29
|
+
apiKeyId = privacy_1.Privacy.passThroughApiKey(req.authToken);
|
|
30
|
+
}
|
|
31
|
+
let latencyMs = req.latencyMs;
|
|
32
|
+
if (!Number.isFinite(latencyMs) || latencyMs < 0 || latencyMs >= 120000) {
|
|
33
|
+
latencyMs = undefined;
|
|
34
|
+
}
|
|
35
|
+
this.aggregator.record({
|
|
36
|
+
serviceName: serviceName,
|
|
37
|
+
method: req.method.toUpperCase(),
|
|
38
|
+
route: req.route,
|
|
39
|
+
status: req.status,
|
|
40
|
+
apiKeyId: apiKeyId,
|
|
41
|
+
ipHash: finalIpHash,
|
|
42
|
+
uaHash: finalUaHash,
|
|
43
|
+
latencyMs: latencyMs,
|
|
44
|
+
timestamp: req.timestamp || Date.now()
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
flush() {
|
|
48
|
+
return this.aggregator.flush();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.WarningClient = WarningClient;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface WarningSDKConfig {
|
|
2
|
+
projectId: string;
|
|
3
|
+
projectToken: string;
|
|
4
|
+
ingestKey: string;
|
|
5
|
+
serviceName?: string;
|
|
6
|
+
collectorUrl?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Which inbound header contains the business's end-user API token.
|
|
9
|
+
* Used to derive `api_key_id` (hashed identifier) for key-sharing detection.
|
|
10
|
+
*
|
|
11
|
+
* Defaults to "authorization".
|
|
12
|
+
*/
|
|
13
|
+
authHeaderName?: string;
|
|
14
|
+
debug?: boolean;
|
|
15
|
+
logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare const DEFAULT_CONFIG: Partial<WarningSDKConfig>;
|
|
18
|
+
export declare function validateConfig(config: Partial<WarningSDKConfig>): WarningSDKConfig;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
4
|
+
exports.validateConfig = validateConfig;
|
|
5
|
+
exports.DEFAULT_CONFIG = {};
|
|
6
|
+
function validateConfig(config) {
|
|
7
|
+
const sanitized = { ...config };
|
|
8
|
+
const merged = { ...exports.DEFAULT_CONFIG, ...sanitized };
|
|
9
|
+
const log = (level, message) => {
|
|
10
|
+
if (merged.logger) {
|
|
11
|
+
merged.logger(level, message);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (level === 'error') {
|
|
15
|
+
console.error(message);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (merged.debug) {
|
|
19
|
+
const fn = level === 'debug'
|
|
20
|
+
? console.debug
|
|
21
|
+
: level === 'info'
|
|
22
|
+
? console.info
|
|
23
|
+
: level === 'warn'
|
|
24
|
+
? console.warn
|
|
25
|
+
: console.error;
|
|
26
|
+
fn(message);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
if (!merged.projectId) {
|
|
30
|
+
log('error', "WarningSDK: Missing 'projectId'.");
|
|
31
|
+
throw new Error("WarningSDK: Missing required config 'projectId'.");
|
|
32
|
+
}
|
|
33
|
+
if (!merged.projectToken) {
|
|
34
|
+
log('error', "WarningSDK: Missing 'projectToken'.");
|
|
35
|
+
throw new Error("WarningSDK: Missing required config 'projectToken'.");
|
|
36
|
+
}
|
|
37
|
+
if (!merged.ingestKey) {
|
|
38
|
+
log('error', "WarningSDK: Missing 'ingestKey'.");
|
|
39
|
+
throw new Error("WarningSDK: Missing required config 'ingestKey'.");
|
|
40
|
+
}
|
|
41
|
+
if (!merged.collectorUrl) {
|
|
42
|
+
log('error', "WarningSDK: Missing 'collectorUrl' (Collector URL).");
|
|
43
|
+
throw new Error("WarningSDK: Missing required config 'collectorUrl'.");
|
|
44
|
+
}
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DeterministicCrypto = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
const VERSION_PREFIX = 'w1.';
|
|
39
|
+
const VERSION_BYTE = 1;
|
|
40
|
+
const SALT = Buffer.from('warning-crypto-v1', 'utf8');
|
|
41
|
+
const INFO = Buffer.from('deterministic-v1', 'utf8');
|
|
42
|
+
function toBase64Url(buf) {
|
|
43
|
+
return buf
|
|
44
|
+
.toString('base64')
|
|
45
|
+
.replace(/\+/g, '-')
|
|
46
|
+
.replace(/\//g, '_')
|
|
47
|
+
.replace(/=+$/g, '');
|
|
48
|
+
}
|
|
49
|
+
function fromBase64Url(s) {
|
|
50
|
+
const normalized = s.replace(/-/g, '+').replace(/_/g, '/');
|
|
51
|
+
const padLen = (4 - (normalized.length % 4)) % 4;
|
|
52
|
+
const padded = normalized + '='.repeat(padLen);
|
|
53
|
+
return Buffer.from(padded, 'base64');
|
|
54
|
+
}
|
|
55
|
+
function packParts(...parts) {
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
const len = Buffer.alloc(4);
|
|
59
|
+
len.writeUInt32BE(part.length, 0);
|
|
60
|
+
out.push(len, part);
|
|
61
|
+
}
|
|
62
|
+
return Buffer.concat(out);
|
|
63
|
+
}
|
|
64
|
+
function hmacSha256(key, data) {
|
|
65
|
+
return crypto.createHmac('sha256', key).update(data).digest();
|
|
66
|
+
}
|
|
67
|
+
function deriveKeys(masterKey) {
|
|
68
|
+
// HKDF-SHA256(masterKey, salt=SALT, info=INFO) -> 96 bytes
|
|
69
|
+
const okm = Buffer.from(crypto.hkdfSync('sha256', masterKey, SALT, INFO, 96));
|
|
70
|
+
return {
|
|
71
|
+
encKey: okm.subarray(0, 32),
|
|
72
|
+
macKey: okm.subarray(32, 64),
|
|
73
|
+
nonceKey: okm.subarray(64, 96),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parseKeyString(key) {
|
|
77
|
+
const trimmed = (key || '').trim();
|
|
78
|
+
if (!trimmed)
|
|
79
|
+
throw new Error("WarningSDK: Missing required config 'cryptoKey'.");
|
|
80
|
+
if (trimmed.startsWith('hex:')) {
|
|
81
|
+
const hex = trimmed.slice(4).trim();
|
|
82
|
+
const buf = Buffer.from(hex, 'hex');
|
|
83
|
+
if (buf.length < 32)
|
|
84
|
+
throw new Error("WarningSDK: 'cryptoKey' (hex) must be at least 32 bytes.");
|
|
85
|
+
return buf;
|
|
86
|
+
}
|
|
87
|
+
// Accept base64 / base64url, with or without padding.
|
|
88
|
+
const buf = fromBase64Url(trimmed);
|
|
89
|
+
if (buf.length < 32)
|
|
90
|
+
throw new Error("WarningSDK: 'cryptoKey' (base64) must be at least 32 bytes.");
|
|
91
|
+
return buf;
|
|
92
|
+
}
|
|
93
|
+
function xorWithKeystream(encKey, nonce, data) {
|
|
94
|
+
const out = Buffer.alloc(data.length);
|
|
95
|
+
let offset = 0;
|
|
96
|
+
let counter = 0;
|
|
97
|
+
while (offset < data.length) {
|
|
98
|
+
const ctr = Buffer.alloc(4);
|
|
99
|
+
ctr.writeUInt32BE(counter >>> 0, 0);
|
|
100
|
+
const block = hmacSha256(encKey, Buffer.concat([nonce, ctr]));
|
|
101
|
+
const take = Math.min(block.length, data.length - offset);
|
|
102
|
+
for (let i = 0; i < take; i += 1) {
|
|
103
|
+
out[offset + i] = data[offset + i] ^ block[i];
|
|
104
|
+
}
|
|
105
|
+
offset += take;
|
|
106
|
+
counter += 1;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
class DeterministicCrypto {
|
|
111
|
+
constructor(cryptoKey) {
|
|
112
|
+
const master = parseKeyString(cryptoKey);
|
|
113
|
+
const keys = deriveKeys(master);
|
|
114
|
+
this.encKey = keys.encKey;
|
|
115
|
+
this.macKey = keys.macKey;
|
|
116
|
+
this.nonceKey = keys.nonceKey;
|
|
117
|
+
}
|
|
118
|
+
encryptDeterministic(value, aad) {
|
|
119
|
+
if (!value)
|
|
120
|
+
return '';
|
|
121
|
+
const aadBuf = Buffer.from(aad || '', 'utf8');
|
|
122
|
+
const pt = Buffer.from(value, 'utf8');
|
|
123
|
+
const nonce = hmacSha256(this.nonceKey, packParts(aadBuf, pt)).subarray(0, 16);
|
|
124
|
+
const ct = xorWithKeystream(this.encKey, nonce, pt);
|
|
125
|
+
const tag = hmacSha256(this.macKey, packParts(aadBuf, nonce, ct)).subarray(0, 16);
|
|
126
|
+
const payload = Buffer.concat([Buffer.from([VERSION_BYTE]), nonce, ct, tag]);
|
|
127
|
+
return VERSION_PREFIX + toBase64Url(payload);
|
|
128
|
+
}
|
|
129
|
+
decryptDeterministic(token, aad) {
|
|
130
|
+
if (!token || !token.startsWith(VERSION_PREFIX))
|
|
131
|
+
return null;
|
|
132
|
+
const aadBuf = Buffer.from(aad || '', 'utf8');
|
|
133
|
+
const raw = fromBase64Url(token.slice(VERSION_PREFIX.length));
|
|
134
|
+
if (raw.length < 1 + 16 + 16)
|
|
135
|
+
return null;
|
|
136
|
+
if (raw[0] !== VERSION_BYTE)
|
|
137
|
+
return null;
|
|
138
|
+
const nonce = raw.subarray(1, 1 + 16);
|
|
139
|
+
const tag = raw.subarray(raw.length - 16);
|
|
140
|
+
const ct = raw.subarray(1 + 16, raw.length - 16);
|
|
141
|
+
const expected = hmacSha256(this.macKey, packParts(aadBuf, nonce, ct)).subarray(0, 16);
|
|
142
|
+
if (expected.length !== tag.length || !crypto.timingSafeEqual(expected, tag))
|
|
143
|
+
return null;
|
|
144
|
+
const pt = xorWithKeystream(this.encKey, nonce, ct);
|
|
145
|
+
try {
|
|
146
|
+
return pt.toString('utf8');
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
exports.DeterministicCrypto = DeterministicCrypto;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Privacy = void 0;
|
|
4
|
+
class Privacy {
|
|
5
|
+
static passThroughIp(ip) {
|
|
6
|
+
const v = (ip || '').trim();
|
|
7
|
+
return v.length ? v : undefined;
|
|
8
|
+
}
|
|
9
|
+
static passThroughApiKey(rawKey) {
|
|
10
|
+
const v = (rawKey || '').trim();
|
|
11
|
+
return v.length ? v : undefined;
|
|
12
|
+
}
|
|
13
|
+
static passThroughUa(ua) {
|
|
14
|
+
const v = (ua || '').trim();
|
|
15
|
+
return v.length ? v : undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.Privacy = Privacy;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { WarningSDKConfig } from './config';
|
|
2
|
+
export type TransportSendResult = {
|
|
3
|
+
paused?: boolean;
|
|
4
|
+
reason?: string;
|
|
5
|
+
retryAfterMs?: number;
|
|
6
|
+
statusCode?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare class Transport {
|
|
9
|
+
private config;
|
|
10
|
+
constructor(config: WarningSDKConfig);
|
|
11
|
+
send(payload: any): Promise<TransportSendResult>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Transport = void 0;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const https = __importStar(require("https"));
|
|
39
|
+
class Transport {
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.config = config;
|
|
42
|
+
}
|
|
43
|
+
async send(payload) {
|
|
44
|
+
if (!this.config.projectToken)
|
|
45
|
+
return {};
|
|
46
|
+
const data = JSON.stringify(payload);
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(this.config.collectorUrl || '');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
const isHttps = url.protocol === 'https:';
|
|
55
|
+
const options = {
|
|
56
|
+
hostname: url.hostname,
|
|
57
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
58
|
+
path: `${url.pathname}${url.search}`,
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
'Content-Length': Buffer.byteLength(data),
|
|
63
|
+
'x-ingest-key': this.config.ingestKey,
|
|
64
|
+
'User-Agent': 'warning-sdk-node/1.0.0'
|
|
65
|
+
},
|
|
66
|
+
timeout: 2000
|
|
67
|
+
};
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const requester = isHttps ? https : http;
|
|
70
|
+
const req = requester.request(options, (res) => {
|
|
71
|
+
const statusCode = res.statusCode || 0;
|
|
72
|
+
let body = '';
|
|
73
|
+
res.setEncoding('utf8');
|
|
74
|
+
res.on('data', (chunk) => {
|
|
75
|
+
if (body.length <= 8192)
|
|
76
|
+
body += chunk;
|
|
77
|
+
});
|
|
78
|
+
res.on('end', () => {
|
|
79
|
+
const out = { statusCode };
|
|
80
|
+
// Default behavior: treat as "sent" even if backend rejected (so app never breaks).
|
|
81
|
+
// But if backend says credits are 0, we pause sending on the SDK side.
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(body || '{}');
|
|
84
|
+
const paused = Boolean(parsed?.paused) || statusCode === 402;
|
|
85
|
+
if (paused) {
|
|
86
|
+
out.paused = true;
|
|
87
|
+
out.reason = String(parsed?.reason || 'paused');
|
|
88
|
+
const retry = parsed?.retry_after_ms;
|
|
89
|
+
if (Number.isFinite(retry) && retry > 0)
|
|
90
|
+
out.retryAfterMs = Number(retry);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// ignore
|
|
95
|
+
}
|
|
96
|
+
resolve(out);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
req.on('error', () => {
|
|
100
|
+
resolve({});
|
|
101
|
+
});
|
|
102
|
+
req.write(data);
|
|
103
|
+
req.end();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.Transport = Transport;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.default = void 0;
|
|
18
|
+
__exportStar(require("./core/config"), exports);
|
|
19
|
+
__exportStar(require("./core/client"), exports);
|
|
20
|
+
__exportStar(require("./core/bucket"), exports);
|
|
21
|
+
__exportStar(require("./core/aggregator"), exports);
|
|
22
|
+
__exportStar(require("./platforms/express"), exports);
|
|
23
|
+
// export * from './platforms/fastify';
|
|
24
|
+
// export * from './platforms/nestjs';
|
|
25
|
+
// export * from './platforms/next';
|
|
26
|
+
var express_1 = require("./platforms/express");
|
|
27
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return express_1.warning; } });
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { WarningClient } from '../core/client';
|
|
3
|
+
import { WarningSDKConfig } from '../core/config';
|
|
4
|
+
export declare function warningMiddleware(config: Partial<WarningSDKConfig> | WarningClient): (req: Request, res: Response, next: NextFunction) => void;
|
|
5
|
+
export declare function warning(config: Partial<WarningSDKConfig> | WarningClient): (req: Request, res: Response, next: NextFunction) => void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.warningMiddleware = warningMiddleware;
|
|
4
|
+
exports.warning = warning;
|
|
5
|
+
const client_1 = require("../core/client");
|
|
6
|
+
function warningMiddleware(config) {
|
|
7
|
+
const client = config instanceof client_1.WarningClient ? config : new client_1.WarningClient(config);
|
|
8
|
+
return (req, res, next) => {
|
|
9
|
+
if (client.isPaused()) {
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
12
|
+
const startTime = process.hrtime.bigint();
|
|
13
|
+
let recorded = false;
|
|
14
|
+
const record = () => {
|
|
15
|
+
if (recorded)
|
|
16
|
+
return;
|
|
17
|
+
recorded = true;
|
|
18
|
+
try {
|
|
19
|
+
const method = req.method;
|
|
20
|
+
const statusCode = res.statusCode;
|
|
21
|
+
const aborted = req.aborted || !res.writableEnded;
|
|
22
|
+
let routePath = 'UNKNOWN';
|
|
23
|
+
if (req.route && req.route.path) {
|
|
24
|
+
routePath = req.baseUrl ? (req.baseUrl + req.route.path) : req.route.path;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const fullPath = (req.baseUrl || '') + (req.path || '');
|
|
28
|
+
const segments = fullPath.split('/').map((segment) => {
|
|
29
|
+
if (!segment)
|
|
30
|
+
return segment; // empty
|
|
31
|
+
if (/^[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(segment)) {
|
|
32
|
+
return ':uuid';
|
|
33
|
+
}
|
|
34
|
+
if (/^[0-9a-fA-F]{24,}$/.test(segment)) {
|
|
35
|
+
return ':hex_id';
|
|
36
|
+
}
|
|
37
|
+
if (/^\d+$/.test(segment)) {
|
|
38
|
+
return ':id';
|
|
39
|
+
}
|
|
40
|
+
if (segment.length >= 26) {
|
|
41
|
+
if (/[0-9]/.test(segment)) {
|
|
42
|
+
return ':id_str';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (segment.length >= 20 && /[-_]/.test(segment)) {
|
|
46
|
+
return ':id_str';
|
|
47
|
+
}
|
|
48
|
+
return segment;
|
|
49
|
+
});
|
|
50
|
+
routePath = segments.join('/');
|
|
51
|
+
}
|
|
52
|
+
if (!routePath || routePath === 'UNKNOWN') {
|
|
53
|
+
routePath = 'RAW_PATH_UNAVAILABLE';
|
|
54
|
+
}
|
|
55
|
+
// If behind a proxy/CDN, enable: app.set('trust proxy', true)
|
|
56
|
+
let ip = req.ip;
|
|
57
|
+
if (ip) {
|
|
58
|
+
ip = ip.trim();
|
|
59
|
+
}
|
|
60
|
+
if (!ip || ip.length === 0)
|
|
61
|
+
ip = undefined;
|
|
62
|
+
const authHeaderName = (client.config.authHeaderName || 'authorization').toLowerCase();
|
|
63
|
+
const authHeader = req.headers[authHeaderName];
|
|
64
|
+
let authToken;
|
|
65
|
+
if (authHeader) {
|
|
66
|
+
let raw = Array.isArray(authHeader) ? authHeader[0] : authHeader;
|
|
67
|
+
if (raw.toLowerCase().startsWith('bearer ')) {
|
|
68
|
+
raw = raw.substring(7).trim();
|
|
69
|
+
}
|
|
70
|
+
if (raw.toLowerCase().startsWith('token ')) {
|
|
71
|
+
raw = raw.substring(6).trim();
|
|
72
|
+
}
|
|
73
|
+
if (raw.length > 0) {
|
|
74
|
+
authToken = raw;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const userAgent = req.headers['user-agent'];
|
|
78
|
+
const endTime = process.hrtime.bigint();
|
|
79
|
+
const latencyMs = Number((endTime - startTime) / BigInt(1000000));
|
|
80
|
+
client.recordRequest({
|
|
81
|
+
serviceName: client.config.serviceName,
|
|
82
|
+
method: method,
|
|
83
|
+
route: routePath,
|
|
84
|
+
status: statusCode,
|
|
85
|
+
ip: ip,
|
|
86
|
+
userAgent: userAgent,
|
|
87
|
+
authToken: authToken,
|
|
88
|
+
aborted: aborted,
|
|
89
|
+
latencyMs: latencyMs,
|
|
90
|
+
timestamp: Date.now()
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (e) { }
|
|
94
|
+
};
|
|
95
|
+
res.once('finish', record);
|
|
96
|
+
res.once('close', record);
|
|
97
|
+
next();
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function warning(config) {
|
|
101
|
+
return warningMiddleware(config);
|
|
102
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "warning_sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"example:simple-server": "ts-node examples/simple-server.ts",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [],
|
|
16
|
+
"author": "",
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"type": "commonjs",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@nestjs/common": "^11.1.9",
|
|
21
|
+
"@nestjs/core": "^11.1.9",
|
|
22
|
+
"@nestjs/platform-express": "^11.1.9",
|
|
23
|
+
"@types/express": "^5.0.6",
|
|
24
|
+
"@types/node": "^25.0.3",
|
|
25
|
+
"@types/react": "^19.2.7",
|
|
26
|
+
"@types/react-dom": "^19.2.3",
|
|
27
|
+
"fastify": "^5.6.2",
|
|
28
|
+
"fastify-plugin": "^5.1.0",
|
|
29
|
+
"next": "^16.1.0",
|
|
30
|
+
"react": "^19.2.3",
|
|
31
|
+
"react-dom": "^19.2.3",
|
|
32
|
+
"reflect-metadata": "^0.2.2",
|
|
33
|
+
"rxjs": "^7.8.2",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"typescript": "^5.9.3",
|
|
36
|
+
"express": "^5.2.1"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"express": ">=4"
|
|
40
|
+
}
|
|
41
|
+
}
|