@provide-io/telemetry 0.2.2 → 0.2.4

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 (52) hide show
  1. package/README.md +3 -3
  2. package/dist/backpressure.d.ts +0 -4
  3. package/dist/backpressure.d.ts.map +1 -1
  4. package/dist/backpressure.js +8 -1
  5. package/dist/classification.d.ts.map +1 -1
  6. package/dist/classification.js +1 -0
  7. package/dist/config.d.ts +1 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +2 -0
  10. package/dist/consent.d.ts.map +1 -1
  11. package/dist/consent.js +3 -0
  12. package/dist/context.d.ts.map +1 -1
  13. package/dist/context.js +1 -0
  14. package/dist/health.d.ts +4 -0
  15. package/dist/health.d.ts.map +1 -1
  16. package/dist/health.js +16 -0
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/pii.d.ts +19 -0
  21. package/dist/pii.d.ts.map +1 -1
  22. package/dist/pii.js +50 -8
  23. package/dist/react.d.ts +9 -0
  24. package/dist/react.d.ts.map +1 -1
  25. package/dist/react.js +9 -0
  26. package/dist/receipts.d.ts +2 -0
  27. package/dist/receipts.d.ts.map +1 -1
  28. package/dist/receipts.js +19 -5
  29. package/dist/resilience.d.ts.map +1 -1
  30. package/dist/resilience.js +2 -1
  31. package/dist/runtime.d.ts +2 -2
  32. package/dist/runtime.d.ts.map +1 -1
  33. package/dist/runtime.js +55 -9
  34. package/dist/sampling.d.ts.map +1 -1
  35. package/dist/sampling.js +12 -5
  36. package/dist/tracing.d.ts.map +1 -1
  37. package/dist/tracing.js +1 -0
  38. package/package.json +8 -7
  39. package/src/backpressure.ts +6 -1
  40. package/src/classification.ts +1 -0
  41. package/src/config.ts +5 -0
  42. package/src/consent.ts +3 -0
  43. package/src/context.ts +1 -0
  44. package/src/health.ts +14 -0
  45. package/src/index.ts +4 -1
  46. package/src/pii.ts +58 -6
  47. package/src/react.ts +9 -0
  48. package/src/receipts.ts +20 -5
  49. package/src/resilience.ts +2 -1
  50. package/src/runtime.ts +57 -10
  51. package/src/sampling.ts +14 -5
  52. package/src/tracing.ts +1 -0
package/src/runtime.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  configFromEnv,
13
13
  setupTelemetry,
14
14
  } from './config';
15
+ import { ConfigurationError } from './exceptions';
15
16
 
16
17
  /** Minimal interface for providers that can be flushed and shut down cleanly. */
17
18
  export interface ShutdownableProvider {
@@ -47,6 +48,7 @@ export function _areProvidersRegistered(): boolean {
47
48
 
48
49
  function deepFreeze<T extends object>(obj: T): Readonly<T> {
49
50
  for (const val of Object.values(obj)) {
51
+ // Stryker disable next-line ConditionalExpression,EqualityOperator,LogicalOperator: frozen-object guard — all sub-conditions required but only observable with deeply nested mutable objects
50
52
  if (typeof val === 'object' && val !== null && !Object.isFrozen(val)) {
51
53
  deepFreeze(val as object);
52
54
  }
@@ -62,6 +64,7 @@ export function getRuntimeConfig(): Readonly<TelemetryConfig> {
62
64
 
63
65
  /** Merge hot-reloadable overrides into the active config and re-apply policies. */
64
66
  export function updateRuntimeConfig(overrides: RuntimeOverrides): void {
67
+ validateRuntimeOverrides(overrides);
65
68
  const base = _activeConfig ?? configFromEnv();
66
69
  const merged: TelemetryConfig = { ...base };
67
70
  for (const [key, value] of Object.entries(overrides)) {
@@ -73,6 +76,53 @@ export function updateRuntimeConfig(overrides: RuntimeOverrides): void {
73
76
  setupTelemetry(_activeConfig);
74
77
  }
75
78
 
79
+ function validateRate(name: string, value: number | undefined): void {
80
+ if (value === undefined) return;
81
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
82
+ // Stryker disable next-line StringLiteral: error message content
83
+ throw new ConfigurationError(`${name} must be in [0, 1], got ${String(value)}`);
84
+ }
85
+ }
86
+
87
+ function validateNonNegativeInteger(name: string, value: number | undefined): void {
88
+ if (value === undefined) return;
89
+ if (!Number.isInteger(value) || value < 0) {
90
+ // Stryker disable next-line StringLiteral: error message content
91
+ throw new ConfigurationError(`${name} must be a non-negative integer, got ${String(value)}`);
92
+ }
93
+ }
94
+
95
+ function validateNonNegativeNumber(name: string, value: number | undefined): void {
96
+ if (value === undefined) return;
97
+ if (!Number.isFinite(value) || value < 0) {
98
+ // Stryker disable next-line StringLiteral: error message content
99
+ throw new ConfigurationError(`${name} must be >= 0, got ${String(value)}`);
100
+ }
101
+ }
102
+
103
+ /* Stryker disable StringLiteral: field names in validation calls are only used in error messages — mutating them does not change validation behavior */
104
+ function validateRuntimeOverrides(overrides: RuntimeOverrides): void {
105
+ validateRate('samplingLogsRate', overrides.samplingLogsRate);
106
+ validateRate('samplingTracesRate', overrides.samplingTracesRate);
107
+ validateRate('samplingMetricsRate', overrides.samplingMetricsRate);
108
+ validateNonNegativeInteger('backpressureLogsMaxsize', overrides.backpressureLogsMaxsize);
109
+ validateNonNegativeInteger('backpressureTracesMaxsize', overrides.backpressureTracesMaxsize);
110
+ validateNonNegativeInteger('backpressureMetricsMaxsize', overrides.backpressureMetricsMaxsize);
111
+ validateNonNegativeInteger('exporterLogsRetries', overrides.exporterLogsRetries);
112
+ validateNonNegativeInteger('exporterTracesRetries', overrides.exporterTracesRetries);
113
+ validateNonNegativeInteger('exporterMetricsRetries', overrides.exporterMetricsRetries);
114
+ validateNonNegativeNumber('exporterLogsBackoffMs', overrides.exporterLogsBackoffMs);
115
+ validateNonNegativeNumber('exporterTracesBackoffMs', overrides.exporterTracesBackoffMs);
116
+ validateNonNegativeNumber('exporterMetricsBackoffMs', overrides.exporterMetricsBackoffMs);
117
+ validateNonNegativeNumber('exporterLogsTimeoutMs', overrides.exporterLogsTimeoutMs);
118
+ validateNonNegativeNumber('exporterTracesTimeoutMs', overrides.exporterTracesTimeoutMs);
119
+ validateNonNegativeNumber('exporterMetricsTimeoutMs', overrides.exporterMetricsTimeoutMs);
120
+ validateNonNegativeInteger('securityMaxAttrValueLength', overrides.securityMaxAttrValueLength);
121
+ validateNonNegativeInteger('securityMaxAttrCount', overrides.securityMaxAttrCount);
122
+ validateNonNegativeInteger('piiMaxDepth', overrides.piiMaxDepth);
123
+ }
124
+ /* Stryker restore StringLiteral */
125
+
76
126
  const _COLD_FIELDS: (keyof TelemetryConfig)[] = [
77
127
  'serviceName',
78
128
  'environment',
@@ -91,11 +141,13 @@ export function reloadRuntimeFromEnv(): void {
91
141
  (k) => JSON.stringify(current[k]) !== JSON.stringify(fresh[k]),
92
142
  );
93
143
  if (drifted.length > 0) {
144
+ /* Stryker disable StringLiteral: warning message content */
94
145
  console.warn(
95
146
  '[provide-telemetry] runtime.cold_field_drift:',
96
147
  drifted.join(', '),
97
148
  '— restart required to apply',
98
149
  );
150
+ /* Stryker restore StringLiteral */
99
151
  }
100
152
  }
101
153
  // Apply only hot fields via overrides
@@ -123,6 +175,7 @@ export function reloadRuntimeFromEnv(): void {
123
175
  sloEnableRedMetrics: fresh.sloEnableRedMetrics,
124
176
  sloEnableUseMetrics: fresh.sloEnableUseMetrics,
125
177
  piiMaxDepth: fresh.piiMaxDepth,
178
+ strictSchema: fresh.strictSchema,
126
179
  };
127
180
  updateRuntimeConfig(overrides);
128
181
  }
@@ -135,8 +188,8 @@ const PROVIDER_CHANGING_FIELDS: (keyof TelemetryConfig)[] = [
135
188
 
136
189
  /**
137
190
  * Apply config changes.
138
- * If provider-changing fields differ and providers are already registered, performs a
139
- * best-effort shutdown (fire-and-forget) then re-initialises matching Go/Python behaviour.
191
+ * If provider-changing fields differ and providers are already registered, fail fast:
192
+ * provider replacement requires explicit process restart to avoid async export loss.
140
193
  * Otherwise delegates to setupTelemetry.
141
194
  */
142
195
  export function reconfigureTelemetry(config: Partial<TelemetryConfig>): void {
@@ -148,15 +201,9 @@ export function reconfigureTelemetry(config: Partial<TelemetryConfig>): void {
148
201
  (k) => JSON.stringify(current[k]) !== JSON.stringify(proposed[k]),
149
202
  );
150
203
  if (changed) {
151
- // Best-effort async shutdown — fire-and-forget, errors ignored (mirrors Go's `_ = ShutdownTelemetry(ctx)`)
152
- const providers = _getRegisteredProviders();
153
- // Stryker disable LogicalOperator: ?? vs && is equivalent here — forceFlush/shutdown return Promise (truthy) so && still resolves; when undefined, Promise.allSettled wraps both in Promise.resolve
154
- void Promise.allSettled(providers.map((p) => p.forceFlush?.() ?? Promise.resolve())).then(
155
- () => Promise.allSettled(providers.map((p) => p.shutdown?.() ?? Promise.resolve())),
204
+ throw new ConfigurationError(
205
+ 'provider-changing reconfiguration is unsupported after OpenTelemetry providers are installed; restart the process and call setupTelemetry() with the new config',
156
206
  );
157
- // Stryker restore LogicalOperator
158
- _providersRegistered = false;
159
- _registeredProviders = [];
160
207
  }
161
208
  }
162
209
 
package/src/sampling.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { ConfigurationError } from './exceptions';
9
+ import { _droppedField, _emittedField, _incrementHealth } from './health';
9
10
 
10
11
  export interface SamplingPolicy {
11
12
  defaultRate: number;
@@ -55,12 +56,20 @@ export function shouldSample(signal: string, key?: string): boolean {
55
56
  const lookupKey = key ?? signal;
56
57
  const rate = overrides && lookupKey in overrides ? overrides[lookupKey] : _policy.defaultRate;
57
58
  const clamped = _clamp(rate);
58
- // Stryker disable next-line ConditionalExpression,EqualityOperator: equivalent mutant Math.random() in [0,1) so boundary is not observable
59
- if (clamped <= 0) return false;
60
- // Stryker disable next-line ConditionalExpression,EqualityOperator: equivalent mutant — Math.random() in [0,1) so boundary is not observable
61
- if (clamped >= 1) return true;
59
+ /* Stryker disable ConditionalExpression,EqualityOperator,BlockStatement: boundary not observable (Math.random [0,1)); health counter updates tested but perTest coverage misattributes */
60
+ if (clamped <= 0) {
61
+ _incrementHealth(_droppedField(signal));
62
+ return false;
63
+ }
64
+ if (clamped >= 1) {
65
+ _incrementHealth(_emittedField(signal));
66
+ return true;
67
+ }
68
+ /* Stryker restore ConditionalExpression,EqualityOperator,BlockStatement */
62
69
  // Stryker disable next-line EqualityOperator: Math.random() is in [0,1) so < 1.0 and <= 1.0 are equivalent (random never equals 1.0)
63
- return Math.random() < clamped;
70
+ const sampled = Math.random() < clamped;
71
+ _incrementHealth(sampled ? _emittedField(signal) : _droppedField(signal));
72
+ return sampled;
64
73
  }
65
74
 
66
75
  export function _resetSamplingForTests(): void {
package/src/tracing.ts CHANGED
@@ -88,6 +88,7 @@ const NOOP_TRACE_ID = '00000000000000000000000000000000';
88
88
  * Used to decide whether to inject synthetic random IDs.
89
89
  */
90
90
  function _isNoopSpan(span: Span): boolean {
91
+ // Stryker disable next-line ConditionalExpression: without a registered OTel SDK all spans are noop — mutating to true is equivalent
91
92
  return span.spanContext().traceId === NOOP_TRACE_ID;
92
93
  }
93
94