@logbrew/sdk 0.1.37 → 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 +35 -2984
- 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-breadcrumbs.cjs +143 -0
- package/issue-diagnostics.cjs +94 -582
- package/issue-evidence.cjs +256 -0
- package/issue-exception.cjs +268 -0
- package/issue-stack-frames.cjs +220 -0
- package/issue-stack-text.cjs +101 -0
- package/issue-stack.cjs +188 -225
- package/issue-text.cjs +72 -0
- package/log-context.cjs +87 -67
- 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 +64 -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/sdk-error.cjs +36 -0
- package/span-validation.cjs +191 -0
- package/stored-events.cjs +243 -0
- package/support-ticket.cjs +232 -122
- package/telemetry-context.cjs +343 -251
- package/trace-context.cjs +376 -179
- package/transport-error.cjs +23 -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 +327 -0
- 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 -290
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
const { isClientReportStore, validateEventStore } = require("./event-store.cjs");
|
|
2
|
+
const { MAX_DELIVERY_INTERVAL_MS } = require("./retry-delay.cjs");
|
|
3
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
4
|
+
const { cloneTelemetryContext, validateTelemetryContext } = require("./telemetry-context.cjs");
|
|
5
|
+
const { isCallable, isRecord, requireNonEmpty, requireNonNegativeInteger, requirePositiveInteger } = require("./validation.cjs");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @import { Callable, DeepReadonly } from "./validation.cjs"
|
|
9
|
+
* @import { BatchTransport } from "./batch-sender.cjs"
|
|
10
|
+
* @import { ClientReportStore, EventStore } from "./event-store.cjs"
|
|
11
|
+
* @import { TelemetryContext } from "./index.cjs"
|
|
12
|
+
* @typedef {Readonly<Record<PropertyKey, unknown>>} ClientOptions
|
|
13
|
+
* @typedef {Readonly<{
|
|
14
|
+
* apiKey: string,
|
|
15
|
+
* automaticDelivery: boolean,
|
|
16
|
+
* deliveryIntervalMs: number,
|
|
17
|
+
* deliveryQueueThreshold: number,
|
|
18
|
+
* eventStore: EventStore | undefined,
|
|
19
|
+
* eventQueueFactory: Callable | undefined,
|
|
20
|
+
* clientReportStore: ClientReportStore | undefined,
|
|
21
|
+
* eventFilter: Callable | undefined,
|
|
22
|
+
* maxBatchBytes: number,
|
|
23
|
+
* maxBatchEvents: number,
|
|
24
|
+
* maxQueueBytes: number,
|
|
25
|
+
* maxQueueSize: number,
|
|
26
|
+
* onEventDropped: Callable | undefined,
|
|
27
|
+
* context: DeepReadonly<TelemetryContext> | undefined,
|
|
28
|
+
* contextProvider: Callable | undefined,
|
|
29
|
+
* breadcrumbProvider: Callable | undefined,
|
|
30
|
+
* sdk: Readonly<{ name: string, language: string, version: string }>,
|
|
31
|
+
* maxRetries: number,
|
|
32
|
+
* transport: BatchTransport | undefined
|
|
33
|
+
* }>} ClientConfig
|
|
34
|
+
* @typedef {Omit<ClientConfig, "clientReportStore" | "context" | "eventQueueFactory" | "eventStore"> & {
|
|
35
|
+
* context: TelemetryContext | undefined
|
|
36
|
+
* }} ClientSettings
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const EVENT_QUEUE_FACTORY = Symbol.for("@logbrew/sdk.eventQueueFactory");
|
|
40
|
+
const CLIENT_REPORT_STORE = Symbol.for("@logbrew/sdk.clientReportStore");
|
|
41
|
+
const CONTEXT_PROVIDER = Symbol.for("@logbrew/sdk.contextProvider");
|
|
42
|
+
const BREADCRUMB_PROVIDER = Symbol.for("@logbrew/sdk.breadcrumbProvider");
|
|
43
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
44
|
+
const DEFAULT_MAX_QUEUE_SIZE = 1000;
|
|
45
|
+
const DEFAULT_MAX_QUEUE_BYTES = 4_194_304;
|
|
46
|
+
const DEFAULT_MAX_BATCH_EVENTS = 100;
|
|
47
|
+
const DEFAULT_MAX_BATCH_BYTES = 262_144;
|
|
48
|
+
const DEFAULT_DELIVERY_INTERVAL_MS = 1000;
|
|
49
|
+
const DEFAULT_DELIVERY_QUEUE_THRESHOLD = 50;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {unknown} value
|
|
53
|
+
* @param {string} message
|
|
54
|
+
* @returns {Callable | undefined}
|
|
55
|
+
*/
|
|
56
|
+
function optionalFunction(value, message) {
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (!isCallable(value)) {
|
|
61
|
+
throw new SdkError("validation_error", message);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {unknown} maxQueueSize
|
|
68
|
+
* @returns {number}
|
|
69
|
+
*/
|
|
70
|
+
function defaultDeliveryQueueThreshold(maxQueueSize) {
|
|
71
|
+
if (typeof maxQueueSize === "bigint") {
|
|
72
|
+
throw new TypeError("Cannot convert a BigInt value to a number");
|
|
73
|
+
}
|
|
74
|
+
return Math.min(DEFAULT_DELIVERY_QUEUE_THRESHOLD, Number(maxQueueSize));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {unknown} transport
|
|
79
|
+
* @returns {transport is BatchTransport}
|
|
80
|
+
*/
|
|
81
|
+
function isTransport(transport) {
|
|
82
|
+
return isRecord(transport) && typeof transport["send"] === "function";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {unknown} transport
|
|
87
|
+
* @returns {BatchTransport | undefined}
|
|
88
|
+
*/
|
|
89
|
+
function validateTransport(transport) {
|
|
90
|
+
if (transport === undefined) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
if (!isTransport(transport)) {
|
|
94
|
+
throw new SdkError("validation_error", "transport.send must be a function");
|
|
95
|
+
}
|
|
96
|
+
return transport;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {ClientOptions} options
|
|
101
|
+
* @returns {Pick<ClientConfig, "apiKey" | "context" | "contextProvider" | "breadcrumbProvider" | "sdk">}
|
|
102
|
+
*/
|
|
103
|
+
function validateIdentity(options) {
|
|
104
|
+
const apiKey = requireNonEmpty("apiKey", options["apiKey"]);
|
|
105
|
+
const name = requireNonEmpty("sdkName", options["sdkName"]);
|
|
106
|
+
const version = requireNonEmpty("sdkVersion", options["sdkVersion"]);
|
|
107
|
+
const context = validateTelemetryContext(options["context"], "client telemetry context");
|
|
108
|
+
const contextProvider = optionalFunction(options["contextProvider"], "context provider must be a function");
|
|
109
|
+
const breadcrumbProvider = optionalFunction(options["breadcrumbProvider"], "breadcrumb provider must be a function");
|
|
110
|
+
return {
|
|
111
|
+
apiKey,
|
|
112
|
+
breadcrumbProvider,
|
|
113
|
+
context,
|
|
114
|
+
contextProvider,
|
|
115
|
+
sdk: {
|
|
116
|
+
name,
|
|
117
|
+
|
|
118
|
+
language: "javascript",
|
|
119
|
+
|
|
120
|
+
version
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {ClientOptions} options
|
|
127
|
+
* @returns {Pick<ClientConfig, "maxRetries" | "eventFilter" | "maxQueueSize" | "maxQueueBytes" | "maxBatchEvents" | "maxBatchBytes" | "onEventDropped">}
|
|
128
|
+
*/
|
|
129
|
+
function validateLimits(options) {
|
|
130
|
+
const maxRetries = requireNonNegativeInteger("maxRetries", options["maxRetries"]);
|
|
131
|
+
const eventFilter = optionalFunction(options["eventFilter"], "eventFilter must be a function");
|
|
132
|
+
const maxQueueSize = requirePositiveInteger("maxQueueSize", options["maxQueueSize"]);
|
|
133
|
+
const maxQueueBytes = requirePositiveInteger("maxQueueBytes", options["maxQueueBytes"]);
|
|
134
|
+
const maxBatchEvents = requirePositiveInteger("maxBatchEvents", options["maxBatchEvents"]);
|
|
135
|
+
const maxBatchBytes = requirePositiveInteger("maxBatchBytes", options["maxBatchBytes"]);
|
|
136
|
+
const onEventDropped = optionalFunction(options["onEventDropped"], "onEventDropped must be a function");
|
|
137
|
+
return { eventFilter, maxBatchBytes, maxBatchEvents, maxQueueBytes, maxQueueSize, maxRetries, onEventDropped };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {ClientOptions} options
|
|
142
|
+
* @returns {Pick<ClientConfig, "eventStore" | "eventQueueFactory" | "clientReportStore">}
|
|
143
|
+
*/
|
|
144
|
+
function validateStorage(options) {
|
|
145
|
+
const eventStore = validateEventStore(options["eventStore"]);
|
|
146
|
+
const eventQueueFactory = optionalFunction(options["eventQueueFactory"], "event queue factory must be a function");
|
|
147
|
+
if (eventStore !== undefined && eventQueueFactory !== undefined) {
|
|
148
|
+
throw new SdkError("validation_error", "eventStore and event queue factory are mutually exclusive");
|
|
149
|
+
}
|
|
150
|
+
const { clientReportStore } = options;
|
|
151
|
+
if (clientReportStore !== undefined && !isClientReportStore(clientReportStore)) {
|
|
152
|
+
throw new SdkError("validation_error", "client report store must provide loadClientReport and saveClientReport");
|
|
153
|
+
}
|
|
154
|
+
return { clientReportStore, eventQueueFactory, eventStore };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {ClientOptions} options
|
|
159
|
+
* @returns {Pick<ClientConfig, "transport" | "automaticDelivery">}
|
|
160
|
+
*/
|
|
161
|
+
function validateAutomaticDelivery(options) {
|
|
162
|
+
const transport = validateTransport(options["transport"]);
|
|
163
|
+
const { automaticDelivery } = options;
|
|
164
|
+
if (typeof automaticDelivery !== "boolean") {
|
|
165
|
+
throw new SdkError("validation_error", "automaticDelivery must be a boolean");
|
|
166
|
+
}
|
|
167
|
+
if (automaticDelivery && transport === undefined) {
|
|
168
|
+
throw new SdkError("validation_error", "automaticDelivery requires transport");
|
|
169
|
+
}
|
|
170
|
+
return { automaticDelivery, transport };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {ClientOptions} options
|
|
175
|
+
* @param {number} maxQueueSize
|
|
176
|
+
* @returns {Pick<ClientConfig, "transport" | "automaticDelivery" | "deliveryIntervalMs" | "deliveryQueueThreshold">}
|
|
177
|
+
*/
|
|
178
|
+
function validateDelivery(options, maxQueueSize) {
|
|
179
|
+
const automatic = validateAutomaticDelivery(options);
|
|
180
|
+
const deliveryIntervalMs = requirePositiveInteger("deliveryIntervalMs", options["deliveryIntervalMs"]);
|
|
181
|
+
if (deliveryIntervalMs > MAX_DELIVERY_INTERVAL_MS) {
|
|
182
|
+
throw new SdkError("validation_error", `deliveryIntervalMs must be at most ${MAX_DELIVERY_INTERVAL_MS}`);
|
|
183
|
+
}
|
|
184
|
+
const deliveryQueueThreshold = requirePositiveInteger("deliveryQueueThreshold", options["deliveryQueueThreshold"]);
|
|
185
|
+
if (deliveryQueueThreshold > maxQueueSize) {
|
|
186
|
+
throw new SdkError("validation_error", "deliveryQueueThreshold must not exceed maxQueueSize");
|
|
187
|
+
}
|
|
188
|
+
return { ...automatic, deliveryIntervalMs, deliveryQueueThreshold };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {ClientOptions} options
|
|
193
|
+
* @returns {ClientConfig}
|
|
194
|
+
*/
|
|
195
|
+
function clientConfig({
|
|
196
|
+
apiKey,
|
|
197
|
+
sdkName,
|
|
198
|
+
sdkVersion,
|
|
199
|
+
context,
|
|
200
|
+
[CONTEXT_PROVIDER]: contextProvider,
|
|
201
|
+
[BREADCRUMB_PROVIDER]: breadcrumbProvider,
|
|
202
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
203
|
+
eventFilter,
|
|
204
|
+
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
|
|
205
|
+
maxQueueBytes = DEFAULT_MAX_QUEUE_BYTES,
|
|
206
|
+
maxBatchEvents = DEFAULT_MAX_BATCH_EVENTS,
|
|
207
|
+
maxBatchBytes = DEFAULT_MAX_BATCH_BYTES,
|
|
208
|
+
onEventDropped,
|
|
209
|
+
eventStore,
|
|
210
|
+
[EVENT_QUEUE_FACTORY]: eventQueueFactory,
|
|
211
|
+
[CLIENT_REPORT_STORE]: clientReportStore,
|
|
212
|
+
transport,
|
|
213
|
+
automaticDelivery = transport !== undefined,
|
|
214
|
+
deliveryIntervalMs = DEFAULT_DELIVERY_INTERVAL_MS,
|
|
215
|
+
deliveryQueueThreshold = defaultDeliveryQueueThreshold(maxQueueSize)
|
|
216
|
+
}) {
|
|
217
|
+
const options = {
|
|
218
|
+
apiKey,
|
|
219
|
+
automaticDelivery,
|
|
220
|
+
breadcrumbProvider,
|
|
221
|
+
clientReportStore,
|
|
222
|
+
context,
|
|
223
|
+
contextProvider,
|
|
224
|
+
deliveryIntervalMs,
|
|
225
|
+
deliveryQueueThreshold,
|
|
226
|
+
eventFilter,
|
|
227
|
+
eventQueueFactory,
|
|
228
|
+
eventStore,
|
|
229
|
+
maxBatchBytes,
|
|
230
|
+
maxBatchEvents,
|
|
231
|
+
maxQueueBytes,
|
|
232
|
+
maxQueueSize,
|
|
233
|
+
maxRetries,
|
|
234
|
+
onEventDropped,
|
|
235
|
+
sdkName,
|
|
236
|
+
sdkVersion,
|
|
237
|
+
transport
|
|
238
|
+
};
|
|
239
|
+
const identity = validateIdentity(options);
|
|
240
|
+
const limits = validateLimits(options);
|
|
241
|
+
return { ...identity, ...limits, ...validateStorage(options), ...validateDelivery(options, limits.maxQueueSize) };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* @param {ClientConfig} config
|
|
246
|
+
* @returns {ClientSettings}
|
|
247
|
+
*/
|
|
248
|
+
function publicSettings(config) {
|
|
249
|
+
return {
|
|
250
|
+
apiKey: config.apiKey,
|
|
251
|
+
|
|
252
|
+
automaticDelivery: config.automaticDelivery,
|
|
253
|
+
|
|
254
|
+
deliveryIntervalMs: config.deliveryIntervalMs,
|
|
255
|
+
|
|
256
|
+
deliveryQueueThreshold: config.deliveryQueueThreshold,
|
|
257
|
+
|
|
258
|
+
eventFilter: config.eventFilter,
|
|
259
|
+
|
|
260
|
+
maxBatchBytes: config.maxBatchBytes,
|
|
261
|
+
|
|
262
|
+
maxBatchEvents: config.maxBatchEvents,
|
|
263
|
+
|
|
264
|
+
maxQueueBytes: config.maxQueueBytes,
|
|
265
|
+
|
|
266
|
+
maxQueueSize: config.maxQueueSize,
|
|
267
|
+
|
|
268
|
+
onEventDropped: config.onEventDropped,
|
|
269
|
+
|
|
270
|
+
context: cloneTelemetryContext(config.context),
|
|
271
|
+
|
|
272
|
+
contextProvider: config.contextProvider,
|
|
273
|
+
|
|
274
|
+
breadcrumbProvider: config.breadcrumbProvider,
|
|
275
|
+
|
|
276
|
+
transport: config.transport,
|
|
277
|
+
|
|
278
|
+
sdk: config.sdk,
|
|
279
|
+
|
|
280
|
+
maxRetries: config.maxRetries
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
BREADCRUMB_PROVIDER,
|
|
286
|
+
CLIENT_REPORT_STORE,
|
|
287
|
+
CONTEXT_PROVIDER,
|
|
288
|
+
EVENT_QUEUE_FACTORY,
|
|
289
|
+
clientConfig,
|
|
290
|
+
publicSettings,
|
|
291
|
+
validateTransport
|
|
292
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const { incrementBounded, nextBoundedTimestamp } = require("./bounded-number.cjs");
|
|
2
|
+
const { propertyOf } = require("./error-details.cjs");
|
|
3
|
+
const { NULL_VALUE } = require("./null-value.cjs");
|
|
4
|
+
const { randomHex } = require("./random-hex.cjs");
|
|
5
|
+
const { requireSynchronousStoreResult } = require("./stored-events.cjs");
|
|
6
|
+
const { isRecord } = require("./validation.cjs");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @import { ClientReportStore } from "./event-store.cjs"
|
|
10
|
+
* @typedef {"event_too_large" | "evicted_for_fatal" | "queue_bytes_overflow" | "queue_overflow"} DropReason
|
|
11
|
+
* @typedef {Readonly<{ report_id: unknown, dropped_events: Readonly<Record<string, number>> }>} ClientReport
|
|
12
|
+
* @typedef {{ pending: ClientReport | undefined, unreported: Record<string, number> }} RestoredReport
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const MAX_CLIENT_REPORT_REASONS = 16;
|
|
16
|
+
const MAX_CLIENT_REPORT_COUNT = 1_000_000_000;
|
|
17
|
+
const MIN_CLIENT_REPORT_COUNT = 1;
|
|
18
|
+
const CLIENT_REPORT_SAVE_INTERVAL_MS = 1000;
|
|
19
|
+
const CLIENT_REPORT_ID_PATTERN = /^[A-Za-z0-9_-]{16,64}$/u;
|
|
20
|
+
const CLIENT_REPORT_REASON_PATTERN = /^[a-z][a-z0-9_]{0,63}$/u;
|
|
21
|
+
const REPORT_ID_BYTES = 16;
|
|
22
|
+
const NO_COUNT = 0;
|
|
23
|
+
const STEP = 1;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} reason
|
|
27
|
+
* @param {unknown} count
|
|
28
|
+
* @returns {count is number}
|
|
29
|
+
*/
|
|
30
|
+
function isReportCount(reason, count) {
|
|
31
|
+
return CLIENT_REPORT_REASON_PATTERN.test(reason) && typeof count === "number" && Number.isSafeInteger(count)
|
|
32
|
+
&& count >= MIN_CLIENT_REPORT_COUNT && count <= MAX_CLIENT_REPORT_COUNT;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {unknown} counts
|
|
37
|
+
* @returns {Record<string, number>}
|
|
38
|
+
*/
|
|
39
|
+
function validClientReportCounts(counts) {
|
|
40
|
+
/** @type {Record<string, number>} */
|
|
41
|
+
const valid = {};
|
|
42
|
+
if (!isRecord(counts)) {
|
|
43
|
+
return valid;
|
|
44
|
+
}
|
|
45
|
+
for (const [reason, count] of Object.entries(counts)) {
|
|
46
|
+
if (Object.keys(valid).length < MAX_CLIENT_REPORT_REASONS && isReportCount(reason, count)) {
|
|
47
|
+
valid[reason] = count;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return valid;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {Readonly<Record<string, number>>} counts
|
|
55
|
+
* @returns {Record<string, number>}
|
|
56
|
+
*/
|
|
57
|
+
function reportableCounts(counts) {
|
|
58
|
+
/** @type {Record<string, number>} */
|
|
59
|
+
const reportable = {};
|
|
60
|
+
for (const [reason, count] of Object.entries(counts)) {
|
|
61
|
+
if (Object.keys(reportable).length < MAX_CLIENT_REPORT_REASONS && count > NO_COUNT) {
|
|
62
|
+
reportable[reason] = count;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return reportable;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {Readonly<Record<string, number>>} counts
|
|
70
|
+
* @param {Readonly<Record<string, number>>} reported
|
|
71
|
+
* @returns {Record<string, number>}
|
|
72
|
+
*/
|
|
73
|
+
function withoutReported(counts, reported) {
|
|
74
|
+
const reportedReasons = new Set(Object.keys(reported));
|
|
75
|
+
/** @type {Record<string, number>} */
|
|
76
|
+
const remaining = {};
|
|
77
|
+
for (const [reason, count] of Object.entries(counts)) {
|
|
78
|
+
if (!reportedReasons.has(reason)) {
|
|
79
|
+
remaining[reason] = count;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return remaining;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {unknown} reportId
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function isReportId(reportId) {
|
|
90
|
+
if (typeof reportId === "number") {
|
|
91
|
+
return CLIENT_REPORT_ID_PATTERN.test(String(reportId));
|
|
92
|
+
}
|
|
93
|
+
return typeof reportId === "string" && CLIENT_REPORT_ID_PATTERN.test(reportId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {ClientReportStore | undefined} store
|
|
98
|
+
* @returns {unknown}
|
|
99
|
+
*/
|
|
100
|
+
function loadClientReportState(store) {
|
|
101
|
+
try {
|
|
102
|
+
return store === undefined ? undefined : requireSynchronousStoreResult("loadClientReport", store.loadClientReport());
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {ClientReportStore | undefined} store
|
|
110
|
+
* @returns {RestoredReport}
|
|
111
|
+
*/
|
|
112
|
+
function loadClientReport(store) {
|
|
113
|
+
const state = loadClientReportState(store);
|
|
114
|
+
const pending = propertyOf(state, "pending");
|
|
115
|
+
const pendingCounts = validClientReportCounts(propertyOf(pending, "dropped_events"));
|
|
116
|
+
const reportId = propertyOf(pending, "report_id");
|
|
117
|
+
return {
|
|
118
|
+
pending: isReportId(reportId) && Object.keys(pendingCounts).length > NO_COUNT
|
|
119
|
+
? Object.freeze({
|
|
120
|
+
report_id: reportId,
|
|
121
|
+
|
|
122
|
+
dropped_events: pendingCounts
|
|
123
|
+
})
|
|
124
|
+
: undefined,
|
|
125
|
+
unreported: validClientReportCounts(propertyOf(state, "unreported"))
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {ClientReportStore} store
|
|
131
|
+
* @param {unknown} state
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
function saveClientReportState(store, state) {
|
|
135
|
+
try {
|
|
136
|
+
requireSynchronousStoreResult("saveClientReport", store.saveClientReport(state));
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class DropLedger {
|
|
144
|
+
droppedEventCount = NO_COUNT;
|
|
145
|
+
|
|
146
|
+
/** @type {Record<DropReason, number>} */
|
|
147
|
+
droppedEventsByReason = {
|
|
148
|
+
event_too_large: NO_COUNT,
|
|
149
|
+
evicted_for_fatal: NO_COUNT,
|
|
150
|
+
queue_bytes_overflow: NO_COUNT,
|
|
151
|
+
queue_overflow: NO_COUNT
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
clientReportSavedAtMs = NO_COUNT;
|
|
155
|
+
|
|
156
|
+
/** @type {DropReason | "none"} */
|
|
157
|
+
lastDropReason = "none";
|
|
158
|
+
|
|
159
|
+
lastDroppedAtUnixMs = NO_COUNT;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {ClientReportStore | undefined} store
|
|
163
|
+
*/
|
|
164
|
+
constructor(store) {
|
|
165
|
+
this.clientReportStore = store;
|
|
166
|
+
const restoredReport = loadClientReport(store);
|
|
167
|
+
this.unreportedDropsByReason = restoredReport.unreported;
|
|
168
|
+
this.pendingClientReport = restoredReport.pending;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {DropReason} reason
|
|
173
|
+
* @returns {void}
|
|
174
|
+
*/
|
|
175
|
+
record(reason) {
|
|
176
|
+
this.droppedEventCount = incrementBounded(this.droppedEventCount);
|
|
177
|
+
this.droppedEventsByReason[reason] = incrementBounded(this.droppedEventsByReason[reason]);
|
|
178
|
+
const previous = this.unreportedDropsByReason[reason];
|
|
179
|
+
this.unreportedDropsByReason[reason] = Math.min(MAX_CLIENT_REPORT_COUNT, (previous ?? NO_COUNT) + STEP);
|
|
180
|
+
this.save(previous === undefined);
|
|
181
|
+
this.lastDropReason = reason;
|
|
182
|
+
this.lastDroppedAtUnixMs = nextBoundedTimestamp(this.lastDroppedAtUnixMs);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @returns {ClientReport | undefined}
|
|
187
|
+
*/
|
|
188
|
+
pendingReport() {
|
|
189
|
+
if (this.pendingClientReport !== undefined) {
|
|
190
|
+
return this.pendingClientReport;
|
|
191
|
+
}
|
|
192
|
+
const droppedEvents = reportableCounts(this.unreportedDropsByReason);
|
|
193
|
+
if (Object.keys(droppedEvents).length === NO_COUNT) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
this.unreportedDropsByReason = withoutReported(this.unreportedDropsByReason, droppedEvents);
|
|
197
|
+
this.pendingClientReport = Object.freeze({
|
|
198
|
+
report_id: randomHex(REPORT_ID_BYTES),
|
|
199
|
+
|
|
200
|
+
dropped_events: droppedEvents
|
|
201
|
+
});
|
|
202
|
+
this.save(true);
|
|
203
|
+
return this.pendingClientReport;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @returns {void}
|
|
208
|
+
*/
|
|
209
|
+
acknowledgeReport() {
|
|
210
|
+
this.pendingClientReport = undefined;
|
|
211
|
+
this.save(true);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {boolean} immediately
|
|
216
|
+
* @returns {void}
|
|
217
|
+
*/
|
|
218
|
+
save(immediately) {
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
const store = this.clientReportStore;
|
|
221
|
+
if (store === undefined || (!immediately && now - this.clientReportSavedAtMs < CLIENT_REPORT_SAVE_INTERVAL_MS)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.clientReportSavedAtMs = now;
|
|
225
|
+
saveClientReportState(store, {
|
|
226
|
+
pending: this.pendingClientReport ?? NULL_VALUE,
|
|
227
|
+
unreported: { ...this.unreportedDropsByReason }
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = { DropLedger };
|