@logbrew/sdk 0.1.38 → 0.1.39
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/automatic-delivery.cjs +271 -0
- package/batch-sender.cjs +164 -0
- package/bounded-number.cjs +34 -0
- package/browser-breadcrumbs.cjs +346 -136
- package/capture-support.cjs +100 -0
- package/client-options.cjs +292 -0
- package/client-report.cjs +232 -0
- package/client.cjs +447 -0
- package/console-arguments.cjs +151 -0
- package/console-capture.cjs +194 -0
- package/core.cjs +30 -2810
- package/delivery-health.cjs +133 -0
- package/error-causes.cjs +130 -0
- package/error-details.cjs +124 -0
- package/error-issue.cjs +187 -0
- package/event-clone.cjs +55 -0
- package/event-metadata.cjs +40 -0
- package/event-pipeline.cjs +325 -0
- package/event-queue.cjs +306 -0
- package/event-store.cjs +237 -0
- package/event-validation.cjs +159 -0
- package/examples/agent-timeline.cjs +57 -46
- package/examples/agent-timeline.mjs +41 -35
- package/examples/index.mjs +122 -94
- package/examples/readme-example.cjs +36 -27
- package/examples/readme-example.mjs +21 -17
- package/examples/real-user-smoke.cjs +41 -32
- package/examples/real-user-smoke.mjs +26 -22
- package/exception-chain.cjs +205 -0
- package/index.d.ts +144 -1076
- package/index.js +1 -1
- package/issue-exception.cjs +13 -8
- package/metric-validation.cjs +95 -0
- package/network-milestone.cjs +155 -0
- package/null-value.cjs +4 -0
- package/opentelemetry-bridge.cjs +330 -0
- package/opentelemetry-context.cjs +245 -0
- package/opentelemetry-exceptions.cjs +69 -0
- package/opentelemetry-keys.cjs +183 -0
- package/opentelemetry-span.cjs +286 -0
- package/opentelemetry-summary.cjs +213 -0
- package/opentelemetry-time.cjs +131 -0
- package/opentelemetry.cjs +99 -1068
- package/package.json +55 -1
- package/pino-destination.cjs +108 -0
- package/pino-record.cjs +237 -0
- package/product-action.cjs +163 -0
- package/provided-breadcrumbs.cjs +117 -0
- package/random-hex.cjs +29 -0
- package/react-native.js +1 -1
- package/recording-transport.cjs +68 -0
- package/release-artifacts-build.cjs +263 -84
- package/release-artifacts-common.js +347 -66
- package/release-artifacts-debug-id.js +178 -0
- package/release-artifacts-inspect.js +213 -0
- package/release-artifacts-issue-event.js +99 -0
- package/release-artifacts-manifest-artifact.js +107 -0
- package/release-artifacts-manifest.js +77 -0
- package/release-artifacts-mappings.js +265 -0
- package/release-artifacts-prepare.js +297 -0
- package/release-artifacts-source-context.js +210 -0
- package/release-artifacts-symbolication.js +272 -421
- package/release-artifacts-upload-files.js +122 -0
- package/release-artifacts-upload-request.js +246 -0
- package/release-artifacts-upload.js +155 -300
- package/release-artifacts-urls.js +49 -0
- package/release-artifacts.js +189 -662
- package/retry-delay.cjs +32 -0
- package/span-validation.cjs +191 -0
- package/stored-events.cjs +243 -0
- package/types/client.d.ts +67 -0
- package/types/delivery.d.ts +125 -0
- package/types/issues.d.ts +249 -0
- package/types/logging.d.ts +175 -0
- package/types/opentelemetry.d.ts +205 -0
- package/types/product.d.ts +128 -0
- package/types/recording-transport.d.ts +15 -0
- package/types/sdk-error.d.ts +9 -0
- package/types/telemetry.d.ts +171 -0
- package/types/trace-context.d.ts +110 -0
- package/types/transport-error.d.ts +10 -0
- package/utf8.cjs +37 -0
- package/validation.cjs +49 -1
- package/vite-release-artifacts-config.cjs +120 -0
- package/vite-release-artifacts.cjs +199 -99
- package/vite-release-artifacts.d.cts +65 -40
- package/vite-release-artifacts.d.ts +65 -40
- package/vite-release-artifacts.js +2 -2
- package/winston-info.cjs +205 -0
- package/winston.cjs +189 -289
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
const { BatchSender } = require("./batch-sender.cjs");
|
|
2
|
+
const { addBounded, incrementBounded, nextBoundedTimestamp } = require("./bounded-number.cjs");
|
|
3
|
+
const { DropLedger } = require("./client-report.cjs");
|
|
4
|
+
const { cloneEvent } = require("./event-clone.cjs");
|
|
5
|
+
const { EventQueue, isFatalEvent } = require("./event-queue.cjs");
|
|
6
|
+
const { isClientReportStore } = require("./event-store.cjs");
|
|
7
|
+
const { utf8ByteLength } = require("./utf8.cjs");
|
|
8
|
+
const { isCallable } = require("./validation.cjs");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @import { BatchTransport } from "./batch-sender.cjs"
|
|
12
|
+
* @import { DropReason } from "./client-report.cjs"
|
|
13
|
+
* @import { SdkEvent } from "./event-validation.cjs"
|
|
14
|
+
* @import { Batch, EventQueueConfig, OverflowReason } from "./event-queue.cjs"
|
|
15
|
+
* @import { ClientReportStore } from "./event-store.cjs"
|
|
16
|
+
* @import { HealthState } from "./delivery-health.cjs"
|
|
17
|
+
* @import { Callable, DeepReadonly } from "./validation.cjs"
|
|
18
|
+
* @typedef {Readonly<{ status: string, reason?: DropReason }>} EventAdmission
|
|
19
|
+
* @typedef {Readonly<{ eventFilter: () => unknown, onEventDropped: () => unknown, receiver: unknown }>} PipelineHooks
|
|
20
|
+
* @typedef {Readonly<{ statusCode: number, attempts: number, batches: number }>} FlushResult
|
|
21
|
+
* @typedef {"idle" | "empty" | "accepted" | "failed"} DeliveryOutcome
|
|
22
|
+
* @typedef {EventQueueConfig & Readonly<{
|
|
23
|
+
* apiKey: () => string,
|
|
24
|
+
* clientReportStore: ClientReportStore | undefined,
|
|
25
|
+
* maxBatchEvents: number,
|
|
26
|
+
* maxRetries: number,
|
|
27
|
+
* hooks: PipelineHooks
|
|
28
|
+
* }>} PipelineConfig
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {DropReason} reason
|
|
33
|
+
* @returns {EventAdmission}
|
|
34
|
+
*/
|
|
35
|
+
function droppedAdmission(reason) {
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
status: "dropped",
|
|
38
|
+
|
|
39
|
+
reason
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const EVENT_ADMISSIONS = Object.freeze({
|
|
44
|
+
admitted: Object.freeze({ status: "admitted" }),
|
|
45
|
+
|
|
46
|
+
filtered: Object.freeze({ status: "filtered" }),
|
|
47
|
+
|
|
48
|
+
unavailable: Object.freeze({ status: "unavailable" }),
|
|
49
|
+
|
|
50
|
+
event_too_large: droppedAdmission("event_too_large"),
|
|
51
|
+
|
|
52
|
+
queue_overflow: droppedAdmission("queue_overflow"),
|
|
53
|
+
|
|
54
|
+
queue_bytes_overflow: droppedAdmission("queue_bytes_overflow")
|
|
55
|
+
});
|
|
56
|
+
const NO_CONTENT = 204;
|
|
57
|
+
const NO_EVENTS = 0;
|
|
58
|
+
const NO_OPERATIONS = 0;
|
|
59
|
+
const STEP = 1;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {Readonly<Promise<unknown>>} operation
|
|
63
|
+
* @returns {Promise<boolean>}
|
|
64
|
+
*/
|
|
65
|
+
async function settled(operation) {
|
|
66
|
+
try {
|
|
67
|
+
await operation;
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {Callable} callback
|
|
76
|
+
* @param {unknown} receiver
|
|
77
|
+
* @param {unknown} argument
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function notify(callback, receiver, argument) {
|
|
81
|
+
try {
|
|
82
|
+
Reflect.apply(callback, receiver, [argument]);
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class EventPipeline {
|
|
90
|
+
/** @type {Promise<boolean>} */
|
|
91
|
+
operationTail = Promise.resolve(true);
|
|
92
|
+
|
|
93
|
+
pendingOperations = NO_OPERATIONS;
|
|
94
|
+
|
|
95
|
+
deliveryInFlight = false;
|
|
96
|
+
|
|
97
|
+
stats = {
|
|
98
|
+
acceptedBatchCount: NO_EVENTS,
|
|
99
|
+
acceptedEventCount: NO_EVENTS,
|
|
100
|
+
failedFlushCount: NO_EVENTS,
|
|
101
|
+
lastAcceptedAtUnixMs: NO_EVENTS,
|
|
102
|
+
successfulFlushCount: NO_EVENTS
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/** @type {DeliveryOutcome} */
|
|
106
|
+
lastDeliveryOutcome = "idle";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {PipelineConfig} config
|
|
110
|
+
*/
|
|
111
|
+
constructor(config) {
|
|
112
|
+
this.hooks = config.hooks;
|
|
113
|
+
this.maxBatchEvents = config.maxBatchEvents;
|
|
114
|
+
this.queue = new EventQueue(config);
|
|
115
|
+
const { eventStore } = this.queue;
|
|
116
|
+
this.ledger = new DropLedger(config.clientReportStore ?? (isClientReportStore(eventStore) ? eventStore : undefined));
|
|
117
|
+
this.sender = new BatchSender(config);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @returns {Omit<HealthState, "automaticDelivery" | "lifecycle" | "scheduled" | "coalesced" | "pausedReason" | "consecutiveFailures" | "retryDelayMs">}
|
|
122
|
+
*/
|
|
123
|
+
health() {
|
|
124
|
+
const { ledger, queue, sender, stats } = this;
|
|
125
|
+
return {
|
|
126
|
+
acceptedEvents: stats.acceptedEventCount,
|
|
127
|
+
attempts: sender.deliveryAttemptCount,
|
|
128
|
+
batches: stats.acceptedBatchCount,
|
|
129
|
+
droppedByReason: ledger.droppedEventsByReason,
|
|
130
|
+
droppedEvents: ledger.droppedEventCount,
|
|
131
|
+
failures: stats.failedFlushCount,
|
|
132
|
+
flushes: stats.successfulFlushCount,
|
|
133
|
+
hydratedBytes: queue.hydrated.bytes,
|
|
134
|
+
hydratedEvents: queue.hydrated.events,
|
|
135
|
+
inFlight: this.deliveryInFlight,
|
|
136
|
+
lastAcceptedAtUnixMs: stats.lastAcceptedAtUnixMs,
|
|
137
|
+
lastAttemptAtUnixMs: sender.lastAttemptAtUnixMs,
|
|
138
|
+
lastDropReason: ledger.lastDropReason,
|
|
139
|
+
lastDroppedAtUnixMs: ledger.lastDroppedAtUnixMs,
|
|
140
|
+
lastOutcome: this.lastDeliveryOutcome,
|
|
141
|
+
lastStatusClass: sender.lastStatusClass,
|
|
142
|
+
pendingOperations: this.pendingOperations,
|
|
143
|
+
queueBytes: queue.queuedEventBytes,
|
|
144
|
+
queueEvents: queue.events.length,
|
|
145
|
+
storage: queue.eventStore === undefined ? "memory" : "persistent"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {DeepReadonly<SdkEvent>} event
|
|
151
|
+
* @returns {EventAdmission}
|
|
152
|
+
*/
|
|
153
|
+
admit(event) {
|
|
154
|
+
const eventFilter = this.hooks.eventFilter();
|
|
155
|
+
if (isCallable(eventFilter) && Reflect.apply(eventFilter, this.hooks.receiver, [cloneEvent(event)]) === false) {
|
|
156
|
+
return EVENT_ADMISSIONS.filtered;
|
|
157
|
+
}
|
|
158
|
+
const serializedEvent = JSON.stringify(event);
|
|
159
|
+
const eventBytes = utf8ByteLength(serializedEvent);
|
|
160
|
+
const { queue } = this;
|
|
161
|
+
if (queue.limits.batchPrefixBytes + eventBytes + queue.limits.batchSuffixBytes > queue.limits.maxBatchBytes) {
|
|
162
|
+
this.recordDroppedEvent(event, "event_too_large");
|
|
163
|
+
return EVENT_ADMISSIONS.event_too_large;
|
|
164
|
+
}
|
|
165
|
+
return this.enqueue(event, serializedEvent, eventBytes);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {DeepReadonly<SdkEvent>} event
|
|
170
|
+
* @param {number} eventBytes
|
|
171
|
+
* @returns {Readonly<{ evicted: DeepReadonly<SdkEvent>[], overflow: OverflowReason | undefined }>}
|
|
172
|
+
*/
|
|
173
|
+
makeRoom(event, eventBytes) {
|
|
174
|
+
const { queue } = this;
|
|
175
|
+
const fatal = isFatalEvent(event);
|
|
176
|
+
if (fatal) {
|
|
177
|
+
queue.synchronize();
|
|
178
|
+
}
|
|
179
|
+
const overflow = queue.overflowReason(queue.events.length, queue.queuedEventBytes + eventBytes);
|
|
180
|
+
return { evicted: overflow !== undefined && fatal ? queue.evictForFatal(eventBytes) : [], overflow };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {DeepReadonly<SdkEvent>} event
|
|
185
|
+
* @param {string} serializedEvent
|
|
186
|
+
* @param {number} eventBytes
|
|
187
|
+
* @returns {EventAdmission}
|
|
188
|
+
*/
|
|
189
|
+
enqueue(event, serializedEvent, eventBytes) {
|
|
190
|
+
const { evicted, overflow } = this.makeRoom(event, eventBytes);
|
|
191
|
+
if (overflow !== undefined && evicted.length === NO_EVENTS) {
|
|
192
|
+
this.recordDroppedEvent(event, overflow);
|
|
193
|
+
return EVENT_ADMISSIONS[overflow];
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
this.queue.append(event, serializedEvent, eventBytes);
|
|
197
|
+
} finally {
|
|
198
|
+
for (const evictedEvent of evicted) {
|
|
199
|
+
this.recordDroppedEvent(evictedEvent, "evicted_for_fatal");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return EVENT_ADMISSIONS.admitted;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {DeepReadonly<SdkEvent>} event
|
|
207
|
+
* @param {DropReason} reason
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
recordDroppedEvent(event, reason) {
|
|
211
|
+
this.ledger.record(reason);
|
|
212
|
+
const onEventDropped = this.hooks.onEventDropped();
|
|
213
|
+
return isCallable(onEventDropped) && notify(onEventDropped, this.hooks.receiver, {
|
|
214
|
+
droppedEvents: this.ledger.droppedEventCount,
|
|
215
|
+
eventId: event.id,
|
|
216
|
+
eventType: event.type,
|
|
217
|
+
reason
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @template T
|
|
223
|
+
* @param {() => Promise<T>} operation
|
|
224
|
+
* @returns {Promise<T>}
|
|
225
|
+
*/
|
|
226
|
+
async runSerialized(operation) {
|
|
227
|
+
this.pendingOperations = incrementBounded(this.pendingOperations);
|
|
228
|
+
const result = this.afterTail(operation);
|
|
229
|
+
this.operationTail = settled(result);
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @template T
|
|
235
|
+
* @param {() => Promise<T>} operation
|
|
236
|
+
* @returns {Promise<T>}
|
|
237
|
+
*/
|
|
238
|
+
async afterTail(operation) {
|
|
239
|
+
await this.operationTail;
|
|
240
|
+
try {
|
|
241
|
+
return await operation();
|
|
242
|
+
} finally {
|
|
243
|
+
this.pendingOperations = Math.max(NO_OPERATIONS, this.pendingOperations - STEP);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {BatchTransport} transport
|
|
249
|
+
* @returns {Promise<FlushResult>}
|
|
250
|
+
*/
|
|
251
|
+
async flushWithHealth(transport) {
|
|
252
|
+
this.deliveryInFlight = true;
|
|
253
|
+
try {
|
|
254
|
+
const response = await this.flushSnapshot(transport);
|
|
255
|
+
this.stats.successfulFlushCount = incrementBounded(this.stats.successfulFlushCount);
|
|
256
|
+
this.lastDeliveryOutcome = response.batches === NO_EVENTS ? "empty" : "accepted";
|
|
257
|
+
return response;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
this.stats.failedFlushCount = incrementBounded(this.stats.failedFlushCount);
|
|
260
|
+
this.lastDeliveryOutcome = "failed";
|
|
261
|
+
throw error;
|
|
262
|
+
} finally {
|
|
263
|
+
this.deliveryInFlight = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* @param {BatchTransport} transport
|
|
269
|
+
* @returns {Promise<FlushResult>}
|
|
270
|
+
*/
|
|
271
|
+
async flushSnapshot(transport) {
|
|
272
|
+
this.queue.synchronize();
|
|
273
|
+
const totals = {
|
|
274
|
+
statusCode: NO_CONTENT,
|
|
275
|
+
|
|
276
|
+
attempts: 0,
|
|
277
|
+
|
|
278
|
+
batches: 0
|
|
279
|
+
};
|
|
280
|
+
for (let remainingEvents = this.queue.events.length; remainingEvents > NO_EVENTS;) {
|
|
281
|
+
const batch = this.currentBatch(remainingEvents);
|
|
282
|
+
const response = await this.sender.send(transport, batch.body);
|
|
283
|
+
remainingEvents -= this.settleBatch(batch);
|
|
284
|
+
totals.attempts += response.attempts;
|
|
285
|
+
totals.batches += STEP;
|
|
286
|
+
totals.statusCode = response.statusCode;
|
|
287
|
+
}
|
|
288
|
+
return totals;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @param {number} remainingEvents
|
|
293
|
+
* @returns {Batch}
|
|
294
|
+
*/
|
|
295
|
+
currentBatch(remainingEvents) {
|
|
296
|
+
const { queue } = this;
|
|
297
|
+
const batch = queue.unacknowledgedBatch ?? Object.freeze(queue.nextBatch(
|
|
298
|
+
Math.min(remainingEvents, queue.events.length, this.maxBatchEvents),
|
|
299
|
+
this.ledger.pendingReport()
|
|
300
|
+
));
|
|
301
|
+
queue.unacknowledgedBatch = batch;
|
|
302
|
+
return batch;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {Batch} batch
|
|
307
|
+
* @returns {number}
|
|
308
|
+
*/
|
|
309
|
+
settleBatch(batch) {
|
|
310
|
+
this.queue.unacknowledgedBatch = undefined;
|
|
311
|
+
if (batch.carriesClientReport) {
|
|
312
|
+
this.ledger.acknowledgeReport();
|
|
313
|
+
}
|
|
314
|
+
this.queue.acknowledge(batch.eventsCount);
|
|
315
|
+
this.stats.acceptedEventCount = addBounded(this.stats.acceptedEventCount, batch.eventsCount);
|
|
316
|
+
this.stats.lastAcceptedAtUnixMs = nextBoundedTimestamp(Math.max(
|
|
317
|
+
this.stats.lastAcceptedAtUnixMs,
|
|
318
|
+
this.sender.lastAttemptAtUnixMs
|
|
319
|
+
));
|
|
320
|
+
this.stats.acceptedBatchCount = incrementBounded(this.stats.acceptedBatchCount);
|
|
321
|
+
return batch.eventsCount;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = { EVENT_ADMISSIONS, EventPipeline };
|
package/event-queue.cjs
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const { cloneEvent } = require("./event-clone.cjs");
|
|
2
|
+
const { createEventStoreFromQueueFactory, loadStoredEvents } = require("./event-store.cjs");
|
|
3
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
4
|
+
const { isOrderedSubsequence, requireSynchronousStoreResult } = require("./stored-events.cjs");
|
|
5
|
+
const { utf8ByteLength } = require("./utf8.cjs");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @import { ClientReport } from "./client-report.cjs"
|
|
9
|
+
* @import { SdkEvent } from "./event-validation.cjs"
|
|
10
|
+
* @import { EventStore, StoreLimits } from "./event-store.cjs"
|
|
11
|
+
* @typedef {ReturnType<typeof loadStoredEvents>} RecoveredEvents
|
|
12
|
+
* @import { Callable, DeepReadonly } from "./validation.cjs"
|
|
13
|
+
* @typedef {Readonly<{ body: string, carriesClientReport: boolean, eventsCount: number }>} Batch
|
|
14
|
+
* @typedef {"queue_overflow" | "queue_bytes_overflow"} OverflowReason
|
|
15
|
+
* @typedef {Readonly<{
|
|
16
|
+
* eventStore: EventStore | undefined,
|
|
17
|
+
* eventQueueFactory: Callable | undefined,
|
|
18
|
+
* sdk: Readonly<{ name: string, language: string, version: string }>,
|
|
19
|
+
* maxBatchBytes: number,
|
|
20
|
+
* maxQueueBytes: number,
|
|
21
|
+
* maxQueueSize: number
|
|
22
|
+
* }>} EventQueueConfig
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const BATCH_SUFFIX = "]}";
|
|
26
|
+
const BATCH_SUFFIX_BYTES = 2;
|
|
27
|
+
const PREVIEW_INDENT = 2;
|
|
28
|
+
const FIRST_INDEX = 0;
|
|
29
|
+
const SEPARATOR_BYTES = 1;
|
|
30
|
+
const NO_BYTES = 0;
|
|
31
|
+
const NO_EVENTS = 0;
|
|
32
|
+
const SINGLE = 1;
|
|
33
|
+
const PREVIOUS = 1;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {DeepReadonly<SdkEvent> | undefined} event
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
function isFatalEvent(event) {
|
|
40
|
+
return event?.type === "issue" && event.attributes["level"] === "critical";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {EventStore} store
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
function closeOwnedStore(store) {
|
|
48
|
+
try {
|
|
49
|
+
requireSynchronousStoreResult("close", store.close());
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {EventQueueConfig} config
|
|
58
|
+
* @param {StoreLimits} limits
|
|
59
|
+
* @returns {Readonly<{ contents: RecoveredEvents, eventStore: EventStore | undefined }>}
|
|
60
|
+
*/
|
|
61
|
+
function openEventStore(config, limits) {
|
|
62
|
+
const { eventQueueFactory } = config;
|
|
63
|
+
const eventStore = eventQueueFactory === undefined
|
|
64
|
+
? config.eventStore
|
|
65
|
+
: createEventStoreFromQueueFactory(eventQueueFactory, limits);
|
|
66
|
+
try {
|
|
67
|
+
return { contents: loadStoredEvents(eventStore, limits), eventStore };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (eventQueueFactory !== undefined && eventStore !== undefined) {
|
|
70
|
+
closeOwnedStore(eventStore);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class EventQueue {
|
|
77
|
+
/**
|
|
78
|
+
* @param {EventQueueConfig} config
|
|
79
|
+
*/
|
|
80
|
+
constructor(config) {
|
|
81
|
+
this.sdk = config.sdk;
|
|
82
|
+
this.batchPrefix = `{"sdk":${JSON.stringify(config.sdk)},"events":[`;
|
|
83
|
+
/** @type {StoreLimits} */
|
|
84
|
+
this.limits = Object.freeze({
|
|
85
|
+
batchPrefixBytes: utf8ByteLength(this.batchPrefix),
|
|
86
|
+
batchSuffixBytes: BATCH_SUFFIX_BYTES,
|
|
87
|
+
maxBatchBytes: config.maxBatchBytes,
|
|
88
|
+
maxQueueBytes: config.maxQueueBytes,
|
|
89
|
+
maxQueueSize: config.maxQueueSize
|
|
90
|
+
});
|
|
91
|
+
const { contents, eventStore } = openEventStore(config, this.limits);
|
|
92
|
+
this.eventStore = eventStore;
|
|
93
|
+
this.contents = contents;
|
|
94
|
+
this.hydrated = Object.freeze({ bytes: contents.queuedEventBytes, events: contents.events.length });
|
|
95
|
+
/** @type {Batch | undefined} */
|
|
96
|
+
this.unacknowledgedBatch = undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @returns {DeepReadonly<SdkEvent>[]}
|
|
101
|
+
*/
|
|
102
|
+
get events() {
|
|
103
|
+
return this.contents.events;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @returns {number}
|
|
108
|
+
*/
|
|
109
|
+
get queuedEventBytes() {
|
|
110
|
+
return this.contents.queuedEventBytes;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
synchronize() {
|
|
117
|
+
if (this.eventStore === undefined) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const recovered = loadStoredEvents(this.eventStore, this.limits);
|
|
121
|
+
if (!isOrderedSubsequence(this.contents.serializedEvents, recovered.serializedEvents)) {
|
|
122
|
+
throw new SdkError("persistence_error", "event store lost a queued record");
|
|
123
|
+
}
|
|
124
|
+
this.contents = recovered;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {number} count
|
|
129
|
+
* @param {number} bytes
|
|
130
|
+
* @returns {OverflowReason | undefined}
|
|
131
|
+
*/
|
|
132
|
+
overflowReason(count, bytes) {
|
|
133
|
+
if (count >= this.limits.maxQueueSize) {
|
|
134
|
+
return "queue_overflow";
|
|
135
|
+
}
|
|
136
|
+
return bytes > this.limits.maxQueueBytes ? "queue_bytes_overflow" : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {number} eventBytes
|
|
141
|
+
* @returns {number[] | undefined}
|
|
142
|
+
*/
|
|
143
|
+
evictionIndexes(eventBytes) {
|
|
144
|
+
const indexes = [];
|
|
145
|
+
const { events, serializedEventBytes } = this.contents;
|
|
146
|
+
let count = events.length;
|
|
147
|
+
let bytes = this.contents.queuedEventBytes + eventBytes;
|
|
148
|
+
for (
|
|
149
|
+
let index = this.unacknowledgedBatch?.eventsCount ?? FIRST_INDEX;
|
|
150
|
+
index < events.length && this.overflowReason(count, bytes) !== undefined;
|
|
151
|
+
index += SINGLE
|
|
152
|
+
) {
|
|
153
|
+
if (!isFatalEvent(events[index])) {
|
|
154
|
+
indexes.push(index);
|
|
155
|
+
count -= SINGLE;
|
|
156
|
+
bytes -= serializedEventBytes[index] ?? NO_BYTES;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return this.overflowReason(count, bytes) === undefined ? indexes : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {number} eventBytes
|
|
164
|
+
* @returns {DeepReadonly<SdkEvent>[]}
|
|
165
|
+
*/
|
|
166
|
+
evictForFatal(eventBytes) {
|
|
167
|
+
const { eventStore } = this;
|
|
168
|
+
if (eventStore !== undefined && typeof eventStore.remove !== "function") {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const indexes = this.evictionIndexes(eventBytes);
|
|
172
|
+
if (indexes === undefined) {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
if (eventStore !== undefined) {
|
|
176
|
+
requireSynchronousStoreResult("remove", eventStore.remove?.(indexes));
|
|
177
|
+
}
|
|
178
|
+
return this.removeIndexes(indexes);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @param {readonly number[]} indexes
|
|
183
|
+
* @returns {DeepReadonly<SdkEvent>[]}
|
|
184
|
+
*/
|
|
185
|
+
removeIndexes(indexes) {
|
|
186
|
+
const evicted = indexes.map((index) => this.contents.events[index]).filter((event) => event !== undefined);
|
|
187
|
+
for (let position = indexes.length - PREVIOUS; position >= FIRST_INDEX; position -= SINGLE) {
|
|
188
|
+
this.removeAt(indexes[position] ?? FIRST_INDEX);
|
|
189
|
+
}
|
|
190
|
+
return evicted;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {number} index
|
|
195
|
+
* @returns {void}
|
|
196
|
+
*/
|
|
197
|
+
removeAt(index) {
|
|
198
|
+
const { contents } = this;
|
|
199
|
+
contents.events.splice(index, SINGLE);
|
|
200
|
+
contents.serializedEvents.splice(index, SINGLE);
|
|
201
|
+
contents.queuedEventBytes -= contents.serializedEventBytes.splice(index, SINGLE)[FIRST_INDEX] ?? NO_BYTES;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {DeepReadonly<SdkEvent>} event
|
|
206
|
+
* @param {string} serializedEvent
|
|
207
|
+
* @param {number} eventBytes
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
append(event, serializedEvent, eventBytes) {
|
|
211
|
+
if (this.eventStore !== undefined) {
|
|
212
|
+
requireSynchronousStoreResult("append", this.eventStore.append({ event: cloneEvent(event), eventBytes, serializedEvent }));
|
|
213
|
+
}
|
|
214
|
+
const { contents } = this;
|
|
215
|
+
contents.events.push(event);
|
|
216
|
+
contents.serializedEvents.push(serializedEvent);
|
|
217
|
+
contents.serializedEventBytes.push(eventBytes);
|
|
218
|
+
contents.queuedEventBytes += eventBytes;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {number} eventsCount
|
|
223
|
+
* @returns {void}
|
|
224
|
+
*/
|
|
225
|
+
acknowledge(eventsCount) {
|
|
226
|
+
if (this.eventStore !== undefined) {
|
|
227
|
+
requireSynchronousStoreResult("acknowledge", this.eventStore.acknowledge(eventsCount));
|
|
228
|
+
}
|
|
229
|
+
const { contents } = this;
|
|
230
|
+
const acknowledgedBytes = contents.serializedEventBytes.slice(FIRST_INDEX, eventsCount).reduce((sum, bytes) => sum + bytes, NO_BYTES);
|
|
231
|
+
contents.events.splice(FIRST_INDEX, eventsCount);
|
|
232
|
+
contents.serializedEvents.splice(FIRST_INDEX, eventsCount);
|
|
233
|
+
contents.serializedEventBytes.splice(FIRST_INDEX, eventsCount);
|
|
234
|
+
contents.queuedEventBytes -= acknowledgedBytes;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @returns {number}
|
|
239
|
+
*/
|
|
240
|
+
purge() {
|
|
241
|
+
const { contents } = this;
|
|
242
|
+
const purgedEvents = contents.events.length;
|
|
243
|
+
if (this.eventStore !== undefined) {
|
|
244
|
+
requireSynchronousStoreResult("purge", this.eventStore.purge());
|
|
245
|
+
}
|
|
246
|
+
this.unacknowledgedBatch = undefined;
|
|
247
|
+
contents.events.splice(FIRST_INDEX);
|
|
248
|
+
contents.serializedEvents.splice(FIRST_INDEX);
|
|
249
|
+
contents.serializedEventBytes.splice(FIRST_INDEX);
|
|
250
|
+
contents.queuedEventBytes = 0;
|
|
251
|
+
return purgedEvents;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @returns {string}
|
|
256
|
+
*/
|
|
257
|
+
previewJson() {
|
|
258
|
+
return JSON.stringify({
|
|
259
|
+
sdk: this.sdk,
|
|
260
|
+
|
|
261
|
+
events: this.contents.events
|
|
262
|
+
}, undefined, PREVIEW_INDENT);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {number} maxEvents
|
|
267
|
+
* @param {ClientReport | undefined} report
|
|
268
|
+
* @returns {Batch}
|
|
269
|
+
*/
|
|
270
|
+
nextBatch(maxEvents, report) {
|
|
271
|
+
const reportSuffix = report === undefined ? undefined : `],"client_report":${JSON.stringify(report)}}`;
|
|
272
|
+
const reportedEvents = reportSuffix === undefined ? NO_EVENTS : this.batchEventCount(maxEvents, utf8ByteLength(reportSuffix));
|
|
273
|
+
const eventsCount = reportedEvents === NO_EVENTS ? this.batchEventCount(maxEvents, BATCH_SUFFIX_BYTES) : reportedEvents;
|
|
274
|
+
if (eventsCount === NO_EVENTS) {
|
|
275
|
+
throw new SdkError("transport_error", "queued event cannot fit the configured batch byte limit");
|
|
276
|
+
}
|
|
277
|
+
const suffix = reportedEvents === NO_EVENTS ? BATCH_SUFFIX : reportSuffix ?? BATCH_SUFFIX;
|
|
278
|
+
return {
|
|
279
|
+
body: `${this.batchPrefix}${this.contents.serializedEvents.slice(FIRST_INDEX, eventsCount).join(",")}${suffix}`,
|
|
280
|
+
carriesClientReport: reportedEvents > NO_EVENTS,
|
|
281
|
+
eventsCount
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @param {number} maxEvents
|
|
287
|
+
* @param {number} suffixBytes
|
|
288
|
+
* @returns {number}
|
|
289
|
+
*/
|
|
290
|
+
batchEventCount(maxEvents, suffixBytes) {
|
|
291
|
+
const { events, serializedEventBytes } = this.contents;
|
|
292
|
+
let bodyBytes = this.limits.batchPrefixBytes + suffixBytes;
|
|
293
|
+
const eventIds = new Set();
|
|
294
|
+
for (let index = FIRST_INDEX; index < maxEvents; index += SINGLE) {
|
|
295
|
+
const event = events[index];
|
|
296
|
+
bodyBytes += (index === FIRST_INDEX ? NO_BYTES : SEPARATOR_BYTES) + (serializedEventBytes[index] ?? NO_BYTES);
|
|
297
|
+
if (event === undefined || eventIds.has(event.id) || bodyBytes > this.limits.maxBatchBytes) {
|
|
298
|
+
return index;
|
|
299
|
+
}
|
|
300
|
+
eventIds.add(event.id);
|
|
301
|
+
}
|
|
302
|
+
return maxEvents;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { EventQueue, isFatalEvent };
|