@logbrew/sdk 0.1.38 → 0.1.40

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.
Files changed (91) hide show
  1. package/automatic-delivery.cjs +271 -0
  2. package/batch-sender.cjs +164 -0
  3. package/bounded-number.cjs +34 -0
  4. package/browser-breadcrumbs.cjs +346 -136
  5. package/capture-support.cjs +100 -0
  6. package/client-options.cjs +292 -0
  7. package/client-report.cjs +232 -0
  8. package/client.cjs +447 -0
  9. package/console-arguments.cjs +151 -0
  10. package/console-capture.cjs +194 -0
  11. package/core.cjs +30 -2810
  12. package/delivery-health.cjs +133 -0
  13. package/error-causes.cjs +130 -0
  14. package/error-details.cjs +124 -0
  15. package/error-issue.cjs +187 -0
  16. package/event-clone.cjs +55 -0
  17. package/event-metadata.cjs +40 -0
  18. package/event-pipeline.cjs +325 -0
  19. package/event-queue.cjs +306 -0
  20. package/event-store.cjs +237 -0
  21. package/event-validation.cjs +159 -0
  22. package/examples/agent-timeline.cjs +57 -46
  23. package/examples/agent-timeline.mjs +41 -35
  24. package/examples/index.mjs +122 -94
  25. package/examples/readme-example.cjs +36 -27
  26. package/examples/readme-example.mjs +21 -17
  27. package/examples/real-user-smoke.cjs +41 -32
  28. package/examples/real-user-smoke.mjs +26 -22
  29. package/exception-chain.cjs +205 -0
  30. package/index.d.ts +144 -1076
  31. package/index.js +1 -1
  32. package/issue-exception.cjs +13 -8
  33. package/metric-validation.cjs +95 -0
  34. package/network-milestone.cjs +155 -0
  35. package/null-value.cjs +4 -0
  36. package/opentelemetry-bridge.cjs +330 -0
  37. package/opentelemetry-context.cjs +245 -0
  38. package/opentelemetry-exceptions.cjs +69 -0
  39. package/opentelemetry-keys.cjs +183 -0
  40. package/opentelemetry-span.cjs +286 -0
  41. package/opentelemetry-summary.cjs +213 -0
  42. package/opentelemetry-time.cjs +131 -0
  43. package/opentelemetry.cjs +99 -1068
  44. package/package.json +55 -1
  45. package/pino-destination.cjs +108 -0
  46. package/pino-record.cjs +237 -0
  47. package/product-action.cjs +163 -0
  48. package/provided-breadcrumbs.cjs +117 -0
  49. package/random-hex.cjs +29 -0
  50. package/react-native.d.ts +67 -6
  51. package/react-native.js +1 -1
  52. package/recording-transport.cjs +68 -0
  53. package/release-artifacts-build.cjs +263 -84
  54. package/release-artifacts-common.js +347 -66
  55. package/release-artifacts-debug-id.js +178 -0
  56. package/release-artifacts-inspect.js +213 -0
  57. package/release-artifacts-issue-event.js +99 -0
  58. package/release-artifacts-manifest-artifact.js +107 -0
  59. package/release-artifacts-manifest.js +77 -0
  60. package/release-artifacts-mappings.js +265 -0
  61. package/release-artifacts-prepare.js +297 -0
  62. package/release-artifacts-source-context.js +210 -0
  63. package/release-artifacts-symbolication.js +272 -421
  64. package/release-artifacts-upload-files.js +122 -0
  65. package/release-artifacts-upload-request.js +246 -0
  66. package/release-artifacts-upload.js +155 -300
  67. package/release-artifacts-urls.js +49 -0
  68. package/release-artifacts.js +189 -662
  69. package/retry-delay.cjs +32 -0
  70. package/span-validation.cjs +191 -0
  71. package/stored-events.cjs +243 -0
  72. package/types/client.d.ts +67 -0
  73. package/types/delivery.d.ts +125 -0
  74. package/types/issues.d.ts +249 -0
  75. package/types/logging.d.ts +175 -0
  76. package/types/opentelemetry.d.ts +205 -0
  77. package/types/product.d.ts +128 -0
  78. package/types/recording-transport.d.ts +15 -0
  79. package/types/sdk-error.d.ts +9 -0
  80. package/types/telemetry.d.ts +171 -0
  81. package/types/trace-context.d.ts +110 -0
  82. package/types/transport-error.d.ts +10 -0
  83. package/utf8.cjs +37 -0
  84. package/validation.cjs +49 -1
  85. package/vite-release-artifacts-config.cjs +120 -0
  86. package/vite-release-artifacts.cjs +199 -99
  87. package/vite-release-artifacts.d.cts +65 -40
  88. package/vite-release-artifacts.d.ts +65 -40
  89. package/vite-release-artifacts.js +2 -2
  90. package/winston-info.cjs +205 -0
  91. package/winston.cjs +189 -289
@@ -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 };