@equipe-tech/observability 0.2.1 → 0.3.5
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/README.md +11 -9
- package/dist/BrowserEvents.d.ts +89 -12
- package/dist/BrowserEvents.js +67 -13
- package/dist/Correlation.d.ts +51 -0
- package/dist/Correlation.js +95 -0
- package/dist/Metrics.d.ts +19 -3
- package/dist/Metrics.js +16 -0
- package/dist/MetricsRuntime.d.ts +45 -3
- package/dist/MetricsRuntime.js +478 -120
- package/dist/PolicyOtlpLogger.d.ts +18 -0
- package/dist/PolicyOtlpLogger.js +142 -0
- package/dist/RedactionPolicy.d.ts +2 -9
- package/dist/RedactionPolicy.js +2 -246
- package/dist/ResourceIdentity.d.ts +47 -0
- package/dist/ResourceIdentity.js +82 -0
- package/dist/Telemetry.d.ts +18 -3
- package/dist/Telemetry.js +43 -25
- package/dist/TelemetryConfig.d.ts +11 -4
- package/dist/TelemetryConfig.js +38 -10
- package/dist/audit/AuditCommitDocument.d.ts +41 -0
- package/dist/audit/AuditCommitDocument.js +58 -0
- package/dist/audit/AuditDigest.d.ts +11 -0
- package/dist/audit/AuditDigest.js +25 -0
- package/dist/audit/AuditOutbox.d.ts +75 -0
- package/dist/audit/AuditOutbox.js +104 -0
- package/dist/audit/AuditPublisher.d.ts +49 -0
- package/dist/audit/AuditPublisher.js +60 -0
- package/dist/audit/AuditRecord.d.ts +128 -0
- package/dist/audit/AuditRecord.js +172 -0
- package/dist/audit/CommittedAuditRecordInternal.d.ts +11 -0
- package/dist/audit/CommittedAuditRecordInternal.js +13 -0
- package/dist/audit/InvalidAuditRecord.d.ts +12 -0
- package/dist/audit/InvalidAuditRecord.js +16 -0
- package/dist/audit/index.d.ts +7 -0
- package/dist/audit/index.js +7 -0
- package/dist/browser/BrowserClient.d.ts +82 -3
- package/dist/browser/BrowserClient.js +322 -28
- package/dist/browser/BrowserEventLimits.d.ts +8 -0
- package/dist/browser/BrowserEventLimits.js +8 -0
- package/dist/browser/BrowserMetricRecorder.d.ts +17 -0
- package/dist/browser/BrowserMetricRecorder.js +35 -0
- package/dist/browser/BrowserTelemetry.d.ts +40 -0
- package/dist/browser/BrowserTelemetry.js +105 -0
- package/dist/browser/ClientPolicy.d.ts +3 -0
- package/dist/browser/ClientPolicy.js +52 -0
- package/dist/browser/client.d.ts +3 -2
- package/dist/browser/client.js +2 -1
- package/dist/browser/index.d.ts +4 -37
- package/dist/browser/index.js +3 -95
- package/dist/contract/ContractCompatibility.d.ts +31 -0
- package/dist/contract/ContractCompatibility.js +274 -0
- package/dist/contract/ContractIndex.d.ts +54 -0
- package/dist/contract/ContractIndex.js +178 -0
- package/dist/contract/ContractSurface.d.ts +111 -0
- package/dist/contract/ContractSurface.js +116 -0
- package/dist/contract/EventName.d.ts +6 -0
- package/dist/contract/EventName.js +55 -0
- package/dist/contract/EventProducer.d.ts +123 -0
- package/dist/contract/EventProducer.js +209 -0
- package/dist/contract/MetricContractError.d.ts +19 -0
- package/dist/contract/MetricContractError.js +19 -0
- package/dist/contract/MetricDefinition.d.ts +61 -0
- package/dist/contract/MetricDefinition.js +91 -0
- package/dist/contract/MetricProducer.d.ts +42 -0
- package/dist/contract/MetricProducer.js +123 -0
- package/dist/contract/OrganizationEvents.d.ts +165 -0
- package/dist/contract/OrganizationEvents.js +97 -0
- package/dist/contract/TelemetryContract.d.ts +95 -0
- package/dist/contract/TelemetryContract.js +451 -0
- package/dist/contract/TelemetryContractError.d.ts +44 -0
- package/dist/contract/TelemetryContractError.js +64 -0
- package/dist/contract/TelemetryEvent.d.ts +59 -0
- package/dist/contract/TelemetryEvent.js +54 -0
- package/dist/contract/index.d.ts +11 -0
- package/dist/contract/index.js +11 -0
- package/dist/effect/WideEvent.d.ts +3 -0
- package/dist/effect/WideEvent.js +20 -0
- package/dist/effect/WideEventSink.d.ts +3 -0
- package/dist/effect/WideEventSink.js +79 -0
- package/dist/effect/index.d.ts +2 -0
- package/dist/effect/index.js +2 -0
- package/dist/index.d.ts +17 -2
- package/dist/index.js +11 -2
- package/dist/metrics/InstrumentGrammar.d.ts +5 -0
- package/dist/metrics/InstrumentGrammar.js +12 -0
- package/dist/metrics/MetricScalar.d.ts +3 -0
- package/dist/metrics/MetricScalar.js +5 -0
- package/dist/node/AuditDigest.d.ts +3 -0
- package/dist/node/AuditDigest.js +6 -0
- package/dist/node/BrowserEventIngest.d.ts +7 -2
- package/dist/node/BrowserEventIngest.js +111 -19
- package/dist/node/LocalCollector.d.ts +20 -0
- package/dist/node/LocalCollector.js +188 -0
- package/dist/node/Observability.d.ts +49 -0
- package/dist/node/Observability.js +190 -0
- package/dist/node/Runtime.d.ts +4 -1
- package/dist/node/index.d.ts +4 -0
- package/dist/node/index.js +3 -0
- package/dist/policy/BrowserFieldPolicy.d.ts +7 -0
- package/dist/policy/BrowserFieldPolicy.js +298 -0
- package/dist/policy/DataPolicy.d.ts +19 -0
- package/dist/policy/DataPolicy.js +254 -0
- package/dist/policy/DataPolicyError.d.ts +22 -0
- package/dist/policy/DataPolicyError.js +25 -0
- package/dist/policy/Deduplication.d.ts +13 -0
- package/dist/policy/Deduplication.js +44 -0
- package/dist/policy/DefectEnvelope.d.ts +25 -0
- package/dist/policy/DefectEnvelope.js +52 -0
- package/dist/policy/MetricLabelPolicy.d.ts +5 -0
- package/dist/policy/MetricLabelPolicy.js +65 -0
- package/dist/policy/PolicyTransform.d.ts +16 -0
- package/dist/policy/PolicyTransform.js +136 -0
- package/dist/policy/PolicyVocabulary.d.ts +10 -0
- package/dist/policy/PolicyVocabulary.js +84 -0
- package/dist/policy/ResourceAttributePolicy.d.ts +9 -0
- package/dist/policy/ResourceAttributePolicy.js +33 -0
- package/dist/policy/entrypoint.d.ts +2 -0
- package/dist/policy/entrypoint.js +2 -0
- package/dist/policy/index.d.ts +17 -0
- package/dist/policy/index.js +9 -0
- package/dist/profile/DataPolicy.d.ts +2 -0
- package/dist/profile/DataPolicy.js +1 -0
- package/dist/profile/EnvironmentPolicy.d.ts +22 -0
- package/dist/profile/EnvironmentPolicy.js +41 -0
- package/dist/profile/LifecycleRegistry.d.ts +24 -0
- package/dist/profile/LifecycleRegistry.js +240 -0
- package/dist/profile/ObservabilityAdapter.d.ts +115 -0
- package/dist/profile/ObservabilityAdapter.js +50 -0
- package/dist/profile/ObservabilityConfig.d.ts +68 -0
- package/dist/profile/ObservabilityConfig.js +125 -0
- package/dist/profile/ObservabilityConfigError.d.ts +23 -0
- package/dist/profile/ObservabilityConfigError.js +39 -0
- package/dist/profile/ObservabilityProfile.d.ts +49 -0
- package/dist/profile/ObservabilityProfile.js +121 -0
- package/dist/profile/ReactWebProfile.d.ts +8 -0
- package/dist/profile/ReactWebProfile.js +7 -0
- package/dist/profile/index.d.ts +5 -0
- package/dist/profile/index.js +5 -0
- package/dist/testing/conformance/ConformanceAssertions.d.ts +5 -0
- package/dist/testing/conformance/ConformanceAssertions.js +31 -0
- package/dist/testing/conformance/ConformanceFailure.d.ts +35 -0
- package/dist/testing/conformance/ConformanceFailure.js +56 -0
- package/dist/testing/conformance/ConformanceModel.d.ts +97 -0
- package/dist/testing/conformance/ConformanceModel.js +8 -0
- package/dist/testing/conformance/ConformanceRegistry.d.ts +13 -0
- package/dist/testing/conformance/ConformanceRegistry.js +190 -0
- package/dist/testing/conformance/ConformanceRunner.d.ts +5 -0
- package/dist/testing/conformance/ConformanceRunner.js +198 -0
- package/dist/testing/conformance/TelemetryEvidence.d.ts +91 -0
- package/dist/testing/conformance/TelemetryEvidence.js +370 -0
- package/dist/testing/conformance/index.d.ts +10 -0
- package/dist/testing/conformance/index.js +6 -0
- package/dist/testing/contract.d.ts +81 -0
- package/dist/testing/contract.js +93 -0
- package/dist/testing/deployedCanary.d.ts +1 -0
- package/dist/testing/deployedCanary.js +1 -0
- package/dist/testing/index.d.ts +40 -3
- package/dist/testing/index.js +122 -5
- package/dist/trace/HttpServerOtlpTracer.d.ts +42 -0
- package/dist/trace/HttpServerOtlpTracer.js +301 -0
- package/package.json +16 -29
- package/dist/WideEvent.d.ts +0 -5
- package/dist/WideEvent.js +0 -6
- package/dist/nestjs/BrowserEventsController.d.ts +0 -21
- package/dist/nestjs/BrowserEventsController.js +0 -52
- package/dist/nestjs/HttpRoutePolicy.d.ts +0 -23
- package/dist/nestjs/HttpRoutePolicy.js +0 -179
- package/dist/nestjs/HttpServerOtlpTracer.d.ts +0 -15
- package/dist/nestjs/HttpServerOtlpTracer.js +0 -154
- package/dist/nestjs/RequestWideEventTraceCorrelation.d.ts +0 -15
- package/dist/nestjs/RequestWideEventTraceCorrelation.js +0 -13
- package/dist/nestjs/TelemetryInterceptor.d.ts +0 -31
- package/dist/nestjs/TelemetryInterceptor.js +0 -268
- package/dist/nestjs/TelemetryModule.d.ts +0 -53
- package/dist/nestjs/TelemetryModule.js +0 -428
- package/dist/nestjs/index.d.ts +0 -5
- package/dist/nestjs/index.js +0 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { browserEnvelopeVersion, browserRequestByteBudget, maxEventIdLength, maxFieldValueLength, } from "./BrowserEventLimits.js";
|
|
2
|
+
import { sanitizeClientEventName, sanitizeClientFields } from "./ClientPolicy.js";
|
|
2
3
|
export class BrowserTelemetryClientDeliveryError extends Error {
|
|
3
4
|
retryable;
|
|
4
5
|
code = "OBS_BROWSER_EVENTS_DELIVERY_FAILED";
|
|
@@ -13,7 +14,7 @@ export class BrowserTelemetryClientShutdownError extends Error {
|
|
|
13
14
|
code = "OBS_BROWSER_EVENTS_SHUTDOWN_TIMEOUT";
|
|
14
15
|
retryable = true;
|
|
15
16
|
constructor(timeoutMs) {
|
|
16
|
-
super(`Browser telemetry shutdown exceeded ${timeoutMs} milliseconds
|
|
17
|
+
super(`Browser telemetry shutdown exceeded ${timeoutMs} milliseconds; pending sanitized events were dropped.`);
|
|
17
18
|
this.timeoutMs = timeoutMs;
|
|
18
19
|
this.name = "BrowserTelemetryClientShutdownError";
|
|
19
20
|
}
|
|
@@ -25,8 +26,62 @@ const defaultFlushIntervalMs = 5_000;
|
|
|
25
26
|
const defaultShutdownTimeoutMs = 2_000;
|
|
26
27
|
const maxBatchSizeLimit = 64;
|
|
27
28
|
const fallbackEventName = "browser.event";
|
|
29
|
+
const browserFieldValueByteBudget = 2_048;
|
|
30
|
+
const browserKeepaliveByteBudget = 64 * 1_024;
|
|
31
|
+
const textEncoder = new TextEncoder();
|
|
32
|
+
const boundedOperationalText = (value, fallback) => {
|
|
33
|
+
try {
|
|
34
|
+
return String(value ?? fallback).slice(0, maxFieldValueLength);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const browserBatchByteLength = (batch) => textEncoder.encode(JSON.stringify(batch)).byteLength;
|
|
41
|
+
const fitStringToByteBudget = (value) => {
|
|
42
|
+
if (textEncoder.encode(value).byteLength <= browserFieldValueByteBudget)
|
|
43
|
+
return value;
|
|
44
|
+
let output = "";
|
|
45
|
+
let bytes = 0;
|
|
46
|
+
for (const character of value) {
|
|
47
|
+
const characterBytes = textEncoder.encode(character).byteLength;
|
|
48
|
+
if (bytes + characterBytes > browserFieldValueByteBudget)
|
|
49
|
+
break;
|
|
50
|
+
output += character;
|
|
51
|
+
bytes += characterBytes;
|
|
52
|
+
}
|
|
53
|
+
return output;
|
|
54
|
+
};
|
|
55
|
+
const fitFieldValueToByteBudget = (value) => {
|
|
56
|
+
try {
|
|
57
|
+
return String(value) === value ? fitStringToByteBudget(value) : value;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return "[REDACTED]";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const fitEventToRequestBudget = (event) => {
|
|
64
|
+
const byteBoundedEvent = {
|
|
65
|
+
...event,
|
|
66
|
+
fields: Object.fromEntries(Object.entries(event.fields).map(([key, value]) => [key, fitFieldValueToByteBudget(value)])),
|
|
67
|
+
};
|
|
68
|
+
if (browserBatchByteLength({ version: browserEnvelopeVersion, events: [byteBoundedEvent] }) <=
|
|
69
|
+
browserRequestByteBudget) {
|
|
70
|
+
return byteBoundedEvent;
|
|
71
|
+
}
|
|
72
|
+
const fields = {};
|
|
73
|
+
for (const [key, value] of Object.entries(byteBoundedEvent.fields)) {
|
|
74
|
+
const candidate = { ...fields, [key]: value };
|
|
75
|
+
if (browserBatchByteLength({
|
|
76
|
+
version: browserEnvelopeVersion,
|
|
77
|
+
events: [{ ...byteBoundedEvent, fields: candidate }],
|
|
78
|
+
}) <= browserRequestByteBudget) {
|
|
79
|
+
fields[key] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { ...byteBoundedEvent, fields };
|
|
83
|
+
};
|
|
28
84
|
export const normalizePositiveInteger = (value, fallback) => value === undefined || !Number.isSafeInteger(value) || value <= 0 ? fallback : value;
|
|
29
|
-
const positiveInteger = normalizePositiveInteger;
|
|
30
85
|
const fetchTransport = (endpoint) => async (batch, signal) => {
|
|
31
86
|
let response;
|
|
32
87
|
try {
|
|
@@ -34,20 +89,29 @@ const fetchTransport = (endpoint) => async (batch, signal) => {
|
|
|
34
89
|
method: "POST",
|
|
35
90
|
headers: { "content-type": "application/json" },
|
|
36
91
|
body: JSON.stringify(batch),
|
|
37
|
-
keepalive:
|
|
92
|
+
keepalive: browserBatchByteLength(batch) <= browserKeepaliveByteBudget,
|
|
38
93
|
signal,
|
|
39
94
|
});
|
|
40
95
|
}
|
|
41
96
|
catch (cause) {
|
|
42
|
-
throw new BrowserTelemetryClientDeliveryError("
|
|
97
|
+
throw new BrowserTelemetryClientDeliveryError("Browser delivery failed; sanitized events remain queued.", true, { cause });
|
|
43
98
|
}
|
|
44
99
|
if (!response.ok) {
|
|
45
|
-
throw new BrowserTelemetryClientDeliveryError(`
|
|
100
|
+
throw new BrowserTelemetryClientDeliveryError(`Telemetry returned ${response.status}; verify /_telemetry/events.`, response.status === 429 || response.status >= 500, { cause: response.status });
|
|
46
101
|
}
|
|
47
102
|
};
|
|
103
|
+
const randomHex = (length) => {
|
|
104
|
+
let value = "";
|
|
105
|
+
while (value.length < length)
|
|
106
|
+
value += crypto.randomUUID().replaceAll("-", "");
|
|
107
|
+
return value.slice(0, length);
|
|
108
|
+
};
|
|
48
109
|
export class BrowserClientEngine {
|
|
49
110
|
options;
|
|
50
111
|
events = [];
|
|
112
|
+
spans = [];
|
|
113
|
+
metricPoints = [];
|
|
114
|
+
activeSpans = new Map();
|
|
51
115
|
activeFlush;
|
|
52
116
|
activeDelivery;
|
|
53
117
|
activeControllers = new Set();
|
|
@@ -63,21 +127,149 @@ export class BrowserClientEngine {
|
|
|
63
127
|
}, options.flushIntervalMs);
|
|
64
128
|
}
|
|
65
129
|
}
|
|
66
|
-
|
|
130
|
+
traces = {
|
|
131
|
+
startSpan: (name, fields = {}, parent) => {
|
|
132
|
+
const context = { traceId: parent?.traceId ?? randomHex(32), spanId: randomHex(16) };
|
|
133
|
+
const startedAt = Date.now();
|
|
134
|
+
const active = {
|
|
135
|
+
...context,
|
|
136
|
+
name: sanitizeClientEventName(name) || fallbackEventName,
|
|
137
|
+
startedAt,
|
|
138
|
+
fields: this.sanitizeFields(fields),
|
|
139
|
+
};
|
|
140
|
+
if (parent !== undefined)
|
|
141
|
+
Object.assign(active, { parentSpanId: parent.spanId });
|
|
142
|
+
if (!this.options.disabled && !this.disposed)
|
|
143
|
+
this.activeSpans.set(context.spanId, active);
|
|
144
|
+
return {
|
|
145
|
+
context,
|
|
146
|
+
end: (endFields = {}) => {
|
|
147
|
+
const current = this.activeSpans.get(context.spanId);
|
|
148
|
+
if (current === undefined || this.options.disabled || this.disposed)
|
|
149
|
+
return;
|
|
150
|
+
this.activeSpans.delete(context.spanId);
|
|
151
|
+
this.enqueueSpan({
|
|
152
|
+
...current,
|
|
153
|
+
endedAt: Date.now(),
|
|
154
|
+
fields: this.sanitizeFields({ ...current.fields, ...endFields }),
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
metrics = {
|
|
161
|
+
counter: (name) => ({
|
|
162
|
+
add: (value = 1, fields = {}) => {
|
|
163
|
+
if (this.options.disabled ||
|
|
164
|
+
this.disposed ||
|
|
165
|
+
!this.options.metrics ||
|
|
166
|
+
!Number.isFinite(value) ||
|
|
167
|
+
value < 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.enqueueMetric({
|
|
171
|
+
name: sanitizeClientEventName(name) || fallbackEventName,
|
|
172
|
+
value,
|
|
173
|
+
occurredAt: Date.now(),
|
|
174
|
+
fields: this.sanitizeFields(fields),
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
emit(name, fields = {}, trace) {
|
|
67
180
|
if (this.options.disabled || this.disposed)
|
|
68
181
|
return;
|
|
69
|
-
const sanitizedName =
|
|
182
|
+
const sanitizedName = sanitizeClientEventName(name);
|
|
70
183
|
const event = {
|
|
71
184
|
id: crypto.randomUUID(),
|
|
72
185
|
name: sanitizedName.length === 0 ? fallbackEventName : sanitizedName,
|
|
73
186
|
occurredAt: Date.now(),
|
|
74
|
-
fields:
|
|
187
|
+
fields: this.sanitizeFields(fields),
|
|
75
188
|
};
|
|
76
|
-
|
|
77
|
-
|
|
189
|
+
this.enqueue(trace === undefined ? event : { ...event, trace });
|
|
190
|
+
}
|
|
191
|
+
emitDefect(input) {
|
|
192
|
+
if (this.options.disabled || this.disposed)
|
|
193
|
+
return;
|
|
194
|
+
const sanitizedName = sanitizeClientEventName(input.name);
|
|
195
|
+
const id = input.id ?? crypto.randomUUID().replaceAll("-", "");
|
|
196
|
+
const operationalError = this.sanitizeFields({
|
|
197
|
+
"error.type": input.error.type,
|
|
198
|
+
"error.message": input.error.message,
|
|
199
|
+
});
|
|
200
|
+
const type = operationalError["error.type"];
|
|
201
|
+
const message = operationalError["error.message"];
|
|
202
|
+
this.enqueue({
|
|
203
|
+
id: id.slice(0, maxEventIdLength),
|
|
204
|
+
name: sanitizedName.length === 0 ? fallbackEventName : sanitizedName,
|
|
205
|
+
occurredAt: Date.now(),
|
|
206
|
+
fields: this.sanitizeFields(input.fields ?? {}),
|
|
207
|
+
error: {
|
|
208
|
+
type: boundedOperationalText(type, "Error"),
|
|
209
|
+
message: boundedOperationalText(message, ""),
|
|
210
|
+
retryable: input.error.retryable,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
sanitizeFields(fields) {
|
|
215
|
+
if (this.options.policy === undefined)
|
|
216
|
+
return sanitizeClientFields(fields);
|
|
217
|
+
return this.options.policy(fields);
|
|
218
|
+
}
|
|
219
|
+
batchWithResource(batch) {
|
|
220
|
+
return this.options.resource === undefined
|
|
221
|
+
? batch
|
|
222
|
+
: { ...batch, resource: this.options.resource };
|
|
223
|
+
}
|
|
224
|
+
deliveryBatch(events, spans, metrics) {
|
|
225
|
+
let batch = this.batchWithResource({ version: browserEnvelopeVersion, events });
|
|
226
|
+
if (spans.length > 0)
|
|
227
|
+
batch = { ...batch, spans };
|
|
228
|
+
if (metrics.length > 0)
|
|
229
|
+
batch = { ...batch, metrics };
|
|
230
|
+
return batch;
|
|
231
|
+
}
|
|
232
|
+
enqueue(event) {
|
|
233
|
+
const fitted = fitEventToRequestBudget(event);
|
|
234
|
+
if (browserBatchByteLength(this.batchWithResource({ version: browserEnvelopeVersion, events: [fitted] })) > browserRequestByteBudget) {
|
|
235
|
+
this.droppedEvents += 1;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.ensureQueueCapacity();
|
|
239
|
+
this.events.push(fitted);
|
|
240
|
+
}
|
|
241
|
+
enqueueSpan(span) {
|
|
242
|
+
const fitted = { ...span, fields: this.sanitizeFields(span.fields) };
|
|
243
|
+
if (browserBatchByteLength(this.batchWithResource({ version: browserEnvelopeVersion, events: [], spans: [fitted] })) > browserRequestByteBudget) {
|
|
244
|
+
this.droppedEvents += 1;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
this.ensureQueueCapacity();
|
|
248
|
+
this.spans.push(fitted);
|
|
249
|
+
}
|
|
250
|
+
enqueueMetric(metric) {
|
|
251
|
+
const fitted = { ...metric, fields: this.sanitizeFields(metric.fields) };
|
|
252
|
+
if (browserBatchByteLength(this.batchWithResource({
|
|
253
|
+
version: browserEnvelopeVersion,
|
|
254
|
+
events: [],
|
|
255
|
+
metrics: [fitted],
|
|
256
|
+
})) > browserRequestByteBudget) {
|
|
78
257
|
this.droppedEvents += 1;
|
|
258
|
+
return;
|
|
79
259
|
}
|
|
80
|
-
this.
|
|
260
|
+
this.ensureQueueCapacity();
|
|
261
|
+
this.metricPoints.push(fitted);
|
|
262
|
+
}
|
|
263
|
+
ensureQueueCapacity() {
|
|
264
|
+
if (this.pending() < this.options.maxQueueSize)
|
|
265
|
+
return;
|
|
266
|
+
if (this.events.length > 0)
|
|
267
|
+
this.events.shift();
|
|
268
|
+
else if (this.spans.length > 0)
|
|
269
|
+
this.spans.shift();
|
|
270
|
+
else
|
|
271
|
+
this.metricPoints.shift();
|
|
272
|
+
this.droppedEvents += 1;
|
|
81
273
|
}
|
|
82
274
|
flush() {
|
|
83
275
|
if (this.options.disabled || this.disposed)
|
|
@@ -85,7 +277,9 @@ export class BrowserClientEngine {
|
|
|
85
277
|
return this.flushQueued();
|
|
86
278
|
}
|
|
87
279
|
pending() {
|
|
88
|
-
return this.options.disabled
|
|
280
|
+
return this.options.disabled
|
|
281
|
+
? 0
|
|
282
|
+
: this.events.length + this.spans.length + this.metricPoints.length;
|
|
89
283
|
}
|
|
90
284
|
dropped() {
|
|
91
285
|
return this.droppedEvents;
|
|
@@ -93,12 +287,25 @@ export class BrowserClientEngine {
|
|
|
93
287
|
dispose() {
|
|
94
288
|
if (this.disposal !== undefined)
|
|
95
289
|
return this.disposal;
|
|
290
|
+
for (const span of this.activeSpans.values()) {
|
|
291
|
+
this.enqueueSpan({
|
|
292
|
+
...span,
|
|
293
|
+
endedAt: Date.now(),
|
|
294
|
+
fields: this.sanitizeFields({ ...span.fields, "span.forced_end": true }),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
this.activeSpans.clear();
|
|
96
298
|
this.disposed = true;
|
|
97
299
|
if (this.timer !== undefined) {
|
|
98
300
|
clearInterval(this.timer);
|
|
99
301
|
this.timer = undefined;
|
|
100
302
|
}
|
|
101
|
-
this.disposal = this.options.disabled
|
|
303
|
+
this.disposal = this.options.disabled
|
|
304
|
+
? Promise.resolve()
|
|
305
|
+
: this.shutdown().catch((cause) => {
|
|
306
|
+
this.dropPending();
|
|
307
|
+
throw cause;
|
|
308
|
+
});
|
|
102
309
|
return this.disposal;
|
|
103
310
|
}
|
|
104
311
|
async shutdown() {
|
|
@@ -122,6 +329,8 @@ export class BrowserClientEngine {
|
|
|
122
329
|
}
|
|
123
330
|
}
|
|
124
331
|
await Promise.race([this.flushQueued(true), deadline]);
|
|
332
|
+
if (this.pending() > 0)
|
|
333
|
+
this.dropPending();
|
|
125
334
|
}
|
|
126
335
|
finally {
|
|
127
336
|
if (deadlineTimer !== undefined)
|
|
@@ -132,34 +341,116 @@ export class BrowserClientEngine {
|
|
|
132
341
|
if (this.activeDelivery === undefined || this.activeDelivery.abandoned)
|
|
133
342
|
return;
|
|
134
343
|
this.activeDelivery.abandoned = true;
|
|
135
|
-
|
|
136
|
-
this.
|
|
137
|
-
this.
|
|
344
|
+
this.events = [...this.activeDelivery.batch.events, ...this.events];
|
|
345
|
+
this.spans = [...(this.activeDelivery.batch.spans ?? []), ...this.spans];
|
|
346
|
+
this.metricPoints = [...(this.activeDelivery.batch.metrics ?? []), ...this.metricPoints];
|
|
138
347
|
this.activeDelivery = undefined;
|
|
139
348
|
this.activeControllers.clear();
|
|
140
349
|
this.activeFlush = undefined;
|
|
141
350
|
}
|
|
351
|
+
dropPending() {
|
|
352
|
+
this.droppedEvents += this.pending();
|
|
353
|
+
this.events.length = 0;
|
|
354
|
+
this.spans.length = 0;
|
|
355
|
+
this.metricPoints.length = 0;
|
|
356
|
+
}
|
|
142
357
|
flushQueued(allowDisposed = false) {
|
|
143
358
|
if (this.activeFlush !== undefined)
|
|
144
359
|
return this.activeFlush;
|
|
145
360
|
const run = async () => {
|
|
146
|
-
while (this.
|
|
147
|
-
const batchEvents =
|
|
148
|
-
const
|
|
361
|
+
while (this.pending() > 0 && (!this.disposed || allowDisposed)) {
|
|
362
|
+
const batchEvents = [];
|
|
363
|
+
const batchSpans = [];
|
|
364
|
+
const batchMetrics = [];
|
|
365
|
+
const candidateBatch = () => this.deliveryBatch(batchEvents, batchSpans, batchMetrics);
|
|
366
|
+
const activeTraceIds = new Set(Array.from(this.activeSpans.values(), (span) => span.traceId));
|
|
367
|
+
const queuedTraceIds = new Set(this.spans.map((span) => span.traceId));
|
|
368
|
+
const staleCorrelatedEvents = this.events.filter((event) => event.trace !== undefined &&
|
|
369
|
+
!queuedTraceIds.has(event.trace.traceId) &&
|
|
370
|
+
!activeTraceIds.has(event.trace.traceId));
|
|
371
|
+
if (staleCorrelatedEvents.length > 0) {
|
|
372
|
+
const staleIds = new Set(staleCorrelatedEvents.map((event) => event.id));
|
|
373
|
+
this.events = this.events.filter((event) => !staleIds.has(event.id));
|
|
374
|
+
this.droppedEvents += staleCorrelatedEvents.length;
|
|
375
|
+
}
|
|
376
|
+
const readySpan = this.spans.find((span) => !activeTraceIds.has(span.traceId));
|
|
377
|
+
if (readySpan !== undefined) {
|
|
378
|
+
const traceId = readySpan.traceId;
|
|
379
|
+
const traceSpans = this.spans.filter((span) => span.traceId === traceId);
|
|
380
|
+
const traceEvents = this.events.filter((event) => event.trace?.traceId === traceId);
|
|
381
|
+
batchSpans.push(...traceSpans);
|
|
382
|
+
batchEvents.push(...traceEvents);
|
|
383
|
+
if (batchSpans.length > maxBatchSizeLimit ||
|
|
384
|
+
batchEvents.length > maxBatchSizeLimit ||
|
|
385
|
+
browserBatchByteLength(candidateBatch()) > browserRequestByteBudget) {
|
|
386
|
+
this.spans = this.spans.filter((span) => span.traceId !== traceId);
|
|
387
|
+
this.events = this.events.filter((event) => event.trace?.traceId !== traceId);
|
|
388
|
+
this.droppedEvents += traceSpans.length + traceEvents.length;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
this.spans = this.spans.filter((span) => span.traceId !== traceId);
|
|
392
|
+
this.events = this.events.filter((event) => event.trace?.traceId !== traceId);
|
|
393
|
+
}
|
|
394
|
+
for (const event of this.events) {
|
|
395
|
+
if (event.trace !== undefined)
|
|
396
|
+
continue;
|
|
397
|
+
if (batchEvents.length + batchSpans.length >= this.options.maxBatchSize)
|
|
398
|
+
break;
|
|
399
|
+
batchEvents.push(event);
|
|
400
|
+
if (browserBatchByteLength(candidateBatch()) > browserRequestByteBudget) {
|
|
401
|
+
batchEvents.pop();
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (batchEvents.length > 0) {
|
|
406
|
+
const selectedEventIds = new Set(batchEvents.map((event) => event.id));
|
|
407
|
+
this.events = this.events.filter((event) => !selectedEventIds.has(event.id));
|
|
408
|
+
}
|
|
409
|
+
while (batchEvents.length + batchSpans.length + batchMetrics.length <
|
|
410
|
+
this.options.maxBatchSize &&
|
|
411
|
+
this.metricPoints.length > 0) {
|
|
412
|
+
const metric = this.metricPoints[0];
|
|
413
|
+
if (metric === undefined)
|
|
414
|
+
break;
|
|
415
|
+
batchMetrics.push(metric);
|
|
416
|
+
if (browserBatchByteLength(candidateBatch()) > browserRequestByteBudget) {
|
|
417
|
+
batchMetrics.pop();
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
this.metricPoints.shift();
|
|
421
|
+
}
|
|
422
|
+
if (batchEvents.length === 0 && batchSpans.length === 0 && batchMetrics.length === 0)
|
|
423
|
+
return;
|
|
424
|
+
const batch = candidateBatch();
|
|
425
|
+
const delivery = { batch, abandoned: false };
|
|
149
426
|
const controller = new AbortController();
|
|
150
427
|
this.activeDelivery = delivery;
|
|
151
428
|
this.activeControllers.add(controller);
|
|
152
429
|
try {
|
|
153
|
-
await this.options.transport(
|
|
430
|
+
await this.options.transport(batch, controller.signal);
|
|
154
431
|
if (delivery.abandoned)
|
|
155
432
|
return;
|
|
156
433
|
}
|
|
157
434
|
catch (cause) {
|
|
158
435
|
if (delivery.abandoned)
|
|
159
436
|
return;
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
437
|
+
if (cause instanceof BrowserTelemetryClientDeliveryError && !cause.retryable) {
|
|
438
|
+
this.droppedEvents += batchEvents.length + batchSpans.length + batchMetrics.length;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
this.events = [...batchEvents, ...this.events];
|
|
442
|
+
this.spans = [...batchSpans, ...this.spans];
|
|
443
|
+
this.metricPoints = [...batchMetrics, ...this.metricPoints];
|
|
444
|
+
while (this.pending() > this.options.maxQueueSize) {
|
|
445
|
+
if (this.metricPoints.length > 0)
|
|
446
|
+
this.metricPoints.pop();
|
|
447
|
+
else if (this.spans.length > 0)
|
|
448
|
+
this.spans.pop();
|
|
449
|
+
else
|
|
450
|
+
this.events.pop();
|
|
451
|
+
this.droppedEvents += 1;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
163
454
|
throw cause;
|
|
164
455
|
}
|
|
165
456
|
finally {
|
|
@@ -176,14 +467,17 @@ export class BrowserClientEngine {
|
|
|
176
467
|
}
|
|
177
468
|
}
|
|
178
469
|
export const createBrowserTelemetryClient = (config = {}) => {
|
|
179
|
-
const maxBatchSize = Math.min(
|
|
470
|
+
const maxBatchSize = Math.min(normalizePositiveInteger(config.maxBatchSize, defaultMaxBatchSize), maxBatchSizeLimit);
|
|
180
471
|
return new BrowserClientEngine({
|
|
181
472
|
disabled: config.disabled ?? false,
|
|
182
473
|
maxBatchSize,
|
|
183
|
-
maxQueueSize:
|
|
184
|
-
flushIntervalMs:
|
|
185
|
-
shutdownTimeoutMs:
|
|
474
|
+
maxQueueSize: normalizePositiveInteger(config.maxQueueSize, defaultMaxQueueSize),
|
|
475
|
+
flushIntervalMs: normalizePositiveInteger(config.flushIntervalMs, defaultFlushIntervalMs),
|
|
476
|
+
shutdownTimeoutMs: normalizePositiveInteger(config.shutdownTimeoutMs, defaultShutdownTimeoutMs),
|
|
477
|
+
metrics: config.metrics ?? false,
|
|
478
|
+
resource: config.resource,
|
|
186
479
|
transport: config.transport ?? fetchTransport(config.endpoint ?? defaultEndpoint),
|
|
480
|
+
policy: config.policy,
|
|
187
481
|
startTimer: true,
|
|
188
482
|
});
|
|
189
483
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const browserEnvelopeVersion = 2;
|
|
2
|
+
export declare const browserRequestByteBudget = 90000;
|
|
3
|
+
export declare const maxEventsPerBatch = 64;
|
|
4
|
+
export declare const maxFieldsPerEvent = 32;
|
|
5
|
+
export declare const maxFieldKeyLength = 128;
|
|
6
|
+
export declare const maxFieldValueLength = 1024;
|
|
7
|
+
export declare const maxEventNameLength = 128;
|
|
8
|
+
export declare const maxEventIdLength = 64;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const browserEnvelopeVersion = 2;
|
|
2
|
+
export const browserRequestByteBudget = 90_000;
|
|
3
|
+
export const maxEventsPerBatch = 64;
|
|
4
|
+
export const maxFieldsPerEvent = 32;
|
|
5
|
+
export const maxFieldKeyLength = 128;
|
|
6
|
+
export const maxFieldValueLength = 1024;
|
|
7
|
+
export const maxEventNameLength = 128;
|
|
8
|
+
export const maxEventIdLength = 64;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context, Layer } from "effect";
|
|
2
|
+
import type { BrowserMetricPoint } from "../BrowserEvents.js";
|
|
3
|
+
import { LayerMetricsRuntime } from "../MetricsRuntime.js";
|
|
4
|
+
import type { ContractRegistry } from "../profile/ObservabilityAdapter.js";
|
|
5
|
+
export type BrowserMetricBatchAdmission = {
|
|
6
|
+
readonly commit: () => void;
|
|
7
|
+
};
|
|
8
|
+
declare const BrowserMetricRecorder_base: Context.ServiceClass<BrowserMetricRecorder, "@equipe-tech/observability/node/BrowserMetricRecorder", {
|
|
9
|
+
readonly admit: (metrics: ReadonlyArray<BrowserMetricPoint>) => BrowserMetricBatchAdmission;
|
|
10
|
+
}>;
|
|
11
|
+
export declare class BrowserMetricRecorder extends BrowserMetricRecorder_base {
|
|
12
|
+
}
|
|
13
|
+
export declare const unavailableBrowserMetricRecorder: {
|
|
14
|
+
readonly admit: (metrics: ReadonlyArray<BrowserMetricPoint>) => BrowserMetricBatchAdmission;
|
|
15
|
+
};
|
|
16
|
+
export declare const layerBrowserMetricRecorder: (contract: ContractRegistry | undefined) => Layer.Layer<BrowserMetricRecorder, never, LayerMetricsRuntime>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { InvalidMetricMeasurement } from "../contract/MetricContractError.js";
|
|
3
|
+
import { prepareContractCounterBatchByName } from "../contract/MetricProducer.js";
|
|
4
|
+
import { LayerMetricsRuntime, releaseMetricsLease } from "../MetricsRuntime.js";
|
|
5
|
+
export class BrowserMetricRecorder extends Context.Service()("@equipe-tech/observability/node/BrowserMetricRecorder") {
|
|
6
|
+
}
|
|
7
|
+
export const unavailableBrowserMetricRecorder = BrowserMetricRecorder.of({
|
|
8
|
+
admit: () => {
|
|
9
|
+
throw new InvalidMetricMeasurement({
|
|
10
|
+
code: "OBS_METRIC_UNKNOWN_ALIAS",
|
|
11
|
+
operation: "counter",
|
|
12
|
+
message: "Browser metric admission requires a compiled telemetry contract. Configure the owning contract before sending metrics.",
|
|
13
|
+
metricAlias: "browser",
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
export const layerBrowserMetricRecorder = (contract) => {
|
|
18
|
+
if (contract === undefined)
|
|
19
|
+
return Layer.succeed(BrowserMetricRecorder, unavailableBrowserMetricRecorder);
|
|
20
|
+
return Layer.effect(BrowserMetricRecorder, Effect.acquireRelease(Effect.map(LayerMetricsRuntime, (runtime) => {
|
|
21
|
+
const metrics = runtime.acquireMetrics();
|
|
22
|
+
return {
|
|
23
|
+
service: BrowserMetricRecorder.of({
|
|
24
|
+
admit: (points) => ({
|
|
25
|
+
commit: prepareContractCounterBatchByName(contract, metrics, points.map((point) => ({
|
|
26
|
+
name: point.name,
|
|
27
|
+
value: point.value,
|
|
28
|
+
attributes: point.fields,
|
|
29
|
+
}))),
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
metrics,
|
|
33
|
+
};
|
|
34
|
+
}), ({ metrics }) => Effect.promise(() => releaseMetricsLease(metrics)).pipe(Effect.asVoid)).pipe(Effect.map(({ service }) => service)));
|
|
35
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Cause, Context, Duration, Effect, Layer, Schema } from "effect";
|
|
2
|
+
import { BrowserEventBatch } from "../BrowserEvents.js";
|
|
3
|
+
import type { EventAttributes } from "../contract/TelemetryEvent.js";
|
|
4
|
+
import { type DataPolicyInput } from "../policy/DataPolicy.js";
|
|
5
|
+
export { BrowserTelemetryClientDeliveryError, BrowserTelemetryClientShutdownError, browserBatchByteLength, createBrowserTelemetryClient, } from "./BrowserClient.js";
|
|
6
|
+
export type { BrowserTelemetryClient, BrowserTelemetryClientBatch, BrowserTelemetryClientConfig, BrowserTelemetryClientError, BrowserTelemetryClientEvent, BrowserTelemetryClientFields, BrowserTelemetryClientMetric, BrowserTelemetryClientResource, BrowserTelemetryClientSpan, BrowserTelemetryDefectInput, BrowserCounter, BrowserTraceContext, BrowserTraceHandle, BrowserTelemetryFieldTransform, BrowserTelemetryClientTransport, } from "./BrowserClient.js";
|
|
7
|
+
export { BrowserEvent, BrowserEventBatch, BrowserEventError, BrowserMetricPoint, BrowserResourceIdentity, BrowserTraceContext as BrowserTraceContextSchema, BrowserTraceSpan, browserRequestByteBudget, maxEventNameLength, maxEventsPerBatch, maxBrowserEventOccurredAt, maxFieldKeyLength, maxFieldsPerEvent, maxFieldValueLength, } from "../BrowserEvents.js";
|
|
8
|
+
export declare const defaultEventsEndpoint = "/_telemetry/events";
|
|
9
|
+
declare const BrowserEventDeliveryError_base: Schema.Class<BrowserEventDeliveryError, Schema.TaggedStruct<"BrowserEventDeliveryError", {
|
|
10
|
+
readonly code: Schema.Literal<"OBS_BROWSER_EVENTS_DELIVERY_FAILED">;
|
|
11
|
+
readonly message: Schema.String;
|
|
12
|
+
readonly retryable: Schema.Boolean;
|
|
13
|
+
readonly cause: Schema.Defect;
|
|
14
|
+
}>, Cause.YieldableError>;
|
|
15
|
+
export declare class BrowserEventDeliveryError extends BrowserEventDeliveryError_base {
|
|
16
|
+
}
|
|
17
|
+
declare const BrowserEventTransport_base: Context.ServiceClass<BrowserEventTransport, "@equipe-tech/observability/BrowserEventTransport", {
|
|
18
|
+
send(batch: BrowserEventBatch): Effect.Effect<void, BrowserEventDeliveryError>;
|
|
19
|
+
}>;
|
|
20
|
+
export declare class BrowserEventTransport extends BrowserEventTransport_base {
|
|
21
|
+
static readonly layerFetch: (options?: {
|
|
22
|
+
readonly endpoint?: string;
|
|
23
|
+
}) => Layer.Layer<BrowserEventTransport>;
|
|
24
|
+
}
|
|
25
|
+
export type BrowserTelemetryOptions = {
|
|
26
|
+
readonly maxBatchSize?: number;
|
|
27
|
+
readonly maxQueueSize?: number;
|
|
28
|
+
readonly flushInterval?: Duration.Input;
|
|
29
|
+
readonly metrics?: boolean;
|
|
30
|
+
readonly policy?: DataPolicyInput;
|
|
31
|
+
};
|
|
32
|
+
declare const BrowserTelemetry_base: Context.ServiceClass<BrowserTelemetry, "@equipe-tech/observability/BrowserTelemetry", {
|
|
33
|
+
emit(name: string, fields?: EventAttributes): Effect.Effect<void>;
|
|
34
|
+
flush(): Effect.Effect<void, BrowserEventDeliveryError>;
|
|
35
|
+
pending(): Effect.Effect<number>;
|
|
36
|
+
dropped(): Effect.Effect<number>;
|
|
37
|
+
}>;
|
|
38
|
+
export declare class BrowserTelemetry extends BrowserTelemetry_base {
|
|
39
|
+
static readonly layer: (options?: BrowserTelemetryOptions) => Layer.Layer<BrowserTelemetry, never, BrowserEventTransport>;
|
|
40
|
+
}
|