@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.
Files changed (177) hide show
  1. package/README.md +11 -9
  2. package/dist/BrowserEvents.d.ts +89 -12
  3. package/dist/BrowserEvents.js +67 -13
  4. package/dist/Correlation.d.ts +51 -0
  5. package/dist/Correlation.js +95 -0
  6. package/dist/Metrics.d.ts +19 -3
  7. package/dist/Metrics.js +16 -0
  8. package/dist/MetricsRuntime.d.ts +45 -3
  9. package/dist/MetricsRuntime.js +478 -120
  10. package/dist/PolicyOtlpLogger.d.ts +18 -0
  11. package/dist/PolicyOtlpLogger.js +142 -0
  12. package/dist/RedactionPolicy.d.ts +2 -9
  13. package/dist/RedactionPolicy.js +2 -246
  14. package/dist/ResourceIdentity.d.ts +47 -0
  15. package/dist/ResourceIdentity.js +82 -0
  16. package/dist/Telemetry.d.ts +18 -3
  17. package/dist/Telemetry.js +43 -25
  18. package/dist/TelemetryConfig.d.ts +11 -4
  19. package/dist/TelemetryConfig.js +38 -10
  20. package/dist/audit/AuditCommitDocument.d.ts +41 -0
  21. package/dist/audit/AuditCommitDocument.js +58 -0
  22. package/dist/audit/AuditDigest.d.ts +11 -0
  23. package/dist/audit/AuditDigest.js +25 -0
  24. package/dist/audit/AuditOutbox.d.ts +75 -0
  25. package/dist/audit/AuditOutbox.js +104 -0
  26. package/dist/audit/AuditPublisher.d.ts +49 -0
  27. package/dist/audit/AuditPublisher.js +60 -0
  28. package/dist/audit/AuditRecord.d.ts +128 -0
  29. package/dist/audit/AuditRecord.js +172 -0
  30. package/dist/audit/CommittedAuditRecordInternal.d.ts +11 -0
  31. package/dist/audit/CommittedAuditRecordInternal.js +13 -0
  32. package/dist/audit/InvalidAuditRecord.d.ts +12 -0
  33. package/dist/audit/InvalidAuditRecord.js +16 -0
  34. package/dist/audit/index.d.ts +7 -0
  35. package/dist/audit/index.js +7 -0
  36. package/dist/browser/BrowserClient.d.ts +82 -3
  37. package/dist/browser/BrowserClient.js +322 -28
  38. package/dist/browser/BrowserEventLimits.d.ts +8 -0
  39. package/dist/browser/BrowserEventLimits.js +8 -0
  40. package/dist/browser/BrowserMetricRecorder.d.ts +17 -0
  41. package/dist/browser/BrowserMetricRecorder.js +35 -0
  42. package/dist/browser/BrowserTelemetry.d.ts +40 -0
  43. package/dist/browser/BrowserTelemetry.js +105 -0
  44. package/dist/browser/ClientPolicy.d.ts +3 -0
  45. package/dist/browser/ClientPolicy.js +52 -0
  46. package/dist/browser/client.d.ts +3 -2
  47. package/dist/browser/client.js +2 -1
  48. package/dist/browser/index.d.ts +4 -37
  49. package/dist/browser/index.js +3 -95
  50. package/dist/contract/ContractCompatibility.d.ts +31 -0
  51. package/dist/contract/ContractCompatibility.js +274 -0
  52. package/dist/contract/ContractIndex.d.ts +54 -0
  53. package/dist/contract/ContractIndex.js +178 -0
  54. package/dist/contract/ContractSurface.d.ts +111 -0
  55. package/dist/contract/ContractSurface.js +116 -0
  56. package/dist/contract/EventName.d.ts +6 -0
  57. package/dist/contract/EventName.js +55 -0
  58. package/dist/contract/EventProducer.d.ts +123 -0
  59. package/dist/contract/EventProducer.js +209 -0
  60. package/dist/contract/MetricContractError.d.ts +19 -0
  61. package/dist/contract/MetricContractError.js +19 -0
  62. package/dist/contract/MetricDefinition.d.ts +61 -0
  63. package/dist/contract/MetricDefinition.js +91 -0
  64. package/dist/contract/MetricProducer.d.ts +42 -0
  65. package/dist/contract/MetricProducer.js +123 -0
  66. package/dist/contract/OrganizationEvents.d.ts +165 -0
  67. package/dist/contract/OrganizationEvents.js +97 -0
  68. package/dist/contract/TelemetryContract.d.ts +95 -0
  69. package/dist/contract/TelemetryContract.js +451 -0
  70. package/dist/contract/TelemetryContractError.d.ts +44 -0
  71. package/dist/contract/TelemetryContractError.js +64 -0
  72. package/dist/contract/TelemetryEvent.d.ts +59 -0
  73. package/dist/contract/TelemetryEvent.js +54 -0
  74. package/dist/contract/index.d.ts +11 -0
  75. package/dist/contract/index.js +11 -0
  76. package/dist/effect/WideEvent.d.ts +3 -0
  77. package/dist/effect/WideEvent.js +20 -0
  78. package/dist/effect/WideEventSink.d.ts +3 -0
  79. package/dist/effect/WideEventSink.js +79 -0
  80. package/dist/effect/index.d.ts +2 -0
  81. package/dist/effect/index.js +2 -0
  82. package/dist/index.d.ts +17 -2
  83. package/dist/index.js +11 -2
  84. package/dist/metrics/InstrumentGrammar.d.ts +5 -0
  85. package/dist/metrics/InstrumentGrammar.js +12 -0
  86. package/dist/metrics/MetricScalar.d.ts +3 -0
  87. package/dist/metrics/MetricScalar.js +5 -0
  88. package/dist/node/AuditDigest.d.ts +3 -0
  89. package/dist/node/AuditDigest.js +6 -0
  90. package/dist/node/BrowserEventIngest.d.ts +7 -2
  91. package/dist/node/BrowserEventIngest.js +111 -19
  92. package/dist/node/LocalCollector.d.ts +20 -0
  93. package/dist/node/LocalCollector.js +188 -0
  94. package/dist/node/Observability.d.ts +49 -0
  95. package/dist/node/Observability.js +190 -0
  96. package/dist/node/Runtime.d.ts +4 -1
  97. package/dist/node/index.d.ts +4 -0
  98. package/dist/node/index.js +3 -0
  99. package/dist/policy/BrowserFieldPolicy.d.ts +7 -0
  100. package/dist/policy/BrowserFieldPolicy.js +298 -0
  101. package/dist/policy/DataPolicy.d.ts +19 -0
  102. package/dist/policy/DataPolicy.js +254 -0
  103. package/dist/policy/DataPolicyError.d.ts +22 -0
  104. package/dist/policy/DataPolicyError.js +25 -0
  105. package/dist/policy/Deduplication.d.ts +13 -0
  106. package/dist/policy/Deduplication.js +44 -0
  107. package/dist/policy/DefectEnvelope.d.ts +25 -0
  108. package/dist/policy/DefectEnvelope.js +52 -0
  109. package/dist/policy/MetricLabelPolicy.d.ts +5 -0
  110. package/dist/policy/MetricLabelPolicy.js +65 -0
  111. package/dist/policy/PolicyTransform.d.ts +16 -0
  112. package/dist/policy/PolicyTransform.js +136 -0
  113. package/dist/policy/PolicyVocabulary.d.ts +10 -0
  114. package/dist/policy/PolicyVocabulary.js +84 -0
  115. package/dist/policy/ResourceAttributePolicy.d.ts +9 -0
  116. package/dist/policy/ResourceAttributePolicy.js +33 -0
  117. package/dist/policy/entrypoint.d.ts +2 -0
  118. package/dist/policy/entrypoint.js +2 -0
  119. package/dist/policy/index.d.ts +17 -0
  120. package/dist/policy/index.js +9 -0
  121. package/dist/profile/DataPolicy.d.ts +2 -0
  122. package/dist/profile/DataPolicy.js +1 -0
  123. package/dist/profile/EnvironmentPolicy.d.ts +22 -0
  124. package/dist/profile/EnvironmentPolicy.js +41 -0
  125. package/dist/profile/LifecycleRegistry.d.ts +24 -0
  126. package/dist/profile/LifecycleRegistry.js +240 -0
  127. package/dist/profile/ObservabilityAdapter.d.ts +115 -0
  128. package/dist/profile/ObservabilityAdapter.js +50 -0
  129. package/dist/profile/ObservabilityConfig.d.ts +68 -0
  130. package/dist/profile/ObservabilityConfig.js +125 -0
  131. package/dist/profile/ObservabilityConfigError.d.ts +23 -0
  132. package/dist/profile/ObservabilityConfigError.js +39 -0
  133. package/dist/profile/ObservabilityProfile.d.ts +49 -0
  134. package/dist/profile/ObservabilityProfile.js +121 -0
  135. package/dist/profile/ReactWebProfile.d.ts +8 -0
  136. package/dist/profile/ReactWebProfile.js +7 -0
  137. package/dist/profile/index.d.ts +5 -0
  138. package/dist/profile/index.js +5 -0
  139. package/dist/testing/conformance/ConformanceAssertions.d.ts +5 -0
  140. package/dist/testing/conformance/ConformanceAssertions.js +31 -0
  141. package/dist/testing/conformance/ConformanceFailure.d.ts +35 -0
  142. package/dist/testing/conformance/ConformanceFailure.js +56 -0
  143. package/dist/testing/conformance/ConformanceModel.d.ts +97 -0
  144. package/dist/testing/conformance/ConformanceModel.js +8 -0
  145. package/dist/testing/conformance/ConformanceRegistry.d.ts +13 -0
  146. package/dist/testing/conformance/ConformanceRegistry.js +190 -0
  147. package/dist/testing/conformance/ConformanceRunner.d.ts +5 -0
  148. package/dist/testing/conformance/ConformanceRunner.js +198 -0
  149. package/dist/testing/conformance/TelemetryEvidence.d.ts +91 -0
  150. package/dist/testing/conformance/TelemetryEvidence.js +370 -0
  151. package/dist/testing/conformance/index.d.ts +10 -0
  152. package/dist/testing/conformance/index.js +6 -0
  153. package/dist/testing/contract.d.ts +81 -0
  154. package/dist/testing/contract.js +93 -0
  155. package/dist/testing/deployedCanary.d.ts +1 -0
  156. package/dist/testing/deployedCanary.js +1 -0
  157. package/dist/testing/index.d.ts +40 -3
  158. package/dist/testing/index.js +122 -5
  159. package/dist/trace/HttpServerOtlpTracer.d.ts +42 -0
  160. package/dist/trace/HttpServerOtlpTracer.js +301 -0
  161. package/package.json +16 -29
  162. package/dist/WideEvent.d.ts +0 -5
  163. package/dist/WideEvent.js +0 -6
  164. package/dist/nestjs/BrowserEventsController.d.ts +0 -21
  165. package/dist/nestjs/BrowserEventsController.js +0 -52
  166. package/dist/nestjs/HttpRoutePolicy.d.ts +0 -23
  167. package/dist/nestjs/HttpRoutePolicy.js +0 -179
  168. package/dist/nestjs/HttpServerOtlpTracer.d.ts +0 -15
  169. package/dist/nestjs/HttpServerOtlpTracer.js +0 -154
  170. package/dist/nestjs/RequestWideEventTraceCorrelation.d.ts +0 -15
  171. package/dist/nestjs/RequestWideEventTraceCorrelation.js +0 -13
  172. package/dist/nestjs/TelemetryInterceptor.d.ts +0 -31
  173. package/dist/nestjs/TelemetryInterceptor.js +0 -268
  174. package/dist/nestjs/TelemetryModule.d.ts +0 -53
  175. package/dist/nestjs/TelemetryModule.js +0 -428
  176. package/dist/nestjs/index.d.ts +0 -5
  177. package/dist/nestjs/index.js +0 -4
@@ -1,4 +1,5 @@
1
- import { sanitizeBrowserEventName, sanitizeBrowserFields } from "../RedactionPolicy.js";
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. The client aborted active delivery and retained its sanitized batch.`);
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: true,
92
+ keepalive: browserBatchByteLength(batch) <= browserKeepaliveByteBudget,
38
93
  signal,
39
94
  });
40
95
  }
41
96
  catch (cause) {
42
- throw new BrowserTelemetryClientDeliveryError("The browser events could not be sent. The events stay queued and the next flush retries the same batch.", true, { cause });
97
+ throw new BrowserTelemetryClientDeliveryError("Browser delivery failed; sanitized events remain queued.", true, { cause });
43
98
  }
44
99
  if (!response.ok) {
45
- throw new BrowserTelemetryClientDeliveryError(`The telemetry endpoint rejected the batch with status ${response.status}. Check the /_telemetry/events route of the project API.`, response.status === 429 || response.status >= 500, { cause: response.status });
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
- emit(name, fields = {}) {
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 = sanitizeBrowserEventName(name);
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: sanitizeBrowserFields(fields),
187
+ fields: this.sanitizeFields(fields),
75
188
  };
76
- if (this.events.length >= this.options.maxQueueSize) {
77
- this.events.shift();
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.events.push(event);
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 ? 0 : this.events.length;
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 ? Promise.resolve() : this.shutdown();
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
- const requeued = [...this.activeDelivery.events, ...this.events];
136
- this.events = requeued.slice(0, this.options.maxQueueSize);
137
- this.droppedEvents += Math.max(0, requeued.length - this.options.maxQueueSize);
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.events.length > 0 && (!this.disposed || allowDisposed)) {
147
- const batchEvents = this.events.splice(0, this.options.maxBatchSize);
148
- const delivery = { events: batchEvents, abandoned: false };
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({ version: 1, events: batchEvents }, controller.signal);
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
- const requeued = [...batchEvents, ...this.events];
161
- this.events = requeued.slice(0, this.options.maxQueueSize);
162
- this.droppedEvents += Math.max(0, requeued.length - this.options.maxQueueSize);
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(positiveInteger(config.maxBatchSize, defaultMaxBatchSize), maxBatchSizeLimit);
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: positiveInteger(config.maxQueueSize, defaultMaxQueueSize),
184
- flushIntervalMs: positiveInteger(config.flushIntervalMs, defaultFlushIntervalMs),
185
- shutdownTimeoutMs: positiveInteger(config.shutdownTimeoutMs, defaultShutdownTimeoutMs),
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
+ }