@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
package/retry-delay.cjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const MAX_DELIVERY_INTERVAL_MS = 60_000;
|
|
2
|
+
const MIN_RATE_LIMIT_WAIT_MS = 1000;
|
|
3
|
+
const MAX_RATE_LIMIT_WAIT_MS = 3_600_000;
|
|
4
|
+
const MAX_BACKOFF_EXPONENT = 30;
|
|
5
|
+
const FIRST_FAILURE = 1;
|
|
6
|
+
const BACKOFF_BASE = 2;
|
|
7
|
+
const HALF = 2;
|
|
8
|
+
const INCLUSIVE = 1;
|
|
9
|
+
const JITTER_DIVISOR = 10;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {number} deliveryIntervalMs
|
|
13
|
+
* @param {number} consecutiveFailures
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function automaticRetryDelayMs(deliveryIntervalMs, consecutiveFailures) {
|
|
17
|
+
const exponent = Math.min(consecutiveFailures - FIRST_FAILURE, MAX_BACKOFF_EXPONENT);
|
|
18
|
+
const maximumDelay = Math.min(MAX_DELIVERY_INTERVAL_MS, deliveryIntervalMs * (BACKOFF_BASE ** exponent));
|
|
19
|
+
const minimumDelay = Math.ceil(maximumDelay / HALF);
|
|
20
|
+
return minimumDelay + Math.floor(Math.random() * (maximumDelay - minimumDelay + INCLUSIVE));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {number} delayMs
|
|
25
|
+
* @returns {number}
|
|
26
|
+
*/
|
|
27
|
+
function rateLimitWaitMs(delayMs) {
|
|
28
|
+
const bounded = Math.min(MAX_RATE_LIMIT_WAIT_MS, Math.max(MIN_RATE_LIMIT_WAIT_MS, delayMs));
|
|
29
|
+
return Math.min(MAX_RATE_LIMIT_WAIT_MS, bounded + Math.floor(Math.random() * (Math.floor(bounded / JITTER_DIVISOR) + INCLUSIVE)));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { MAX_DELIVERY_INTERVAL_MS, automaticRetryDelayMs, rateLimitWaitMs };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const { withMetadata } = require("./event-metadata.cjs");
|
|
2
|
+
const { compactMetadata } = require("./log-context.cjs");
|
|
3
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
4
|
+
const { parseTraceparent } = require("./trace-context.cjs");
|
|
5
|
+
const {
|
|
6
|
+
isRecord,
|
|
7
|
+
optionalEntry,
|
|
8
|
+
requireAllowedValue,
|
|
9
|
+
requireNonEmpty,
|
|
10
|
+
requireSpanId,
|
|
11
|
+
requireTimestamp,
|
|
12
|
+
requireTraceId
|
|
13
|
+
} = require("./validation.cjs");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @import { Metadata, SpanAttributes, SpanEventSummary, SpanLinkSummary } from "./index.cjs"
|
|
17
|
+
* @import { EventAttributes } from "./event-validation.cjs"
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const MAX_SPAN_EVENTS = 8;
|
|
21
|
+
const MAX_SPAN_LINKS = 8;
|
|
22
|
+
const NO_ENTRIES = 0;
|
|
23
|
+
const MIN_DURATION_MS = 0;
|
|
24
|
+
/** @type {ReadonlySet<SpanAttributes["status"]>} */
|
|
25
|
+
const SPAN_STATUSES = new Set(["ok", "error"]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {unknown} metadata
|
|
29
|
+
* @returns {Metadata | undefined}
|
|
30
|
+
*/
|
|
31
|
+
function nonEmptyMetadata(metadata) {
|
|
32
|
+
if (metadata === undefined) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const compacted = compactMetadata(metadata);
|
|
36
|
+
return Object.keys(compacted).length > NO_ENTRIES ? compacted : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {unknown} durationMs
|
|
41
|
+
* @returns {number | undefined}
|
|
42
|
+
*/
|
|
43
|
+
function optionalSpanDuration(durationMs) {
|
|
44
|
+
if (durationMs === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (typeof durationMs !== "number" || Number.isNaN(durationMs) || durationMs < MIN_DURATION_MS) {
|
|
48
|
+
throw new SdkError("validation_error", "span durationMs must be non-negative");
|
|
49
|
+
}
|
|
50
|
+
return durationMs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {unknown} list
|
|
55
|
+
* @param {string} label
|
|
56
|
+
* @param {number} maxEntries
|
|
57
|
+
* @returns {readonly unknown[] | undefined}
|
|
58
|
+
*/
|
|
59
|
+
function optionalBoundedList(list, label, maxEntries) {
|
|
60
|
+
if (list === undefined) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(list)) {
|
|
64
|
+
throw new SdkError("validation_error", `${label} must be an array`);
|
|
65
|
+
}
|
|
66
|
+
if (list.length > maxEntries) {
|
|
67
|
+
throw new SdkError("validation_error", `${label} must contain at most ${maxEntries} entries`);
|
|
68
|
+
}
|
|
69
|
+
return list.length === NO_ENTRIES ? undefined : list;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {unknown} event
|
|
74
|
+
* @returns {SpanEventSummary}
|
|
75
|
+
*/
|
|
76
|
+
function validateSpanEvent(event) {
|
|
77
|
+
if (!isRecord(event)) {
|
|
78
|
+
throw new SdkError("validation_error", "span event must be an object");
|
|
79
|
+
}
|
|
80
|
+
const name = requireNonEmpty("span event name", event["name"]);
|
|
81
|
+
const timestamp = event["timestamp"] === undefined ? undefined : requireTimestamp(event["timestamp"]);
|
|
82
|
+
return {
|
|
83
|
+
name,
|
|
84
|
+
...optionalEntry("timestamp", timestamp),
|
|
85
|
+
...optionalEntry("metadata", nonEmptyMetadata(event["metadata"]))
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {unknown} events
|
|
91
|
+
* @returns {SpanEventSummary[] | undefined}
|
|
92
|
+
*/
|
|
93
|
+
function validateSpanEvents(events) {
|
|
94
|
+
return optionalBoundedList(events, "span events", MAX_SPAN_EVENTS)?.map((event) => validateSpanEvent(event));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {unknown} link
|
|
99
|
+
* @returns {SpanLinkSummary}
|
|
100
|
+
*/
|
|
101
|
+
function validateSpanLink(link) {
|
|
102
|
+
if (!isRecord(link)) {
|
|
103
|
+
throw new SdkError("validation_error", "span link must be an object");
|
|
104
|
+
}
|
|
105
|
+
const traceId = requireTraceId(link["traceId"]);
|
|
106
|
+
const spanId = requireSpanId("span link spanId", link["spanId"]);
|
|
107
|
+
const { sampled } = link;
|
|
108
|
+
if (sampled !== undefined && typeof sampled !== "boolean") {
|
|
109
|
+
throw new SdkError("validation_error", "span link sampled must be a boolean");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
traceId: traceId.toLowerCase(),
|
|
113
|
+
|
|
114
|
+
spanId: spanId.toLowerCase(),
|
|
115
|
+
...optionalEntry("sampled", sampled),
|
|
116
|
+
...optionalEntry("metadata", nonEmptyMetadata(link["metadata"]))
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {unknown} links
|
|
122
|
+
* @returns {SpanLinkSummary[] | undefined}
|
|
123
|
+
*/
|
|
124
|
+
function validateSpanLinks(links) {
|
|
125
|
+
return optionalBoundedList(links, "span links", MAX_SPAN_LINKS)?.map((link) => validateSpanLink(link));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {Readonly<Record<string, unknown>>} attributes
|
|
130
|
+
* @returns {EventAttributes}
|
|
131
|
+
*/
|
|
132
|
+
function validateSpan(attributes) {
|
|
133
|
+
const name = requireNonEmpty("span name", attributes["name"]);
|
|
134
|
+
const traceId = requireNonEmpty("span traceId", attributes["traceId"]);
|
|
135
|
+
const spanId = requireNonEmpty("span spanId", attributes["spanId"]);
|
|
136
|
+
const status = requireAllowedValue("span status", attributes["status"], SPAN_STATUSES);
|
|
137
|
+
const parentSpanId = attributes["parentSpanId"] === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: requireNonEmpty("span parentSpanId", attributes["parentSpanId"]);
|
|
140
|
+
const durationMs = optionalSpanDuration(attributes["durationMs"]);
|
|
141
|
+
const events = validateSpanEvents(attributes["events"]);
|
|
142
|
+
const links = validateSpanLinks(attributes["links"]);
|
|
143
|
+
return withMetadata({
|
|
144
|
+
name,
|
|
145
|
+
|
|
146
|
+
traceId,
|
|
147
|
+
|
|
148
|
+
spanId,
|
|
149
|
+
|
|
150
|
+
status,
|
|
151
|
+
...optionalEntry("parentSpanId", parentSpanId),
|
|
152
|
+
...optionalEntry("durationMs", durationMs),
|
|
153
|
+
...optionalEntry("events", events),
|
|
154
|
+
...optionalEntry("links", links)
|
|
155
|
+
}, attributes["metadata"], attributes["context"]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {unknown} traceparent
|
|
160
|
+
* @param {unknown} attributes
|
|
161
|
+
* @returns {SpanAttributes}
|
|
162
|
+
*/
|
|
163
|
+
function spanAttributesFromTraceparent(traceparent, attributes) {
|
|
164
|
+
if (!isRecord(attributes)) {
|
|
165
|
+
throw new SdkError("validation_error", "span attributes must be an object");
|
|
166
|
+
}
|
|
167
|
+
const context = parseTraceparent(traceparent);
|
|
168
|
+
const name = requireNonEmpty("span name", attributes["name"]);
|
|
169
|
+
const spanId = requireSpanId("spanId", attributes["spanId"]);
|
|
170
|
+
const status = requireAllowedValue("span status", attributes["status"], SPAN_STATUSES);
|
|
171
|
+
const durationMs = optionalSpanDuration(attributes["durationMs"]);
|
|
172
|
+
const events = validateSpanEvents(attributes["events"]);
|
|
173
|
+
const links = validateSpanLinks(attributes["links"]);
|
|
174
|
+
return {
|
|
175
|
+
name,
|
|
176
|
+
|
|
177
|
+
traceId: context.traceId,
|
|
178
|
+
|
|
179
|
+
spanId: spanId.toLowerCase(),
|
|
180
|
+
|
|
181
|
+
parentSpanId: context.parentSpanId,
|
|
182
|
+
|
|
183
|
+
status,
|
|
184
|
+
...optionalEntry("durationMs", durationMs),
|
|
185
|
+
...optionalEntry("events", events),
|
|
186
|
+
...optionalEntry("links", links),
|
|
187
|
+
...optionalEntry("metadata", attributes["metadata"] === undefined ? undefined : compactMetadata(attributes["metadata"]))
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { spanAttributesFromTraceparent, validateSpan };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
const { isObjectLike } = require("./error-details.cjs");
|
|
2
|
+
const { cloneEvent } = require("./event-clone.cjs");
|
|
3
|
+
const { EVENT_VALIDATORS, isEventType } = require("./event-validation.cjs");
|
|
4
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
5
|
+
const { utf8ByteLength } = require("./utf8.cjs");
|
|
6
|
+
const { isRecord, requireNonEmpty, requireTimestamp } = require("./validation.cjs");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @import { EventType, EventValidator, SdkEvent } from "./event-validation.cjs"
|
|
10
|
+
* @import { DeepReadonly } from "./validation.cjs"
|
|
11
|
+
* @typedef {{ event: DeepReadonly<SdkEvent>, eventBytes: number, serializedEvent: string }} StoredRecord
|
|
12
|
+
* @typedef {Readonly<Record<string, unknown>>} UnknownRecord
|
|
13
|
+
* @typedef {{
|
|
14
|
+
* attributes: UnknownRecord,
|
|
15
|
+
* event: UnknownRecord,
|
|
16
|
+
* eventBytes: number,
|
|
17
|
+
* header: { type: EventType, id: string, timestamp: string },
|
|
18
|
+
* serializedEvent: string,
|
|
19
|
+
* validator: EventValidator
|
|
20
|
+
* }} RecordParts
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const MIN_EVENT_BYTES = 0;
|
|
24
|
+
const CONTEXT_SCHEMA_VERSION = 1;
|
|
25
|
+
const KEY_INDEX = 0;
|
|
26
|
+
const STEP = 1;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @returns {InstanceType<typeof SdkError>}
|
|
30
|
+
*/
|
|
31
|
+
function invalidStoredRecord() {
|
|
32
|
+
return new SdkError("persistence_error", "event store returned an invalid record");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} operation
|
|
37
|
+
* @param {unknown} result
|
|
38
|
+
* @returns {unknown}
|
|
39
|
+
*/
|
|
40
|
+
function requireSynchronousStoreResult(operation, result) {
|
|
41
|
+
if (isObjectLike(result) && typeof Reflect.get(result, "then") === "function") {
|
|
42
|
+
throw new SdkError("persistence_error", `eventStore.${operation} must complete synchronously`);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {readonly [string, unknown]} left
|
|
49
|
+
* @param {readonly [string, unknown]} right
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
function byKey(left, right) {
|
|
53
|
+
return left[KEY_INDEX].localeCompare(right[KEY_INDEX]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} _key
|
|
58
|
+
* @param {unknown} entry
|
|
59
|
+
* @returns {unknown}
|
|
60
|
+
*/
|
|
61
|
+
function sortedEntries(_key, entry) {
|
|
62
|
+
if (!isRecord(entry)) {
|
|
63
|
+
return entry;
|
|
64
|
+
}
|
|
65
|
+
const entries = Object.entries(entry);
|
|
66
|
+
entries.sort(byKey);
|
|
67
|
+
return Object.fromEntries(entries);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {unknown} value
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function structuralJson(value) {
|
|
75
|
+
return JSON.stringify(value, sortedEntries);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {UnknownRecord} event
|
|
80
|
+
* @param {UnknownRecord} attributes
|
|
81
|
+
* @returns {{ event: UnknownRecord, attributes: UnknownRecord }}
|
|
82
|
+
*/
|
|
83
|
+
function recoveredEvent(event, attributes) {
|
|
84
|
+
const fatalAttributes = event["type"] === "issue" && attributes["level"] === "fatal"
|
|
85
|
+
? { ...attributes, level: "critical" }
|
|
86
|
+
: attributes;
|
|
87
|
+
const { context } = fatalAttributes;
|
|
88
|
+
const recoveredAttributes = isRecord(context) && context["schemaVersion"] === undefined
|
|
89
|
+
? { ...fatalAttributes, context: { schemaVersion: CONTEXT_SCHEMA_VERSION, ...context } }
|
|
90
|
+
: fatalAttributes;
|
|
91
|
+
return { attributes: recoveredAttributes, event: { ...event, attributes: recoveredAttributes } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {unknown} eventBytes
|
|
96
|
+
* @param {unknown} serializedEvent
|
|
97
|
+
* @returns {Readonly<{ eventBytes: number, serializedEvent: string }>}
|
|
98
|
+
*/
|
|
99
|
+
function storedPayload(eventBytes, serializedEvent) {
|
|
100
|
+
if (typeof eventBytes !== "number" || !Number.isSafeInteger(eventBytes) || eventBytes <= MIN_EVENT_BYTES || typeof serializedEvent !== "string") {
|
|
101
|
+
throw invalidStoredRecord();
|
|
102
|
+
}
|
|
103
|
+
return { eventBytes, serializedEvent };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {UnknownRecord} event
|
|
108
|
+
* @param {Readonly<{ eventBytes: number, serializedEvent: string }>} payload
|
|
109
|
+
* @returns {RecordParts}
|
|
110
|
+
*/
|
|
111
|
+
function storedEventParts(event, payload) {
|
|
112
|
+
const { type } = event;
|
|
113
|
+
const validator = isEventType(type) ? EVENT_VALIDATORS.get(type) : undefined;
|
|
114
|
+
if (!isEventType(type) || validator === undefined) {
|
|
115
|
+
throw invalidStoredRecord();
|
|
116
|
+
}
|
|
117
|
+
const header = {
|
|
118
|
+
type,
|
|
119
|
+
|
|
120
|
+
id: requireNonEmpty("event id", event["id"]),
|
|
121
|
+
|
|
122
|
+
timestamp: requireTimestamp(event["timestamp"])
|
|
123
|
+
};
|
|
124
|
+
const { attributes } = event;
|
|
125
|
+
if (!isRecord(attributes)) {
|
|
126
|
+
throw invalidStoredRecord();
|
|
127
|
+
}
|
|
128
|
+
return { ...payload, attributes, event, header, validator };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {UnknownRecord} record
|
|
133
|
+
* @returns {RecordParts}
|
|
134
|
+
*/
|
|
135
|
+
function storedRecordParts(record) {
|
|
136
|
+
const { event, eventBytes, serializedEvent } = record;
|
|
137
|
+
if (!isRecord(event)) {
|
|
138
|
+
throw invalidStoredRecord();
|
|
139
|
+
}
|
|
140
|
+
return storedEventParts(event, storedPayload(eventBytes, serializedEvent));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {Readonly<RecordParts["header"]>} header
|
|
145
|
+
* @param {Readonly<{ event: UnknownRecord, attributes: UnknownRecord }>} recovered
|
|
146
|
+
* @param {EventValidator} validator
|
|
147
|
+
* @returns {StoredRecord}
|
|
148
|
+
*/
|
|
149
|
+
function normalizedRecord(header, recovered, validator) {
|
|
150
|
+
/** @type {SdkEvent} */
|
|
151
|
+
const normalizedEvent = { ...header, attributes: validator(recovered.attributes) };
|
|
152
|
+
const normalizedSerializedEvent = JSON.stringify(normalizedEvent);
|
|
153
|
+
if (structuralJson(normalizedEvent) !== structuralJson(recovered.event)) {
|
|
154
|
+
throw invalidStoredRecord();
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
event: cloneEvent(normalizedEvent),
|
|
158
|
+
eventBytes: utf8ByteLength(normalizedSerializedEvent),
|
|
159
|
+
serializedEvent: normalizedSerializedEvent
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {unknown} record
|
|
165
|
+
* @returns {StoredRecord}
|
|
166
|
+
*/
|
|
167
|
+
function normalizeRecord(record) {
|
|
168
|
+
if (!isRecord(record)) {
|
|
169
|
+
throw invalidStoredRecord();
|
|
170
|
+
}
|
|
171
|
+
const { attributes, event, eventBytes, header, serializedEvent, validator } = storedRecordParts(record);
|
|
172
|
+
if (JSON.stringify(event) !== serializedEvent || utf8ByteLength(serializedEvent) !== eventBytes) {
|
|
173
|
+
throw invalidStoredRecord();
|
|
174
|
+
}
|
|
175
|
+
return normalizedRecord(header, recoveredEvent(event, attributes), validator);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {unknown} record
|
|
180
|
+
* @returns {StoredRecord}
|
|
181
|
+
*/
|
|
182
|
+
function normalizeStoredRecord(record) {
|
|
183
|
+
try {
|
|
184
|
+
return normalizeRecord(record);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error instanceof SdkError && error.code === "persistence_error") {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
throw invalidStoredRecord();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @param {string} serializedEvent
|
|
195
|
+
* @returns {unknown}
|
|
196
|
+
*/
|
|
197
|
+
function parseStoredEvent(serializedEvent) {
|
|
198
|
+
try {
|
|
199
|
+
/** @type {unknown} */
|
|
200
|
+
const event = JSON.parse(serializedEvent);
|
|
201
|
+
return event;
|
|
202
|
+
} catch {
|
|
203
|
+
throw invalidStoredRecord();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {unknown} serializedEvent
|
|
209
|
+
* @returns {DeepReadonly<SdkEvent>}
|
|
210
|
+
*/
|
|
211
|
+
function restoreStoredEvent(serializedEvent) {
|
|
212
|
+
if (typeof serializedEvent !== "string" || serializedEvent === "") {
|
|
213
|
+
throw invalidStoredRecord();
|
|
214
|
+
}
|
|
215
|
+
return normalizeStoredRecord({
|
|
216
|
+
event: parseStoredEvent(serializedEvent),
|
|
217
|
+
eventBytes: utf8ByteLength(serializedEvent),
|
|
218
|
+
serializedEvent
|
|
219
|
+
}).event;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {readonly string[]} expected
|
|
224
|
+
* @param {readonly string[]} actual
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
function isOrderedSubsequence(expected, actual) {
|
|
228
|
+
let index = 0;
|
|
229
|
+
for (const value of actual) {
|
|
230
|
+
if (value === expected[index]) {
|
|
231
|
+
index += STEP;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return index === expected.length;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
invalidStoredRecord,
|
|
239
|
+
isOrderedSubsequence,
|
|
240
|
+
normalizeStoredRecord,
|
|
241
|
+
requireSynchronousStoreResult,
|
|
242
|
+
restoreStoredEvent
|
|
243
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ActionAttributes, EnvironmentAttributes, LogAttributes, MetricAttributes, ReleaseAttributes, SpanAttributes, TelemetryContext } from "./telemetry.js";
|
|
2
|
+
import type { DeliveryHealthSnapshot, DroppedEvent, EventFilter, EventStore, IssueAdmission, Transport, TransportResponse } from "./delivery.js";
|
|
3
|
+
import type { IssueAttributes, IssueBreadcrumbInput } from "./issues.js";
|
|
4
|
+
|
|
5
|
+
/** Buffered public client for validating, previewing, and flushing LogBrew events. */
|
|
6
|
+
declare class LogBrewClient {
|
|
7
|
+
/** Create a client from public SDK identity, retry, and API key settings. */
|
|
8
|
+
public static create(config: {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
sdkName: string;
|
|
11
|
+
sdkVersion: string;
|
|
12
|
+
/** Versioned context merged into every captured event; event context can override dynamic fields. */
|
|
13
|
+
context?: TelemetryContext;
|
|
14
|
+
/** Retry attempts after the first send. Must be a non-negative integer; defaults to 2. */
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
eventFilter?: EventFilter;
|
|
17
|
+
/** Maximum queued compact event bytes. Defaults to 4 MiB. */
|
|
18
|
+
maxQueueBytes?: number;
|
|
19
|
+
maxQueueSize?: number;
|
|
20
|
+
/** Maximum events per request body. Defaults to 100. */
|
|
21
|
+
maxBatchEvents?: number;
|
|
22
|
+
/** Maximum UTF-8 request body bytes. Defaults to 256 KiB. */
|
|
23
|
+
maxBatchBytes?: number;
|
|
24
|
+
onEventDropped?: (drop: DroppedEvent) => void;
|
|
25
|
+
/** Optional app-scoped persistence adapter. Methods must complete synchronously. */
|
|
26
|
+
eventStore?: EventStore;
|
|
27
|
+
/** Client-owned transport used by automatic delivery and by flush/shutdown when no argument is supplied. */
|
|
28
|
+
transport?: Transport;
|
|
29
|
+
/** Enable interval and queue-threshold delivery. Defaults to true when transport is supplied. */
|
|
30
|
+
automaticDelivery?: boolean;
|
|
31
|
+
/** One-shot delivery interval in milliseconds. Defaults to 1000 and must not exceed 60000. */
|
|
32
|
+
deliveryIntervalMs?: number;
|
|
33
|
+
/** Queue count that triggers delivery without waiting for the interval. Defaults to min(50, maxQueueSize). */
|
|
34
|
+
deliveryQueueThreshold?: number;
|
|
35
|
+
}): LogBrewClient;
|
|
36
|
+
/** Return the queued event count currently buffered in memory. */
|
|
37
|
+
public pendingEvents(): number;
|
|
38
|
+
/** Return compact serialized event bytes currently buffered in memory. */
|
|
39
|
+
public pendingBytes(): number;
|
|
40
|
+
/** Return the number of events dropped because the bounded in-memory queue was full. */
|
|
41
|
+
public droppedEvents(): number;
|
|
42
|
+
/** Return a frozen, content-free snapshot of queue and delivery lifecycle health. */
|
|
43
|
+
public deliveryHealth(): DeliveryHealthSnapshot;
|
|
44
|
+
/** Return the queued event batch as stable, pretty-printed JSON. */
|
|
45
|
+
public previewJson(): string;
|
|
46
|
+
/** Purge queued events from memory and persistence while no delivery operation is active. */
|
|
47
|
+
public purgePendingEvents(): number;
|
|
48
|
+
/** Add one explicit privacy-bounded breadcrumb to the client's 64-entry issue history. */
|
|
49
|
+
public addBreadcrumb(breadcrumb: IssueBreadcrumbInput, timestamp?: string): void;
|
|
50
|
+
/** Clear the current issue breadcrumb history and return the number removed. */
|
|
51
|
+
public clearBreadcrumbs(): number;
|
|
52
|
+
public release(id: string, timestamp: string, attributes: ReleaseAttributes): void;
|
|
53
|
+
public environment(id: string, timestamp: string, attributes: EnvironmentAttributes): void;
|
|
54
|
+
public issue(id: string, timestamp: string, attributes: IssueAttributes): void;
|
|
55
|
+
/** Capture once and report queue admission. Custom issue overrides run once and return unavailable. */
|
|
56
|
+
public issueWithAdmission(id: string, timestamp: string, attributes: IssueAttributes): IssueAdmission;
|
|
57
|
+
public log(id: string, timestamp: string, attributes: LogAttributes): void;
|
|
58
|
+
public span(id: string, timestamp: string, attributes: SpanAttributes): void;
|
|
59
|
+
public action(id: string, timestamp: string, attributes: ActionAttributes): void;
|
|
60
|
+
public metric(id: string, timestamp: string, attributes: MetricAttributes): void;
|
|
61
|
+
/** Flush one queue snapshot in bounded batches while preserving concurrent captures and retry bodies. */
|
|
62
|
+
public flush(transport?: Transport): Promise<TransportResponse>;
|
|
63
|
+
/** Reject new capture, flush queued events, then close; a failed flush reopens the intact remainder. */
|
|
64
|
+
public shutdown(transport?: Transport): Promise<TransportResponse>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { LogBrewClient };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ActionAttributes, EnvironmentAttributes, LogAttributes, MetricAttributes, ReleaseAttributes, SpanAttributes } from "./telemetry.js";
|
|
2
|
+
import type { IssueAttributes } from "./issues.js";
|
|
3
|
+
|
|
4
|
+
declare const DELIVERY_HEALTH_SCHEMA_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
/** Public event union used in preview and transport payloads. */
|
|
7
|
+
type Event =
|
|
8
|
+
| { type: "release"; id: string; timestamp: string; attributes: ReleaseAttributes }
|
|
9
|
+
| { type: "environment"; id: string; timestamp: string; attributes: EnvironmentAttributes }
|
|
10
|
+
| { type: "issue"; id: string; timestamp: string; attributes: IssueAttributes }
|
|
11
|
+
| { type: "log"; id: string; timestamp: string; attributes: LogAttributes }
|
|
12
|
+
| { type: "span"; id: string; timestamp: string; attributes: SpanAttributes }
|
|
13
|
+
| { type: "action"; id: string; timestamp: string; attributes: ActionAttributes }
|
|
14
|
+
| { type: "metric"; id: string; timestamp: string; attributes: MetricAttributes };
|
|
15
|
+
|
|
16
|
+
/** Drop-only event filter called after validation and before an event is queued. */
|
|
17
|
+
type EventFilter = (event: Event) => boolean | void;
|
|
18
|
+
|
|
19
|
+
/** Canonical compact event record exchanged with an app-owned synchronous persistence adapter. */
|
|
20
|
+
type StoredEvent = {
|
|
21
|
+
event: Event;
|
|
22
|
+
serializedEvent: string;
|
|
23
|
+
eventBytes: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Explicit synchronous persistence seam used to recover and acknowledge queued events safely. */
|
|
27
|
+
type EventStore = {
|
|
28
|
+
load(): StoredEvent[];
|
|
29
|
+
append(record: StoredEvent): void;
|
|
30
|
+
acknowledge(count: number): void;
|
|
31
|
+
/** Removes the records at ascending `load()` indexes; without it, fatal issues cannot evict queued events. */
|
|
32
|
+
remove?(indexes: number[]): void;
|
|
33
|
+
purge(): void;
|
|
34
|
+
close(): void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Queue drop notification emitted when a bounded in-memory queue is full. */
|
|
38
|
+
type DroppedEvent = {
|
|
39
|
+
reason: "event_too_large" | "evicted_for_fatal" | "queue_bytes_overflow" | "queue_overflow";
|
|
40
|
+
eventType: Event["type"];
|
|
41
|
+
eventId: string;
|
|
42
|
+
droppedEvents: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Stable transport response returned from flush and shutdown operations. */
|
|
46
|
+
type TransportResponse = {
|
|
47
|
+
/** Final HTTP-like status returned by the transport. */
|
|
48
|
+
statusCode: number;
|
|
49
|
+
/** Number of transport attempts used for the flush. */
|
|
50
|
+
attempts: number;
|
|
51
|
+
/** Number of distinct batches acknowledged; client flush/shutdown responses always include it. */
|
|
52
|
+
batches?: number;
|
|
53
|
+
/** Optional retry delay from a rate-limit response, in milliseconds. */
|
|
54
|
+
retryAfterMs?: number;
|
|
55
|
+
/** Optional `code` from a 429 response body, such as `rate_limited` or `usage_limit_exceeded`. */
|
|
56
|
+
errorCode?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Minimal transport interface accepted by flush and shutdown operations. */
|
|
60
|
+
type Transport = {
|
|
61
|
+
send(apiKey: string, body: string): TransportResponse | Promise<TransportResponse>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Content-free bounded delivery state with no event or sensitive transport fields. */
|
|
65
|
+
type DeliveryHealthSnapshot = Readonly<{
|
|
66
|
+
/** Stable schema discriminator for JSON consumers. */
|
|
67
|
+
schemaVersion: typeof DELIVERY_HEALTH_SCHEMA_VERSION;
|
|
68
|
+
automaticDelivery: boolean;
|
|
69
|
+
lifecycle: "active" | "shutting_down" | "closed";
|
|
70
|
+
deliveryState: "idle" | "queued" | "scheduled" | "in_flight" | "retrying" | "paused" | "accepted" | "failed" | "dropped";
|
|
71
|
+
storage: "memory" | "persistent";
|
|
72
|
+
queueEvents: number;
|
|
73
|
+
queueBytes: number;
|
|
74
|
+
/** Events and compact bytes loaded from persistence when this client started. */
|
|
75
|
+
hydratedEvents: number;
|
|
76
|
+
hydratedBytes: number;
|
|
77
|
+
droppedEvents: number;
|
|
78
|
+
droppedByReason: Readonly<{
|
|
79
|
+
event_too_large: number;
|
|
80
|
+
evicted_for_fatal: number;
|
|
81
|
+
queue_bytes_overflow: number;
|
|
82
|
+
queue_overflow: number;
|
|
83
|
+
}>;
|
|
84
|
+
lastDropReason: "none" | "event_too_large" | "evicted_for_fatal" | "queue_bytes_overflow" | "queue_overflow";
|
|
85
|
+
scheduled: boolean;
|
|
86
|
+
inFlight: boolean;
|
|
87
|
+
coalesced: boolean;
|
|
88
|
+
pendingOperations: number;
|
|
89
|
+
lastOutcome: "idle" | "empty" | "accepted" | "failed";
|
|
90
|
+
lastStatusClass: "none" | "success" | "client_error" | "server_error" | "network_error" | "transport_error" | "invalid_response" | "other_status";
|
|
91
|
+
pausedReason: "none" | "authentication" | "rate_limit" | "non_retryable";
|
|
92
|
+
consecutiveFailures: number;
|
|
93
|
+
/** Bounded transient retry delay; zero when no automatic retry is scheduled. */
|
|
94
|
+
retryDelayMs: number;
|
|
95
|
+
flushes: number;
|
|
96
|
+
failures: number;
|
|
97
|
+
attempts: number;
|
|
98
|
+
batches: number;
|
|
99
|
+
/** Events durably acknowledged since this client was created. */
|
|
100
|
+
acceptedEvents: number;
|
|
101
|
+
/** Bounded monotonic-within-client Unix milliseconds; zero until the transition occurs. */
|
|
102
|
+
lastAttemptAtUnixMs: number;
|
|
103
|
+
lastAcceptedAtUnixMs: number;
|
|
104
|
+
lastDroppedAtUnixMs: number;
|
|
105
|
+
}>;
|
|
106
|
+
|
|
107
|
+
/** Synchronous queue admission only; this result does not report delivery or retention. */
|
|
108
|
+
type IssueAdmission = Readonly<
|
|
109
|
+
| { status: "admitted" }
|
|
110
|
+
| { status: "filtered" }
|
|
111
|
+
| { status: "dropped"; reason: DroppedEvent["reason"] }
|
|
112
|
+
| { status: "unavailable" }
|
|
113
|
+
>;
|
|
114
|
+
|
|
115
|
+
export type {
|
|
116
|
+
DeliveryHealthSnapshot,
|
|
117
|
+
DroppedEvent,
|
|
118
|
+
Event,
|
|
119
|
+
EventFilter,
|
|
120
|
+
EventStore,
|
|
121
|
+
IssueAdmission,
|
|
122
|
+
StoredEvent,
|
|
123
|
+
Transport,
|
|
124
|
+
TransportResponse
|
|
125
|
+
};
|