@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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Cause, Context, Duration, Effect, Exit, Layer, Option, Schema } from "effect";
|
|
2
|
+
import { BrowserEvent, BrowserEventBatch, browserEnvelopeVersion, encodeBrowserEventBatch, maxEventsPerBatch, } from "../BrowserEvents.js";
|
|
3
|
+
import { parseDataPolicy } from "../policy/DataPolicy.js";
|
|
4
|
+
import { transformSignalFields } from "../policy/PolicyTransform.js";
|
|
5
|
+
import { BrowserClientEngine, normalizePositiveInteger } from "./BrowserClient.js";
|
|
6
|
+
export { BrowserTelemetryClientDeliveryError, BrowserTelemetryClientShutdownError, browserBatchByteLength, createBrowserTelemetryClient, } 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 const defaultEventsEndpoint = "/_telemetry/events";
|
|
9
|
+
export class BrowserEventDeliveryError extends Schema.TaggedError()("BrowserEventDeliveryError", {
|
|
10
|
+
code: Schema.Literal("OBS_BROWSER_EVENTS_DELIVERY_FAILED"),
|
|
11
|
+
message: Schema.String,
|
|
12
|
+
retryable: Schema.Boolean,
|
|
13
|
+
cause: Schema.Defect(),
|
|
14
|
+
}) {
|
|
15
|
+
}
|
|
16
|
+
export class BrowserEventTransport extends Context.Service()("@equipe-tech/observability/BrowserEventTransport") {
|
|
17
|
+
static layerFetch = (options) => {
|
|
18
|
+
const endpoint = options?.endpoint ?? defaultEventsEndpoint;
|
|
19
|
+
return Layer.succeed(BrowserEventTransport, BrowserEventTransport.of({
|
|
20
|
+
send: (batch) => Effect.gen(function* () {
|
|
21
|
+
const payload = yield* encodeBrowserEventBatch(batch).pipe(Effect.orDie);
|
|
22
|
+
const response = yield* Effect.tryPromise({
|
|
23
|
+
try: (signal) => fetch(endpoint, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify(payload),
|
|
27
|
+
keepalive: true,
|
|
28
|
+
signal,
|
|
29
|
+
}),
|
|
30
|
+
catch: (cause) => new BrowserEventDeliveryError({
|
|
31
|
+
code: "OBS_BROWSER_EVENTS_DELIVERY_FAILED",
|
|
32
|
+
message: "The browser events could not be sent. The events stay queued and the next flush retries the same batch.",
|
|
33
|
+
retryable: true,
|
|
34
|
+
cause,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
return yield* new BrowserEventDeliveryError({
|
|
39
|
+
code: "OBS_BROWSER_EVENTS_DELIVERY_FAILED",
|
|
40
|
+
message: `The telemetry endpoint rejected the batch with status ${response.status}. Check the /_telemetry/events route of the project API.`,
|
|
41
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
42
|
+
cause: response.status,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const makeBrowserTelemetry = Effect.fn("makeBrowserTelemetry")(function* (options) {
|
|
50
|
+
const transport = yield* BrowserEventTransport;
|
|
51
|
+
const flushInterval = Duration.fromInputUnsafe(options?.flushInterval ?? "5 seconds");
|
|
52
|
+
const policy = options?.policy === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: yield* parseDataPolicy(options.policy).pipe(Effect.orDie);
|
|
55
|
+
const engine = new BrowserClientEngine({
|
|
56
|
+
disabled: false,
|
|
57
|
+
maxBatchSize: Math.min(normalizePositiveInteger(options?.maxBatchSize, 32), maxEventsPerBatch),
|
|
58
|
+
maxQueueSize: normalizePositiveInteger(options?.maxQueueSize, 256),
|
|
59
|
+
flushIntervalMs: normalizePositiveInteger(Duration.toMillis(flushInterval), 5_000),
|
|
60
|
+
shutdownTimeoutMs: 2_000,
|
|
61
|
+
metrics: options?.metrics ?? false,
|
|
62
|
+
resource: undefined,
|
|
63
|
+
policy: policy === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: (fields) => transformSignalFields(policy, "browser-ingest", fields).value,
|
|
66
|
+
transport: (batch) => new Promise((resolve, reject) => {
|
|
67
|
+
Effect.runCallback(transport.send(new BrowserEventBatch({
|
|
68
|
+
version: browserEnvelopeVersion,
|
|
69
|
+
events: batch.events.map((event) => new BrowserEvent(event)),
|
|
70
|
+
})), {
|
|
71
|
+
onExit: (exit) => {
|
|
72
|
+
if (Exit.isSuccess(exit)) {
|
|
73
|
+
resolve();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const failure = Cause.findErrorOption(exit.cause);
|
|
77
|
+
reject(Option.isSome(failure) ? failure.value : Cause.squash(exit.cause));
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}),
|
|
81
|
+
startTimer: false,
|
|
82
|
+
});
|
|
83
|
+
const flush = Effect.tryPromise({
|
|
84
|
+
try: () => engine.flush(),
|
|
85
|
+
catch: (cause) => cause instanceof BrowserEventDeliveryError
|
|
86
|
+
? cause
|
|
87
|
+
: new BrowserEventDeliveryError({
|
|
88
|
+
code: "OBS_BROWSER_EVENTS_DELIVERY_FAILED",
|
|
89
|
+
message: "The browser events could not be sent. The events stay queued and the next flush retries the same batch.",
|
|
90
|
+
retryable: true,
|
|
91
|
+
cause,
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
yield* Effect.forkScoped(flush.pipe(Effect.ignore, Effect.delay(flushInterval), Effect.forever));
|
|
95
|
+
yield* Effect.addFinalizer(() => Effect.tryPromise({ try: () => engine.dispose(), catch: () => undefined }).pipe(Effect.ignore));
|
|
96
|
+
return {
|
|
97
|
+
emit: (name, fields) => Effect.sync(() => engine.emit(name, fields ?? {})),
|
|
98
|
+
flush: () => flush,
|
|
99
|
+
pending: () => Effect.sync(() => engine.pending()),
|
|
100
|
+
dropped: () => Effect.sync(() => engine.dropped()),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
export class BrowserTelemetry extends Context.Service()("@equipe-tech/observability/BrowserTelemetry") {
|
|
104
|
+
static layer = (options) => Layer.effect(BrowserTelemetry, makeBrowserTelemetry(options));
|
|
105
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { baseBlockedValuePatterns, isSensitiveFieldKey, replaceEmailCandidates, sensitiveFieldReplacement, sensitiveTextReplacement, } from "../policy/PolicyVocabulary.js";
|
|
2
|
+
import { maxEventNameLength, maxFieldKeyLength, maxFieldsPerEvent, maxFieldValueLength, } from "./BrowserEventLimits.js";
|
|
3
|
+
const maximumInputLength = 16_384;
|
|
4
|
+
const assignmentPattern = /(?:authorization|proxy[._-]?authorization|cookie|set[._-]?cookie|password|passwd|secret|token|api[._-]?key|apikey|access[._-]?key|client[._-]?secret|private[._-]?key|email|phone|cpf|cnpj|document)(?:[._-]|[A-Z0-9]|\]|\[|["'`])?\s*[:=]\s*(?:\\?["'`])?[^,;&#}\]]*/gi;
|
|
5
|
+
const sanitizeText = (value, limit) => {
|
|
6
|
+
if (value.length > maximumInputLength)
|
|
7
|
+
return sensitiveTextReplacement;
|
|
8
|
+
let sanitized = value;
|
|
9
|
+
for (const pattern of baseBlockedValuePatterns) {
|
|
10
|
+
pattern.lastIndex = 0;
|
|
11
|
+
sanitized = sanitized.replace(pattern, sensitiveTextReplacement);
|
|
12
|
+
}
|
|
13
|
+
sanitized = replaceEmailCandidates(sanitized).replace(assignmentPattern, (assignment) => {
|
|
14
|
+
const separator = Math.max(assignment.indexOf(":"), assignment.indexOf("="));
|
|
15
|
+
return `${assignment.slice(0, separator + 1)}${sensitiveTextReplacement}`;
|
|
16
|
+
});
|
|
17
|
+
return sanitized.slice(0, limit);
|
|
18
|
+
};
|
|
19
|
+
const sanitizeValue = (value) => {
|
|
20
|
+
try {
|
|
21
|
+
if (String(value) === value)
|
|
22
|
+
return sanitizeText(value, maxFieldValueLength);
|
|
23
|
+
if (Number(value) === value && Number.isFinite(value))
|
|
24
|
+
return value;
|
|
25
|
+
if (value === true || value === false)
|
|
26
|
+
return value;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
export const sanitizeClientFields = (fields) => {
|
|
34
|
+
const entries = [];
|
|
35
|
+
const keys = new Set();
|
|
36
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
37
|
+
if (key.length === 0 || key.length > 2_048)
|
|
38
|
+
continue;
|
|
39
|
+
const boundedKey = key.slice(0, maxFieldKeyLength);
|
|
40
|
+
if (keys.has(boundedKey))
|
|
41
|
+
continue;
|
|
42
|
+
const sanitized = isSensitiveFieldKey(key) ? sensitiveFieldReplacement : sanitizeValue(value);
|
|
43
|
+
if (sanitized === undefined)
|
|
44
|
+
continue;
|
|
45
|
+
entries.push([boundedKey, sanitized]);
|
|
46
|
+
keys.add(boundedKey);
|
|
47
|
+
if (keys.size === maxFieldsPerEvent)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return Object.fromEntries(entries);
|
|
51
|
+
};
|
|
52
|
+
export const sanitizeClientEventName = (name) => sanitizeText(name, maxEventNameLength);
|
package/dist/browser/client.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
1
|
+
export { browserEnvelopeVersion } from "./BrowserEventLimits.js";
|
|
2
|
+
export { BrowserTelemetryClientDeliveryError, BrowserTelemetryClientShutdownError, browserBatchByteLength, createBrowserTelemetryClient, } from "./BrowserClient.js";
|
|
3
|
+
export type { BrowserTelemetryClient, BrowserTelemetryClientBatch, BrowserTelemetryClientConfig, BrowserTelemetryClientError, BrowserTelemetryClientEvent, BrowserTelemetryClientFields, BrowserTelemetryClientMetric, BrowserTelemetryClientResource, BrowserTelemetryClientSpan, BrowserTelemetryDefectInput, BrowserCounter, BrowserTraceContext, BrowserTraceHandle, BrowserTelemetryFieldTransform, BrowserTelemetryClientTransport, } from "./BrowserClient.js";
|
package/dist/browser/client.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { browserEnvelopeVersion } from "./BrowserEventLimits.js";
|
|
2
|
+
export { BrowserTelemetryClientDeliveryError, BrowserTelemetryClientShutdownError, browserBatchByteLength, createBrowserTelemetryClient, } from "./BrowserClient.js";
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,37 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export {
|
|
5
|
-
export type { BrowserTelemetryClient, BrowserTelemetryClientBatch, BrowserTelemetryClientConfig, BrowserTelemetryClientEvent, BrowserTelemetryClientFields, BrowserTelemetryClientTransport, } from "./BrowserClient.js";
|
|
6
|
-
export { BrowserEvent, BrowserEventBatch, maxEventNameLength, maxEventsPerBatch, maxFieldKeyLength, maxFieldsPerEvent, maxFieldValueLength, } from "../BrowserEvents.js";
|
|
7
|
-
export declare const defaultEventsEndpoint = "/_telemetry/events";
|
|
8
|
-
declare const BrowserEventDeliveryError_base: Schema.Class<BrowserEventDeliveryError, Schema.TaggedStruct<"BrowserEventDeliveryError", {
|
|
9
|
-
readonly code: Schema.Literal<"OBS_BROWSER_EVENTS_DELIVERY_FAILED">;
|
|
10
|
-
readonly message: Schema.String;
|
|
11
|
-
readonly retryable: Schema.Boolean;
|
|
12
|
-
readonly cause: Schema.Defect;
|
|
13
|
-
}>, Cause.YieldableError>;
|
|
14
|
-
export declare class BrowserEventDeliveryError extends BrowserEventDeliveryError_base {
|
|
15
|
-
}
|
|
16
|
-
declare const BrowserEventTransport_base: Context.ServiceClass<BrowserEventTransport, "@equipe-tech/observability/BrowserEventTransport", {
|
|
17
|
-
send(batch: BrowserEventBatch): Effect.Effect<void, BrowserEventDeliveryError>;
|
|
18
|
-
}>;
|
|
19
|
-
export declare class BrowserEventTransport extends BrowserEventTransport_base {
|
|
20
|
-
static readonly layerFetch: (options?: {
|
|
21
|
-
readonly endpoint?: string;
|
|
22
|
-
}) => Layer.Layer<BrowserEventTransport>;
|
|
23
|
-
}
|
|
24
|
-
export type BrowserTelemetryOptions = {
|
|
25
|
-
readonly maxBatchSize?: number;
|
|
26
|
-
readonly maxQueueSize?: number;
|
|
27
|
-
readonly flushInterval?: Duration.Input;
|
|
28
|
-
};
|
|
29
|
-
declare const BrowserTelemetry_base: Context.ServiceClass<BrowserTelemetry, "@equipe-tech/observability/BrowserTelemetry", {
|
|
30
|
-
emit(name: string, fields?: WideEventFields): Effect.Effect<void>;
|
|
31
|
-
flush(): Effect.Effect<void, BrowserEventDeliveryError>;
|
|
32
|
-
pending(): Effect.Effect<number>;
|
|
33
|
-
dropped(): Effect.Effect<number>;
|
|
34
|
-
}>;
|
|
35
|
-
export declare class BrowserTelemetry extends BrowserTelemetry_base {
|
|
36
|
-
static readonly layer: (options?: BrowserTelemetryOptions) => Layer.Layer<BrowserTelemetry, never, BrowserEventTransport>;
|
|
37
|
-
}
|
|
1
|
+
export { BrowserEventDeliveryError, BrowserEventTransport, BrowserTelemetry, defaultEventsEndpoint, type BrowserTelemetryOptions, } from "./BrowserTelemetry.js";
|
|
2
|
+
export { BrowserTelemetryClientDeliveryError, BrowserTelemetryClientShutdownError, browserBatchByteLength, createBrowserTelemetryClient, } from "./BrowserClient.js";
|
|
3
|
+
export type { BrowserTelemetryClient, BrowserTelemetryClientBatch, BrowserTelemetryClientConfig, BrowserTelemetryClientError, BrowserTelemetryClientEvent, BrowserTelemetryClientFields, BrowserTelemetryClientMetric, BrowserTelemetryClientResource, BrowserTelemetryClientSpan, BrowserTelemetryDefectInput, BrowserCounter, BrowserTraceContext, BrowserTraceHandle, BrowserTelemetryFieldTransform, BrowserTelemetryClientTransport, } from "./BrowserClient.js";
|
|
4
|
+
export { BrowserEvent, BrowserEventBatch, BrowserEventError, BrowserMetricPoint, BrowserResourceIdentity, BrowserTraceContext as BrowserTraceContextSchema, BrowserTraceSpan, browserEnvelopeMetadata, browserEnvelopeVersion, browserRequestByteBudget, maxEventNameLength, maxEventsPerBatch, maxBrowserEventOccurredAt, maxFieldKeyLength, maxFieldsPerEvent, maxFieldValueLength, } from "../BrowserEvents.js";
|
package/dist/browser/index.js
CHANGED
|
@@ -1,95 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export { BrowserTelemetryClientDeliveryError, BrowserTelemetryClientShutdownError, createBrowserTelemetryClient, } from "./BrowserClient.js";
|
|
5
|
-
export { BrowserEvent, BrowserEventBatch, maxEventNameLength, maxEventsPerBatch, maxFieldKeyLength, maxFieldsPerEvent, maxFieldValueLength, } from "../BrowserEvents.js";
|
|
6
|
-
export const defaultEventsEndpoint = "/_telemetry/events";
|
|
7
|
-
export class BrowserEventDeliveryError extends Schema.TaggedError()("BrowserEventDeliveryError", {
|
|
8
|
-
code: Schema.Literal("OBS_BROWSER_EVENTS_DELIVERY_FAILED"),
|
|
9
|
-
message: Schema.String,
|
|
10
|
-
retryable: Schema.Boolean,
|
|
11
|
-
cause: Schema.Defect(),
|
|
12
|
-
}) {
|
|
13
|
-
}
|
|
14
|
-
export class BrowserEventTransport extends Context.Service()("@equipe-tech/observability/BrowserEventTransport") {
|
|
15
|
-
static layerFetch = (options) => {
|
|
16
|
-
const endpoint = options?.endpoint ?? defaultEventsEndpoint;
|
|
17
|
-
return Layer.succeed(BrowserEventTransport, BrowserEventTransport.of({
|
|
18
|
-
send: (batch) => Effect.gen(function* () {
|
|
19
|
-
const payload = yield* encodeBrowserEventBatch(batch).pipe(Effect.orDie);
|
|
20
|
-
const response = yield* Effect.tryPromise({
|
|
21
|
-
try: (signal) => fetch(endpoint, {
|
|
22
|
-
method: "POST",
|
|
23
|
-
headers: { "content-type": "application/json" },
|
|
24
|
-
body: JSON.stringify(payload),
|
|
25
|
-
keepalive: true,
|
|
26
|
-
signal,
|
|
27
|
-
}),
|
|
28
|
-
catch: (cause) => new BrowserEventDeliveryError({
|
|
29
|
-
code: "OBS_BROWSER_EVENTS_DELIVERY_FAILED",
|
|
30
|
-
message: "The browser events could not be sent. The events stay queued and the next flush retries the same batch.",
|
|
31
|
-
retryable: true,
|
|
32
|
-
cause,
|
|
33
|
-
}),
|
|
34
|
-
});
|
|
35
|
-
if (!response.ok) {
|
|
36
|
-
return yield* new BrowserEventDeliveryError({
|
|
37
|
-
code: "OBS_BROWSER_EVENTS_DELIVERY_FAILED",
|
|
38
|
-
message: `The telemetry endpoint rejected the batch with status ${response.status}. Check the /_telemetry/events route of the project API.`,
|
|
39
|
-
retryable: response.status === 429 || response.status >= 500,
|
|
40
|
-
cause: response.status,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}),
|
|
44
|
-
}));
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const makeBrowserTelemetry = Effect.fn("makeBrowserTelemetry")(function* (options) {
|
|
48
|
-
const transport = yield* BrowserEventTransport;
|
|
49
|
-
const flushInterval = Duration.fromInputUnsafe(options?.flushInterval ?? "5 seconds");
|
|
50
|
-
const engine = new BrowserClientEngine({
|
|
51
|
-
disabled: false,
|
|
52
|
-
maxBatchSize: Math.min(normalizePositiveInteger(options?.maxBatchSize, 32), maxEventsPerBatch),
|
|
53
|
-
maxQueueSize: normalizePositiveInteger(options?.maxQueueSize, 256),
|
|
54
|
-
flushIntervalMs: normalizePositiveInteger(Duration.toMillis(flushInterval), 5_000),
|
|
55
|
-
shutdownTimeoutMs: 2_000,
|
|
56
|
-
transport: (batch) => new Promise((resolve, reject) => {
|
|
57
|
-
Effect.runCallback(transport.send(new BrowserEventBatch({
|
|
58
|
-
version: 1,
|
|
59
|
-
events: batch.events.map((event) => new BrowserEvent(event)),
|
|
60
|
-
})), {
|
|
61
|
-
onExit: (exit) => {
|
|
62
|
-
if (Exit.isSuccess(exit)) {
|
|
63
|
-
resolve();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const failure = Cause.findErrorOption(exit.cause);
|
|
67
|
-
reject(Option.isSome(failure) ? failure.value : Cause.squash(exit.cause));
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
}),
|
|
71
|
-
startTimer: false,
|
|
72
|
-
});
|
|
73
|
-
const flush = Effect.tryPromise({
|
|
74
|
-
try: () => engine.flush(),
|
|
75
|
-
catch: (cause) => cause instanceof BrowserEventDeliveryError
|
|
76
|
-
? cause
|
|
77
|
-
: new BrowserEventDeliveryError({
|
|
78
|
-
code: "OBS_BROWSER_EVENTS_DELIVERY_FAILED",
|
|
79
|
-
message: "The browser events could not be sent. The events stay queued and the next flush retries the same batch.",
|
|
80
|
-
retryable: true,
|
|
81
|
-
cause,
|
|
82
|
-
}),
|
|
83
|
-
});
|
|
84
|
-
yield* Effect.forkScoped(flush.pipe(Effect.ignore, Effect.delay(flushInterval), Effect.forever));
|
|
85
|
-
yield* Effect.addFinalizer(() => Effect.tryPromise({ try: () => engine.dispose(), catch: () => undefined }).pipe(Effect.ignore));
|
|
86
|
-
return {
|
|
87
|
-
emit: (name, fields) => Effect.sync(() => engine.emit(name, fields ?? {})),
|
|
88
|
-
flush: () => flush,
|
|
89
|
-
pending: () => Effect.sync(() => engine.pending()),
|
|
90
|
-
dropped: () => Effect.sync(() => engine.dropped()),
|
|
91
|
-
};
|
|
92
|
-
});
|
|
93
|
-
export class BrowserTelemetry extends Context.Service()("@equipe-tech/observability/BrowserTelemetry") {
|
|
94
|
-
static layer = (options) => Layer.effect(BrowserTelemetry, makeBrowserTelemetry(options));
|
|
95
|
-
}
|
|
1
|
+
export { BrowserEventDeliveryError, BrowserEventTransport, BrowserTelemetry, defaultEventsEndpoint, } from "./BrowserTelemetry.js";
|
|
2
|
+
export { BrowserTelemetryClientDeliveryError, BrowserTelemetryClientShutdownError, browserBatchByteLength, createBrowserTelemetryClient, } from "./BrowserClient.js";
|
|
3
|
+
export { BrowserEvent, BrowserEventBatch, BrowserEventError, BrowserMetricPoint, BrowserResourceIdentity, BrowserTraceContext as BrowserTraceContextSchema, BrowserTraceSpan, browserEnvelopeMetadata, browserEnvelopeVersion, browserRequestByteBudget, maxEventNameLength, maxEventsPerBatch, maxBrowserEventOccurredAt, maxFieldKeyLength, maxFieldsPerEvent, maxFieldValueLength, } from "../BrowserEvents.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { ContractSurface } from "./ContractSurface.js";
|
|
3
|
+
export declare const CompatibilityCode: Schema.Literals<readonly ["OBS_COMPAT_SERVICE_CHANGED", "OBS_COMPAT_EVENT_ADDED", "OBS_COMPAT_EVENT_REMOVED", "OBS_COMPAT_EVENT_RENAMED", "OBS_COMPAT_EVENT_KIND_CHANGED", "OBS_COMPAT_OUTCOME_MEANING_CHANGED", "OBS_COMPAT_ATTRIBUTE_ADDED", "OBS_COMPAT_ATTRIBUTE_REMOVED", "OBS_COMPAT_ATTRIBUTE_REQUIRED", "OBS_COMPAT_ATTRIBUTE_CLASSIFICATION_CHANGED", "OBS_COMPAT_ATTRIBUTE_METRIC_LABEL_CHANGED", "OBS_COMPAT_METRIC_ADDED", "OBS_COMPAT_METRIC_REMOVED", "OBS_COMPAT_METRIC_RENAMED", "OBS_COMPAT_METRIC_KIND_CHANGED", "OBS_COMPAT_METRIC_UNIT_CHANGED", "OBS_COMPAT_METRIC_BOUNDARIES_CHANGED", "OBS_COMPAT_METRIC_ATTRIBUTE_ADDED", "OBS_COMPAT_METRIC_ATTRIBUTE_REMOVED", "OBS_COMPAT_METRIC_ATTRIBUTE_CLASSIFICATION_CHANGED", "OBS_COMPAT_METRIC_CARDINALITY_LOWERED", "OBS_COMPAT_METRIC_ALLOWED_VALUES_NARROWED", "OBS_COMPAT_AUDIT_ACTION_ADDED", "OBS_COMPAT_AUDIT_ACTION_REMOVED", "OBS_COMPAT_AUDIT_ACTION_CHANGED", "OBS_COMPAT_ALIAS_ADDED", "OBS_COMPAT_ALIAS_REMOVED_EARLY", "OBS_COMPAT_ALIAS_WINDOW_RESET", "OBS_COMPAT_ALIAS_WINDOW_EXPIRED", "OBS_COMPAT_BROWSER_ENVELOPE_CHANGED", "OBS_COMPAT_RETENTION_WINDOW_RESET"]>;
|
|
4
|
+
export type CompatibilityCode = typeof CompatibilityCode.Type;
|
|
5
|
+
export type CompatibilitySeverity = "compatible" | "breaking" | "notice";
|
|
6
|
+
export type CompatibilityAliasStatus = "not-required" | "missing" | "active" | "expiring" | "expired";
|
|
7
|
+
export type CompatibilityFinding = {
|
|
8
|
+
readonly code: CompatibilityCode;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly severity: CompatibilitySeverity;
|
|
11
|
+
readonly requiredContractVersion: number;
|
|
12
|
+
readonly declaredContractVersion: number;
|
|
13
|
+
readonly aliasStatus: CompatibilityAliasStatus;
|
|
14
|
+
readonly satisfied: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type CompatibilityReport = {
|
|
17
|
+
readonly report: 1;
|
|
18
|
+
readonly service: string;
|
|
19
|
+
readonly baselineContractVersion: number;
|
|
20
|
+
readonly candidateContractVersion: number;
|
|
21
|
+
readonly requiredContractVersion: number;
|
|
22
|
+
readonly accepted: boolean;
|
|
23
|
+
readonly findings: ReadonlyArray<CompatibilityFinding>;
|
|
24
|
+
};
|
|
25
|
+
export type ContractCompatibilityInput = {
|
|
26
|
+
readonly baseline: ContractSurface;
|
|
27
|
+
readonly candidate: ContractSurface;
|
|
28
|
+
readonly now: string;
|
|
29
|
+
};
|
|
30
|
+
export declare const classifyContractChange: (input: ContractCompatibilityInput) => CompatibilityReport;
|
|
31
|
+
export declare const encodeCompatibilityReport: (report: CompatibilityReport) => string;
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export const CompatibilityCode = Schema.Literals([
|
|
3
|
+
"OBS_COMPAT_SERVICE_CHANGED",
|
|
4
|
+
"OBS_COMPAT_EVENT_ADDED",
|
|
5
|
+
"OBS_COMPAT_EVENT_REMOVED",
|
|
6
|
+
"OBS_COMPAT_EVENT_RENAMED",
|
|
7
|
+
"OBS_COMPAT_EVENT_KIND_CHANGED",
|
|
8
|
+
"OBS_COMPAT_OUTCOME_MEANING_CHANGED",
|
|
9
|
+
"OBS_COMPAT_ATTRIBUTE_ADDED",
|
|
10
|
+
"OBS_COMPAT_ATTRIBUTE_REMOVED",
|
|
11
|
+
"OBS_COMPAT_ATTRIBUTE_REQUIRED",
|
|
12
|
+
"OBS_COMPAT_ATTRIBUTE_CLASSIFICATION_CHANGED",
|
|
13
|
+
"OBS_COMPAT_ATTRIBUTE_METRIC_LABEL_CHANGED",
|
|
14
|
+
"OBS_COMPAT_METRIC_ADDED",
|
|
15
|
+
"OBS_COMPAT_METRIC_REMOVED",
|
|
16
|
+
"OBS_COMPAT_METRIC_RENAMED",
|
|
17
|
+
"OBS_COMPAT_METRIC_KIND_CHANGED",
|
|
18
|
+
"OBS_COMPAT_METRIC_UNIT_CHANGED",
|
|
19
|
+
"OBS_COMPAT_METRIC_BOUNDARIES_CHANGED",
|
|
20
|
+
"OBS_COMPAT_METRIC_ATTRIBUTE_ADDED",
|
|
21
|
+
"OBS_COMPAT_METRIC_ATTRIBUTE_REMOVED",
|
|
22
|
+
"OBS_COMPAT_METRIC_ATTRIBUTE_CLASSIFICATION_CHANGED",
|
|
23
|
+
"OBS_COMPAT_METRIC_CARDINALITY_LOWERED",
|
|
24
|
+
"OBS_COMPAT_METRIC_ALLOWED_VALUES_NARROWED",
|
|
25
|
+
"OBS_COMPAT_AUDIT_ACTION_ADDED",
|
|
26
|
+
"OBS_COMPAT_AUDIT_ACTION_REMOVED",
|
|
27
|
+
"OBS_COMPAT_AUDIT_ACTION_CHANGED",
|
|
28
|
+
"OBS_COMPAT_ALIAS_ADDED",
|
|
29
|
+
"OBS_COMPAT_ALIAS_REMOVED_EARLY",
|
|
30
|
+
"OBS_COMPAT_ALIAS_WINDOW_RESET",
|
|
31
|
+
"OBS_COMPAT_ALIAS_WINDOW_EXPIRED",
|
|
32
|
+
"OBS_COMPAT_BROWSER_ENVELOPE_CHANGED",
|
|
33
|
+
"OBS_COMPAT_RETENTION_WINDOW_RESET",
|
|
34
|
+
]);
|
|
35
|
+
const mapByName = (entries) => new Map(entries.map((entry) => [entry.name, entry]));
|
|
36
|
+
const mapByAction = (entries) => new Map(entries.map((entry) => [entry.action, entry]));
|
|
37
|
+
const same = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
38
|
+
const scalarKey = (value) => JSON.stringify(value);
|
|
39
|
+
const containsAllowedScalars = (candidate, baseline) => {
|
|
40
|
+
if (candidate.length === 0)
|
|
41
|
+
return true;
|
|
42
|
+
if (baseline.length === 0)
|
|
43
|
+
return false;
|
|
44
|
+
const candidateKeys = new Set(candidate.map(scalarKey));
|
|
45
|
+
return baseline.every((value) => candidateKeys.has(scalarKey(value)));
|
|
46
|
+
};
|
|
47
|
+
const containsStrings = (candidate, baseline) => {
|
|
48
|
+
const candidates = new Set(candidate);
|
|
49
|
+
return baseline.every((value) => candidates.has(value));
|
|
50
|
+
};
|
|
51
|
+
const classificationRank = (classification) => classification === "public"
|
|
52
|
+
? 0
|
|
53
|
+
: classification === "internal"
|
|
54
|
+
? 1
|
|
55
|
+
: classification === "sensitive"
|
|
56
|
+
? 2
|
|
57
|
+
: 3;
|
|
58
|
+
const eventAliasCompatible = (baseline, candidate) => candidate !== undefined &&
|
|
59
|
+
baseline.kind === candidate.kind &&
|
|
60
|
+
same(baseline.attributes
|
|
61
|
+
.map((attribute) => `${attribute.name}\u0000${attribute.classification}`)
|
|
62
|
+
.sort(), candidate.attributes
|
|
63
|
+
.map((attribute) => `${attribute.name}\u0000${attribute.classification}`)
|
|
64
|
+
.sort());
|
|
65
|
+
const metricAliasCompatible = (baseline, candidate) => candidate !== undefined &&
|
|
66
|
+
baseline.kind === candidate.kind &&
|
|
67
|
+
baseline.unit === candidate.unit &&
|
|
68
|
+
same(baseline.attributes.map((attribute) => attribute.name).sort(), candidate.attributes.map((attribute) => attribute.name).sort());
|
|
69
|
+
const aliasKey = (alias) => `${alias.kind}\u0000${alias.from}\u0000${alias.to}`;
|
|
70
|
+
const dateDay = (date) => {
|
|
71
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
|
72
|
+
if (match === null)
|
|
73
|
+
return undefined;
|
|
74
|
+
const year = Number(match[1]);
|
|
75
|
+
const month = Number(match[2]);
|
|
76
|
+
const day = Number(match[3]);
|
|
77
|
+
const value = Date.UTC(year, month - 1, day);
|
|
78
|
+
const normalized = new Date(value).toISOString().slice(0, 10);
|
|
79
|
+
return normalized === date ? Math.floor(value / 86_400_000) : undefined;
|
|
80
|
+
};
|
|
81
|
+
const aliasStatus = (alias, now, retentionWindowDays) => {
|
|
82
|
+
if (alias === undefined)
|
|
83
|
+
return "missing";
|
|
84
|
+
const since = dateDay(alias.since);
|
|
85
|
+
const today = dateDay(now);
|
|
86
|
+
if (since === undefined || today === undefined)
|
|
87
|
+
return "missing";
|
|
88
|
+
const remaining = retentionWindowDays - (today - since);
|
|
89
|
+
if (remaining <= 0)
|
|
90
|
+
return "expired";
|
|
91
|
+
return remaining <= Math.min(7, retentionWindowDays) ? "expiring" : "active";
|
|
92
|
+
};
|
|
93
|
+
const finding = (code, path, severity, baselineVersion, candidateVersion, status = "not-required", extraSatisfied = true) => {
|
|
94
|
+
const requiredContractVersion = severity === "breaking" ? baselineVersion + 1 : baselineVersion;
|
|
95
|
+
return {
|
|
96
|
+
code,
|
|
97
|
+
path,
|
|
98
|
+
severity,
|
|
99
|
+
requiredContractVersion,
|
|
100
|
+
declaredContractVersion: candidateVersion,
|
|
101
|
+
aliasStatus: status,
|
|
102
|
+
satisfied: extraSatisfied && (severity !== "breaking" || candidateVersion >= requiredContractVersion),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const eventAlias = (aliases, name) => aliases.find((alias) => alias.kind === "event" && alias.from === name);
|
|
106
|
+
const metricAlias = (aliases, name) => aliases.find((alias) => alias.kind === "metric" && alias.from === name);
|
|
107
|
+
const compareEventAttributes = (baseline, candidate, baselineVersion, candidateVersion) => {
|
|
108
|
+
const findings = [];
|
|
109
|
+
const baselineAttributes = mapByName(baseline.attributes);
|
|
110
|
+
const candidateAttributes = mapByName(candidate.attributes);
|
|
111
|
+
for (const attribute of candidate.attributes) {
|
|
112
|
+
if (!baselineAttributes.has(attribute.name)) {
|
|
113
|
+
findings.push(finding("OBS_COMPAT_ATTRIBUTE_ADDED", `events/${baseline.name}/attributes/${attribute.name}`, attribute.required ? "breaking" : "compatible", baselineVersion, candidateVersion));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const attribute of baseline.attributes) {
|
|
117
|
+
const next = candidateAttributes.get(attribute.name);
|
|
118
|
+
const path = `events/${baseline.name}/attributes/${attribute.name}`;
|
|
119
|
+
if (next === undefined) {
|
|
120
|
+
findings.push(finding("OBS_COMPAT_ATTRIBUTE_REMOVED", path, "breaking", baselineVersion, candidateVersion));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!attribute.required && next.required) {
|
|
124
|
+
findings.push(finding("OBS_COMPAT_ATTRIBUTE_REQUIRED", path, "breaking", baselineVersion, candidateVersion));
|
|
125
|
+
}
|
|
126
|
+
if (classificationRank(next.classification) > classificationRank(attribute.classification)) {
|
|
127
|
+
findings.push(finding("OBS_COMPAT_ATTRIBUTE_CLASSIFICATION_CHANGED", path, "breaking", baselineVersion, candidateVersion));
|
|
128
|
+
}
|
|
129
|
+
if (attribute.metricLabel !== next.metricLabel) {
|
|
130
|
+
findings.push(finding("OBS_COMPAT_ATTRIBUTE_METRIC_LABEL_CHANGED", path, "breaking", baselineVersion, candidateVersion));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return findings;
|
|
134
|
+
};
|
|
135
|
+
const compareMetricAttributes = (baseline, candidate, baselineVersion, candidateVersion) => {
|
|
136
|
+
const findings = [];
|
|
137
|
+
const baselineAttributes = mapByName(baseline.attributes);
|
|
138
|
+
const candidateAttributes = mapByName(candidate.attributes);
|
|
139
|
+
for (const attribute of candidate.attributes) {
|
|
140
|
+
if (!baselineAttributes.has(attribute.name)) {
|
|
141
|
+
findings.push(finding("OBS_COMPAT_METRIC_ATTRIBUTE_ADDED", `metrics/${baseline.name}/attributes/${attribute.name}`, "compatible", baselineVersion, candidateVersion));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const attribute of baseline.attributes) {
|
|
145
|
+
const next = candidateAttributes.get(attribute.name);
|
|
146
|
+
const path = `metrics/${baseline.name}/attributes/${attribute.name}`;
|
|
147
|
+
if (next === undefined) {
|
|
148
|
+
findings.push(finding("OBS_COMPAT_METRIC_ATTRIBUTE_REMOVED", path, "breaking", baselineVersion, candidateVersion));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (classificationRank(next.classification) > classificationRank(attribute.classification)) {
|
|
152
|
+
findings.push(finding("OBS_COMPAT_METRIC_ATTRIBUTE_CLASSIFICATION_CHANGED", path, "breaking", baselineVersion, candidateVersion));
|
|
153
|
+
}
|
|
154
|
+
if (next.maximumCardinality < attribute.maximumCardinality) {
|
|
155
|
+
findings.push(finding("OBS_COMPAT_METRIC_CARDINALITY_LOWERED", path, "breaking", baselineVersion, candidateVersion));
|
|
156
|
+
}
|
|
157
|
+
if (!containsAllowedScalars(next.allowedValues, attribute.allowedValues)) {
|
|
158
|
+
findings.push(finding("OBS_COMPAT_METRIC_ALLOWED_VALUES_NARROWED", path, "breaking", baselineVersion, candidateVersion));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return findings;
|
|
162
|
+
};
|
|
163
|
+
const auditChanged = (baseline, candidate) => baseline.resourceType !== candidate.resourceType ||
|
|
164
|
+
!same([...baseline.allowedOutcomes].sort(), [...candidate.allowedOutcomes].sort()) ||
|
|
165
|
+
!containsStrings(candidate.reasonCodes, baseline.reasonCodes);
|
|
166
|
+
export const classifyContractChange = (input) => {
|
|
167
|
+
const baselineVersion = input.baseline.contractVersion;
|
|
168
|
+
const candidateVersion = input.candidate.contractVersion;
|
|
169
|
+
const findings = [];
|
|
170
|
+
if (input.baseline.service !== input.candidate.service)
|
|
171
|
+
findings.push(finding("OBS_COMPAT_SERVICE_CHANGED", "service", "breaking", baselineVersion, candidateVersion, "not-required", false));
|
|
172
|
+
const baselineEvents = mapByName(input.baseline.events);
|
|
173
|
+
const candidateEvents = mapByName(input.candidate.events);
|
|
174
|
+
for (const event of input.candidate.events) {
|
|
175
|
+
if (!baselineEvents.has(event.name)) {
|
|
176
|
+
findings.push(finding("OBS_COMPAT_EVENT_ADDED", `events/${event.name}`, "compatible", baselineVersion, candidateVersion));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
for (const event of input.baseline.events) {
|
|
180
|
+
const next = candidateEvents.get(event.name);
|
|
181
|
+
if (next === undefined) {
|
|
182
|
+
const alias = eventAlias(input.candidate.aliases, event.name);
|
|
183
|
+
const status = aliasStatus(alias, input.now, input.baseline.retentionWindowDays);
|
|
184
|
+
const target = alias === undefined ? undefined : candidateEvents.get(alias.to);
|
|
185
|
+
findings.push(finding(alias === undefined ? "OBS_COMPAT_EVENT_REMOVED" : "OBS_COMPAT_EVENT_RENAMED", `events/${event.name}`, "breaking", baselineVersion, candidateVersion, status, alias !== undefined && status !== "missing" && eventAliasCompatible(event, target)));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (event.kind !== next.kind) {
|
|
189
|
+
findings.push(finding("OBS_COMPAT_EVENT_KIND_CHANGED", `events/${event.name}/kind`, "breaking", baselineVersion, candidateVersion));
|
|
190
|
+
}
|
|
191
|
+
if (!same(event.outcomeMeaning, next.outcomeMeaning)) {
|
|
192
|
+
findings.push(finding("OBS_COMPAT_OUTCOME_MEANING_CHANGED", `events/${event.name}/outcomeMeaning`, "breaking", baselineVersion, candidateVersion, "not-required", false));
|
|
193
|
+
}
|
|
194
|
+
findings.push(...compareEventAttributes(event, next, baselineVersion, candidateVersion));
|
|
195
|
+
}
|
|
196
|
+
const baselineMetrics = mapByName(input.baseline.metrics);
|
|
197
|
+
const candidateMetrics = mapByName(input.candidate.metrics);
|
|
198
|
+
for (const metric of input.candidate.metrics) {
|
|
199
|
+
if (!baselineMetrics.has(metric.name)) {
|
|
200
|
+
findings.push(finding("OBS_COMPAT_METRIC_ADDED", `metrics/${metric.name}`, "compatible", baselineVersion, candidateVersion));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const metric of input.baseline.metrics) {
|
|
204
|
+
const next = candidateMetrics.get(metric.name);
|
|
205
|
+
if (next === undefined) {
|
|
206
|
+
const alias = metricAlias(input.candidate.aliases, metric.name);
|
|
207
|
+
const status = aliasStatus(alias, input.now, input.baseline.retentionWindowDays);
|
|
208
|
+
const target = alias === undefined ? undefined : candidateMetrics.get(alias.to);
|
|
209
|
+
findings.push(finding(alias === undefined ? "OBS_COMPAT_METRIC_REMOVED" : "OBS_COMPAT_METRIC_RENAMED", `metrics/${metric.name}`, "breaking", baselineVersion, candidateVersion, status, alias !== undefined && status !== "missing" && metricAliasCompatible(metric, target)));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (metric.kind !== next.kind)
|
|
213
|
+
findings.push(finding("OBS_COMPAT_METRIC_KIND_CHANGED", `metrics/${metric.name}/kind`, "breaking", baselineVersion, candidateVersion, "not-required", false));
|
|
214
|
+
if (metric.unit !== next.unit)
|
|
215
|
+
findings.push(finding("OBS_COMPAT_METRIC_UNIT_CHANGED", `metrics/${metric.name}/unit`, "breaking", baselineVersion, candidateVersion, "not-required", false));
|
|
216
|
+
if (!same(metric.boundaries, next.boundaries))
|
|
217
|
+
findings.push(finding("OBS_COMPAT_METRIC_BOUNDARIES_CHANGED", `metrics/${metric.name}/boundaries`, "breaking", baselineVersion, candidateVersion, "not-required", false));
|
|
218
|
+
findings.push(...compareMetricAttributes(metric, next, baselineVersion, candidateVersion));
|
|
219
|
+
}
|
|
220
|
+
const baselineActions = mapByAction(input.baseline.auditActions);
|
|
221
|
+
const candidateActions = mapByAction(input.candidate.auditActions);
|
|
222
|
+
for (const action of input.candidate.auditActions) {
|
|
223
|
+
if (!baselineActions.has(action.action))
|
|
224
|
+
findings.push(finding("OBS_COMPAT_AUDIT_ACTION_ADDED", `auditActions/${action.action}`, "compatible", baselineVersion, candidateVersion));
|
|
225
|
+
}
|
|
226
|
+
for (const action of input.baseline.auditActions) {
|
|
227
|
+
const next = candidateActions.get(action.action);
|
|
228
|
+
if (next === undefined)
|
|
229
|
+
findings.push(finding("OBS_COMPAT_AUDIT_ACTION_REMOVED", `auditActions/${action.action}`, "breaking", baselineVersion, candidateVersion));
|
|
230
|
+
else if (auditChanged(action, next))
|
|
231
|
+
findings.push(finding("OBS_COMPAT_AUDIT_ACTION_CHANGED", `auditActions/${action.action}`, "breaking", baselineVersion, candidateVersion));
|
|
232
|
+
}
|
|
233
|
+
const baselineAliases = new Map(input.baseline.aliases.map((alias) => [aliasKey(alias), alias]));
|
|
234
|
+
const candidateAliases = new Map(input.candidate.aliases.map((alias) => [aliasKey(alias), alias]));
|
|
235
|
+
for (const alias of input.candidate.aliases) {
|
|
236
|
+
const previous = baselineAliases.get(aliasKey(alias));
|
|
237
|
+
const status = aliasStatus(alias, input.now, input.baseline.retentionWindowDays);
|
|
238
|
+
if (previous === undefined)
|
|
239
|
+
findings.push(finding("OBS_COMPAT_ALIAS_ADDED", `aliases/${alias.kind}/${alias.from}/${alias.to}`, "compatible", baselineVersion, candidateVersion, status));
|
|
240
|
+
else if (previous.since !== alias.since)
|
|
241
|
+
findings.push(finding("OBS_COMPAT_ALIAS_WINDOW_RESET", `aliases/${alias.kind}/${alias.from}/${alias.to}/since`, "breaking", baselineVersion, candidateVersion, status, false));
|
|
242
|
+
if (status === "expired")
|
|
243
|
+
findings.push(finding("OBS_COMPAT_ALIAS_WINDOW_EXPIRED", `aliases/${alias.kind}/${alias.from}/${alias.to}`, "notice", baselineVersion, candidateVersion, status));
|
|
244
|
+
}
|
|
245
|
+
for (const alias of input.baseline.aliases) {
|
|
246
|
+
if (candidateAliases.has(aliasKey(alias)))
|
|
247
|
+
continue;
|
|
248
|
+
const status = aliasStatus(alias, input.now, input.baseline.retentionWindowDays);
|
|
249
|
+
if (status !== "expired")
|
|
250
|
+
findings.push(finding("OBS_COMPAT_ALIAS_REMOVED_EARLY", `aliases/${alias.kind}/${alias.from}/${alias.to}`, "breaking", baselineVersion, candidateVersion, status, false));
|
|
251
|
+
}
|
|
252
|
+
const baselineBrowser = input.baseline.browserEnvelope;
|
|
253
|
+
const candidateBrowser = input.candidate.browserEnvelope;
|
|
254
|
+
const browserFieldsChanged = !same(baselineBrowser.batchFields, candidateBrowser.batchFields) ||
|
|
255
|
+
!same(baselineBrowser.eventFields, candidateBrowser.eventFields);
|
|
256
|
+
if (browserFieldsChanged && candidateBrowser.version <= baselineBrowser.version) {
|
|
257
|
+
findings.push(finding("OBS_COMPAT_BROWSER_ENVELOPE_CHANGED", "browserEnvelope", "breaking", baselineVersion, candidateVersion, "not-required", false));
|
|
258
|
+
}
|
|
259
|
+
if (input.candidate.retentionWindowDays < input.baseline.retentionWindowDays) {
|
|
260
|
+
findings.push(finding("OBS_COMPAT_RETENTION_WINDOW_RESET", "retentionWindowDays", "breaking", baselineVersion, candidateVersion, "not-required", false));
|
|
261
|
+
}
|
|
262
|
+
findings.sort((left, right) => `${left.path}\u0000${left.code}`.localeCompare(`${right.path}\u0000${right.code}`));
|
|
263
|
+
const requiredContractVersion = findings.reduce((required, entry) => Math.max(required, entry.requiredContractVersion), baselineVersion);
|
|
264
|
+
return {
|
|
265
|
+
report: 1,
|
|
266
|
+
service: input.candidate.service,
|
|
267
|
+
baselineContractVersion: baselineVersion,
|
|
268
|
+
candidateContractVersion: candidateVersion,
|
|
269
|
+
requiredContractVersion,
|
|
270
|
+
accepted: findings.every((entry) => entry.satisfied),
|
|
271
|
+
findings,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
export const encodeCompatibilityReport = (report) => `${JSON.stringify(report, null, 2)}\n`;
|