@grafana/faro-react-native-tracing 1.3.0 → 2.0.0

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 (43) hide show
  1. package/README.md +33 -0
  2. package/dist/cjs/exporters/faroTraceExporter.js +2 -1
  3. package/dist/cjs/exporters/faroTraceExporter.js.map +1 -1
  4. package/dist/cjs/exporters/faroTraceExporter.utils.js +33 -0
  5. package/dist/cjs/exporters/faroTraceExporter.utils.js.map +1 -1
  6. package/dist/cjs/instrumentation.js +259 -44
  7. package/dist/cjs/instrumentation.js.map +1 -1
  8. package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js +28 -20
  9. package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js.map +1 -1
  10. package/dist/cjs/instrumentations/instrumentationUtils.js +15 -0
  11. package/dist/cjs/instrumentations/instrumentationUtils.js.map +1 -1
  12. package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js +27 -13
  13. package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js.map +1 -1
  14. package/dist/cjs/types.js.map +1 -1
  15. package/dist/esm/exporters/faroTraceExporter.js +2 -1
  16. package/dist/esm/exporters/faroTraceExporter.js.map +1 -1
  17. package/dist/esm/exporters/faroTraceExporter.utils.js +32 -0
  18. package/dist/esm/exporters/faroTraceExporter.utils.js.map +1 -1
  19. package/dist/esm/instrumentation.js +237 -43
  20. package/dist/esm/instrumentation.js.map +1 -1
  21. package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js +27 -21
  22. package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js.map +1 -1
  23. package/dist/esm/instrumentations/instrumentationUtils.js +14 -0
  24. package/dist/esm/instrumentations/instrumentationUtils.js.map +1 -1
  25. package/dist/esm/processors/httpRequestMonitorSpanProcessor.js +27 -13
  26. package/dist/esm/processors/httpRequestMonitorSpanProcessor.js.map +1 -1
  27. package/dist/esm/types.js.map +1 -1
  28. package/dist/types/instrumentation.d.ts +12 -4
  29. package/dist/types/instrumentations/getDefaultOTELInstrumentations.d.ts +2 -0
  30. package/dist/types/instrumentations/instrumentationUtils.d.ts +2 -0
  31. package/dist/types/types.d.ts +4 -0
  32. package/package.json +13 -13
  33. package/src/exporters/faroTraceExporter.events.test.ts +90 -0
  34. package/src/exporters/faroTraceExporter.ts +2 -1
  35. package/src/exporters/faroTraceExporter.utils.ts +32 -0
  36. package/src/httpSemantics.test.ts +300 -0
  37. package/src/instrumentation.test.ts +740 -0
  38. package/src/instrumentation.ts +299 -67
  39. package/src/instrumentations/getDefaultOTELInstrumentations.test.ts +28 -1
  40. package/src/instrumentations/getDefaultOTELInstrumentations.ts +41 -32
  41. package/src/instrumentations/instrumentationUtils.ts +13 -0
  42. package/src/processors/httpRequestMonitorSpanProcessor.ts +27 -15
  43. package/src/types.ts +4 -0
@@ -1,4 +1,7 @@
1
+ import { hrTimeToMilliseconds } from '@opentelemetry/core';
1
2
  import { notifyHttpRequestEnd, notifyHttpRequestStart, } from '@grafana/faro-react-native';
3
+ import { captureFetchUserAction } from '../instrumentations/instrumentationUtils';
4
+ // Optional XHR instrumentation still emits these attributes.
2
5
  const ATTR_HTTP_METHOD = 'http.method';
3
6
  const ATTR_HTTP_STATUS_CODE = 'http.status_code';
4
7
  const ATTR_HTTP_URL = 'http.url';
@@ -27,7 +30,16 @@ function getAttr(span, key) {
27
30
  * Check if a span is an HTTP span (from FetchInstrumentation).
28
31
  */
29
32
  function isHttpSpan(span) {
30
- return getAttr(span, ATTR_HTTP_URL) != null || getAttr(span, ATTR_HTTP_METHOD) != null;
33
+ // url.full is shared by non-HTTP spans; stable HTTP spans identify their method at creation.
34
+ return getHttpMethod(span) != null || getAttr(span, ATTR_HTTP_URL) != null;
35
+ }
36
+ function getHttpUrl(span) {
37
+ var _a;
38
+ return (_a = getAttr(span, 'url.full')) !== null && _a !== void 0 ? _a : getAttr(span, ATTR_HTTP_URL);
39
+ }
40
+ function getHttpMethod(span) {
41
+ var _a;
42
+ return (_a = getAttr(span, 'http.request.method')) !== null && _a !== void 0 ? _a : getAttr(span, ATTR_HTTP_METHOD);
31
43
  }
32
44
  /**
33
45
  * SpanProcessor that notifies httpRequestMonitor when HTTP spans start and end.
@@ -44,10 +56,15 @@ export class HttpRequestMonitorSpanProcessor {
44
56
  onStart(span, parentContext) {
45
57
  var _a, _b;
46
58
  if (isHttpSpan(span)) {
47
- const url = (_a = getAttr(span, ATTR_HTTP_URL)) !== null && _a !== void 0 ? _a : '';
48
- const method = (_b = getAttr(span, ATTR_HTTP_METHOD)) !== null && _b !== void 0 ? _b : 'GET';
59
+ // Capture before notifyStart changes Started to Halted. New requests while
60
+ // already Halted are not tracked by the controller and must not inherit it.
61
+ if (span.instrumentationScope.name === '@opentelemetry/instrumentation-fetch') {
62
+ captureFetchUserAction(span);
63
+ }
64
+ const url = (_a = getHttpUrl(span)) !== null && _a !== void 0 ? _a : '';
65
+ const method = (_b = getHttpMethod(span)) !== null && _b !== void 0 ? _b : 'GET';
49
66
  const requestId = span.spanContext().spanId;
50
- const startTimeMs = Date.now();
67
+ const startTimeMs = hrTimeToMilliseconds(span.startTime);
51
68
  const payload = {
52
69
  requestId,
53
70
  url,
@@ -59,18 +76,15 @@ export class HttpRequestMonitorSpanProcessor {
59
76
  this.processor.onStart(span, parentContext);
60
77
  }
61
78
  onEnd(span) {
62
- var _a, _b;
79
+ var _a, _b, _c;
63
80
  if (isHttpSpan(span)) {
64
- const url = (_a = getAttr(span, ATTR_HTTP_URL)) !== null && _a !== void 0 ? _a : '';
65
- const method = (_b = getAttr(span, ATTR_HTTP_METHOD)) !== null && _b !== void 0 ? _b : 'GET';
81
+ const url = (_a = getHttpUrl(span)) !== null && _a !== void 0 ? _a : '';
82
+ const method = (_b = getHttpMethod(span)) !== null && _b !== void 0 ? _b : 'GET';
66
83
  const requestId = span.spanContext().spanId;
67
- const statusAttr = getAttr(span, ATTR_HTTP_STATUS_CODE);
84
+ const statusAttr = (_c = getAttr(span, 'http.response.status_code')) !== null && _c !== void 0 ? _c : getAttr(span, ATTR_HTTP_STATUS_CODE);
68
85
  const status = statusAttr != null ? parseInt(statusAttr, 10) : undefined;
69
- const spanWithTime = span;
70
- const startNs = spanWithTime.startTimeUnixNano;
71
- const endNs = spanWithTime.endTimeUnixNano;
72
- const startTimeMs = startNs != null && !Number.isNaN(Number(startNs)) ? Number(startNs) / 1000000 : Date.now();
73
- const endTimeMs = endNs != null && !Number.isNaN(Number(endNs)) ? Number(endNs) / 1000000 : Date.now();
86
+ const startTimeMs = hrTimeToMilliseconds(span.startTime);
87
+ const endTimeMs = hrTimeToMilliseconds(span.endTime);
74
88
  const payload = {
75
89
  requestId,
76
90
  url,
@@ -1 +1 @@
1
- {"version":3,"file":"httpRequestMonitorSpanProcessor.js","sourceRoot":"","sources":["../../../src/processors/httpRequestMonitorSpanProcessor.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,gBAAgB,GAAG,aAAa,CAAC;AACvC,MAAM,qBAAqB,GAAG,kBAAkB,CAAC;AACjD,MAAM,aAAa,GAAG,UAAU,CAAC;AAEjC;;;GAGG;AACH,SAAS,OAAO,CAAC,IAAyB,EAAE,GAAW;;IACrD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAiD,CAAC;IACrE,IAAI,KAAK,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACpC,MAAM,GAAG,GAAG,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,CAAC,CAAE,KAAyC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrH,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IAClC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,aAAa,IAAI,GAAG,EAAE,CAAC;QACpE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAC,GAAgC,CAAC,WAAW,mCAAI,EAAE,CAAC,CAAC;QACtE,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACtC,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACxB,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1C,CAAC;AAED;;GAEG;AACH,SAAS,UAAU,CAAC,IAAyB;IAC3C,OAAO,OAAO,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,IAAI,IAAI,OAAO,CAAC,IAAI,EAAE,gBAAgB,CAAC,IAAI,IAAI,CAAC;AACzF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,+BAA+B;IAC1C,YAA6B,SAAwB;QAAxB,cAAS,GAAT,SAAS,CAAe;IAAG,CAAC;IAEzD,OAAO,CAAC,IAAU,EAAE,aAAsB;;QACxC,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,MAAA,OAAO,CAAC,IAAI,EAAE,aAAa,CAAC,mCAAI,EAAE,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAA,OAAO,CAAC,IAAI,EAAE,gBAAgB,CAAC,mCAAI,KAAK,CAAC;YACxD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC;YAC5C,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE/B,MAAM,OAAO,GAA8B;gBACzC,SAAS;gBACT,GAAG;gBACH,MAAM;gBACN,SAAS,EAAE,WAAW;aACvB,CAAC;YACF,sBAAsB,CAAC,OAAO,CAAC,CAAC;QAClC,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IAC9C,CAAC;IAED,KAAK,CAAC,IAAkB;;QACtB,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,MAAA,OAAO,CAAC,IAAI,EAAE,aAAa,CAAC,mCAAI,EAAE,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAA,OAAO,CAAC,IAAI,EAAE,gBAAgB,CAAC,mCAAI,KAAK,CAAC;YACxD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC;YAC5C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;YACxD,MAAM,MAAM,GAAG,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACzE,MAAM,YAAY,GAAG,IAGpB,CAAC;YACF,MAAM,OAAO,GAAG,YAAY,CAAC,iBAAiB,CAAC;YAC/C,MAAM,KAAK,GAAG,YAAY,CAAC,eAAe,CAAC;YAC3C,MAAM,WAAW,GAAG,OAAO,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,OAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YACjH,MAAM,SAAS,GAAG,KAAK,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YAEzG,MAAM,OAAO,GAA8B;gBACzC,SAAS;gBACT,GAAG;gBACH,MAAM;gBACN,SAAS,EAAE,WAAW;gBACtB,OAAO,EAAE,SAAS;gBAClB,MAAM,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;aACnD,CAAC;YACF,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC;IACrC,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;IACnC,CAAC;CACF","sourcesContent":["import type { Context } from '@opentelemetry/api';\nimport type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport {\n type HttpRequestMessagePayload,\n notifyHttpRequestEnd,\n notifyHttpRequestStart,\n} from '@grafana/faro-react-native';\n\nconst ATTR_HTTP_METHOD = 'http.method';\nconst ATTR_HTTP_STATUS_CODE = 'http.status_code';\nconst ATTR_HTTP_URL = 'http.url';\n\n/**\n * Get string attribute from span.\n * Handles both SDK attribute format and OTLP-style attribute values.\n */\nfunction getAttr(span: Span | ReadableSpan, key: string): string | undefined {\n const attrs = span.attributes as Record<string, unknown> | undefined;\n if (attrs == null) return undefined;\n const raw = typeof attrs['get'] === 'function' ? (attrs as { get: (k: string) => unknown })['get'](key) : attrs[key];\n if (raw == null) return undefined;\n if (typeof raw === 'string') return raw.length > 0 ? raw : undefined;\n if (typeof raw === 'object' && raw !== null && 'stringValue' in raw) {\n const s = String((raw as { stringValue?: string }).stringValue ?? '');\n return s.length > 0 ? s : undefined;\n }\n const str = String(raw);\n return str.length > 0 ? str : undefined;\n}\n\n/**\n * Check if a span is an HTTP span (from FetchInstrumentation).\n */\nfunction isHttpSpan(span: Span | ReadableSpan): boolean {\n return getAttr(span, ATTR_HTTP_URL) != null || getAttr(span, ATTR_HTTP_METHOD) != null;\n}\n\n/**\n * SpanProcessor that notifies httpRequestMonitor when HTTP spans start and end.\n *\n * This enables user action correlation (UserActionController halt logic) when\n * TracingInstrumentation is used instead of HttpInstrumentation.\n *\n * IMPORTANT: Must not use console or trigger any instrumentation to avoid loops.\n */\nexport class HttpRequestMonitorSpanProcessor implements SpanProcessor {\n constructor(private readonly processor: SpanProcessor) {}\n\n onStart(span: Span, parentContext: Context): void {\n if (isHttpSpan(span)) {\n const url = getAttr(span, ATTR_HTTP_URL) ?? '';\n const method = getAttr(span, ATTR_HTTP_METHOD) ?? 'GET';\n const requestId = span.spanContext().spanId;\n const startTimeMs = Date.now();\n\n const payload: HttpRequestMessagePayload = {\n requestId,\n url,\n method,\n startTime: startTimeMs,\n };\n notifyHttpRequestStart(payload);\n }\n\n this.processor.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n if (isHttpSpan(span)) {\n const url = getAttr(span, ATTR_HTTP_URL) ?? '';\n const method = getAttr(span, ATTR_HTTP_METHOD) ?? 'GET';\n const requestId = span.spanContext().spanId;\n const statusAttr = getAttr(span, ATTR_HTTP_STATUS_CODE);\n const status = statusAttr != null ? parseInt(statusAttr, 10) : undefined;\n const spanWithTime = span as ReadableSpan & {\n startTimeUnixNano?: number | bigint;\n endTimeUnixNano?: number | bigint;\n };\n const startNs = spanWithTime.startTimeUnixNano;\n const endNs = spanWithTime.endTimeUnixNano;\n const startTimeMs = startNs != null && !Number.isNaN(Number(startNs)) ? Number(startNs) / 1_000_000 : Date.now();\n const endTimeMs = endNs != null && !Number.isNaN(Number(endNs)) ? Number(endNs) / 1_000_000 : Date.now();\n\n const payload: HttpRequestMessagePayload = {\n requestId,\n url,\n method,\n startTime: startTimeMs,\n endTime: endTimeMs,\n status: !Number.isNaN(status) ? status : undefined,\n };\n notifyHttpRequestEnd(payload);\n }\n\n this.processor.onEnd(span);\n }\n\n forceFlush(): Promise<void> {\n return this.processor.forceFlush();\n }\n\n shutdown(): Promise<void> {\n return this.processor.shutdown();\n }\n}\n"]}
1
+ {"version":3,"file":"httpRequestMonitorSpanProcessor.js","sourceRoot":"","sources":["../../../src/processors/httpRequestMonitorSpanProcessor.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context } from '@opentelemetry/api';\nimport { hrTimeToMilliseconds } from '@opentelemetry/core';\nimport type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport {\n type HttpRequestMessagePayload,\n notifyHttpRequestEnd,\n notifyHttpRequestStart,\n} from '@grafana/faro-react-native';\n\nimport { captureFetchUserAction } from '../instrumentations/instrumentationUtils';\n\n// Optional XHR instrumentation still emits these attributes.\nconst ATTR_HTTP_METHOD = 'http.method';\nconst ATTR_HTTP_STATUS_CODE = 'http.status_code';\nconst ATTR_HTTP_URL = 'http.url';\n\n/**\n * Get string attribute from span.\n * Handles both SDK attribute format and OTLP-style attribute values.\n */\nfunction getAttr(span: Span | ReadableSpan, key: string): string | undefined {\n const attrs = span.attributes as Record<string, unknown> | undefined;\n if (attrs == null) return undefined;\n const raw = typeof attrs['get'] === 'function' ? (attrs as { get: (k: string) => unknown })['get'](key) : attrs[key];\n if (raw == null) return undefined;\n if (typeof raw === 'string') return raw.length > 0 ? raw : undefined;\n if (typeof raw === 'object' && raw !== null && 'stringValue' in raw) {\n const s = String((raw as { stringValue?: string }).stringValue ?? '');\n return s.length > 0 ? s : undefined;\n }\n const str = String(raw);\n return str.length > 0 ? str : undefined;\n}\n\n/**\n * Check if a span is an HTTP span (from FetchInstrumentation).\n */\nfunction isHttpSpan(span: Span | ReadableSpan): boolean {\n // url.full is shared by non-HTTP spans; stable HTTP spans identify their method at creation.\n return getHttpMethod(span) != null || getAttr(span, ATTR_HTTP_URL) != null;\n}\n\nfunction getHttpUrl(span: Span | ReadableSpan): string | undefined {\n return getAttr(span, 'url.full') ?? getAttr(span, ATTR_HTTP_URL);\n}\n\nfunction getHttpMethod(span: Span | ReadableSpan): string | undefined {\n return getAttr(span, 'http.request.method') ?? getAttr(span, ATTR_HTTP_METHOD);\n}\n\n/**\n * SpanProcessor that notifies httpRequestMonitor when HTTP spans start and end.\n *\n * This enables user action correlation (UserActionController halt logic) when\n * TracingInstrumentation is used instead of HttpInstrumentation.\n *\n * IMPORTANT: Must not use console or trigger any instrumentation to avoid loops.\n */\nexport class HttpRequestMonitorSpanProcessor implements SpanProcessor {\n constructor(private readonly processor: SpanProcessor) {}\n\n onStart(span: Span, parentContext: Context): void {\n if (isHttpSpan(span)) {\n // Capture before notifyStart changes Started to Halted. New requests while\n // already Halted are not tracked by the controller and must not inherit it.\n if (span.instrumentationScope.name === '@opentelemetry/instrumentation-fetch') {\n captureFetchUserAction(span);\n }\n const url = getHttpUrl(span) ?? '';\n const method = getHttpMethod(span) ?? 'GET';\n const requestId = span.spanContext().spanId;\n const startTimeMs = hrTimeToMilliseconds(span.startTime);\n\n const payload: HttpRequestMessagePayload = {\n requestId,\n url,\n method,\n startTime: startTimeMs,\n };\n notifyHttpRequestStart(payload);\n }\n\n this.processor.onStart(span, parentContext);\n }\n\n onEnd(span: ReadableSpan): void {\n if (isHttpSpan(span)) {\n const url = getHttpUrl(span) ?? '';\n const method = getHttpMethod(span) ?? 'GET';\n const requestId = span.spanContext().spanId;\n const statusAttr = getAttr(span, 'http.response.status_code') ?? getAttr(span, ATTR_HTTP_STATUS_CODE);\n const status = statusAttr != null ? parseInt(statusAttr, 10) : undefined;\n const startTimeMs = hrTimeToMilliseconds(span.startTime);\n const endTimeMs = hrTimeToMilliseconds(span.endTime);\n\n const payload: HttpRequestMessagePayload = {\n requestId,\n url,\n method,\n startTime: startTimeMs,\n endTime: endTimeMs,\n status: !Number.isNaN(status) ? status : undefined,\n };\n notifyHttpRequestEnd(payload);\n }\n\n this.processor.onEnd(span);\n }\n\n forceFlush(): Promise<void> {\n return this.processor.forceFlush();\n }\n\n shutdown(): Promise<void> {\n return this.processor.shutdown();\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Attributes, ContextManager, TextMapPropagator } from '@opentelemetry/api';\nimport type { Instrumentation } from '@opentelemetry/instrumentation';\nimport type { FetchCustomAttributeFunction } from '@opentelemetry/instrumentation-fetch';\nimport type { XHRCustomAttributeFunction } from '@opentelemetry/instrumentation-xml-http-request';\nimport type { SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport type { API, Patterns } from '@grafana/faro-core';\n\nexport type InstrumentationOption = Instrumentation | Instrumentation[];\n\nexport interface FaroTraceExporterConfig {\n api: API;\n}\n\nexport interface TracingInstrumentationOptions {\n resourceAttributes?: Attributes;\n propagator?: TextMapPropagator;\n contextManager?: ContextManager;\n instrumentations?: InstrumentationOption[];\n spanProcessor?: SpanProcessor;\n instrumentationOptions?: Omit<DefaultInstrumentationsOptions, 'ignoreUrls'>;\n}\n\nexport type MatchUrlDefinitions = Patterns;\n\nexport type DefaultInstrumentationsOptions = {\n ignoreUrls?: MatchUrlDefinitions;\n propagateTraceHeaderCorsUrls?: MatchUrlDefinitions;\n enableFetchInstrumentation?: boolean;\n enableXhrInstrumentation?: boolean;\n\n fetchInstrumentationOptions?: {\n applyCustomAttributesOnSpan?: FetchCustomAttributeFunction;\n ignoreNetworkEvents?: boolean;\n };\n\n xhrInstrumentationOptions?: {\n applyCustomAttributesOnSpan?: XHRCustomAttributeFunction;\n ignoreNetworkEvents?: boolean;\n };\n};\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Attributes, ContextManager, TextMapPropagator } from '@opentelemetry/api';\nimport type { Instrumentation } from '@opentelemetry/instrumentation';\nimport type { FetchCustomAttributeFunction } from '@opentelemetry/instrumentation-fetch';\nimport type { XHRCustomAttributeFunction } from '@opentelemetry/instrumentation-xml-http-request';\nimport type { SpanProcessor } from '@opentelemetry/sdk-trace-base';\n\nimport type { API, Patterns } from '@grafana/faro-core';\n\nexport type InstrumentationOption = Instrumentation | Instrumentation[];\n\nexport interface FaroTraceExporterConfig {\n api: API;\n}\n\nexport interface TracingInstrumentationOptions {\n resourceAttributes?: Attributes;\n propagator?: TextMapPropagator;\n contextManager?: ContextManager;\n instrumentations?: InstrumentationOption[];\n /**\n * A caller-supplied processor. Faro removal flushes it so the tracing instrumentation can be re-added;\n * TracingInstrumentation.shutdown() shuts it down and is terminal for that processor.\n */\n spanProcessor?: SpanProcessor;\n instrumentationOptions?: Omit<DefaultInstrumentationsOptions, 'ignoreUrls'>;\n}\n\nexport type MatchUrlDefinitions = Patterns;\n\nexport type DefaultInstrumentationsOptions = {\n ignoreUrls?: MatchUrlDefinitions;\n propagateTraceHeaderCorsUrls?: MatchUrlDefinitions;\n enableFetchInstrumentation?: boolean;\n enableXhrInstrumentation?: boolean;\n\n fetchInstrumentationOptions?: {\n applyCustomAttributesOnSpan?: FetchCustomAttributeFunction;\n ignoreNetworkEvents?: boolean;\n };\n\n xhrInstrumentationOptions?: {\n applyCustomAttributesOnSpan?: XHRCustomAttributeFunction;\n ignoreNetworkEvents?: boolean;\n };\n};\n"]}
@@ -31,7 +31,10 @@ export declare class TracingInstrumentation extends BaseInstrumentation {
31
31
  name: string;
32
32
  version: string;
33
33
  static SCHEDULED_BATCH_DELAY_MS: number;
34
- private provider?;
34
+ private activeRegistration?;
35
+ private defaultInstrumentations?;
36
+ private pendingShutdowns;
37
+ private suppliedSpanProcessorShutdown?;
35
38
  constructor(options?: TracingInstrumentationOptions);
36
39
  initialize(): void;
37
40
  /**
@@ -39,8 +42,13 @@ export declare class TracingInstrumentation extends BaseInstrumentation {
39
42
  * CRITICAL: This prevents infinite loops where trace exports trigger more traces
40
43
  */
41
44
  private getIgnoreUrls;
42
- /**
43
- * Shutdown the tracer provider
44
- */
45
+ private detachRegistration;
46
+ private runCleanup;
47
+ private logCleanupError;
48
+ private startProviderShutdown;
49
+ private startSuppliedSpanProcessorShutdown;
50
+ /** Detach request instrumentations and wait for the tracer provider to flush. */
45
51
  shutdown(): Promise<void>;
52
+ /** Detach synchronously when Faro removes this instrumentation. */
53
+ destroy(): void;
46
54
  }
@@ -1,3 +1,4 @@
1
+ import type { Instrumentation } from '@opentelemetry/instrumentation';
1
2
  import type { DefaultInstrumentationsOptions, InstrumentationOption } from '../types';
2
3
  /**
3
4
  * Get default OTEL instrumentations for React Native
@@ -15,3 +16,4 @@ import type { DefaultInstrumentationsOptions, InstrumentationOption } from '../t
15
16
  * @returns Array of OTEL instrumentations
16
17
  */
17
18
  export declare function getDefaultOTELInstrumentations(options?: DefaultInstrumentationsOptions): InstrumentationOption[];
19
+ export declare function updateDefaultOTELInstrumentations(instrumentations: Instrumentation[], options?: DefaultInstrumentationsOptions): void;
@@ -1,6 +1,8 @@
1
1
  import type { Span } from '@opentelemetry/api';
2
2
  import type { FetchCustomAttributeFunction } from '@opentelemetry/instrumentation-fetch';
3
3
  import type { XHRCustomAttributeFunction } from '@opentelemetry/instrumentation-xml-http-request';
4
+ /** Capture request ownership at start, before the monitor can halt the action. */
5
+ export declare function captureFetchUserAction(span: Span): void;
4
6
  /**
5
7
  * Set span status to ERROR when fetch fails
6
8
  *
@@ -13,6 +13,10 @@ export interface TracingInstrumentationOptions {
13
13
  propagator?: TextMapPropagator;
14
14
  contextManager?: ContextManager;
15
15
  instrumentations?: InstrumentationOption[];
16
+ /**
17
+ * A caller-supplied processor. Faro removal flushes it so the tracing instrumentation can be re-added;
18
+ * TracingInstrumentation.shutdown() shuts it down and is terminal for that processor.
19
+ */
16
20
  spanProcessor?: SpanProcessor;
17
21
  instrumentationOptions?: Omit<DefaultInstrumentationsOptions, 'ignoreUrls'>;
18
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/faro-react-native-tracing",
3
- "version": "1.3.0",
3
+ "version": "2.0.0",
4
4
  "description": "Faro React Native tracing implementation with OpenTelemetry.",
5
5
  "keywords": [
6
6
  "observability",
@@ -51,18 +51,18 @@
51
51
  "quality:circular-deps": "madge --circular ."
52
52
  },
53
53
  "dependencies": {
54
- "@grafana/faro-core": "^2.7.0",
55
- "@grafana/faro-react-native": "^1.3.0",
54
+ "@grafana/faro-core": "^2.8.2",
55
+ "@grafana/faro-react-native": "^1.4.0",
56
56
  "@opentelemetry/api": "^1.9.0",
57
- "@opentelemetry/core": "^2.0.0",
58
- "@opentelemetry/instrumentation": "^0.208.0",
59
- "@opentelemetry/instrumentation-fetch": "^0.208.0",
60
- "@opentelemetry/instrumentation-xml-http-request": "^0.208.0",
61
- "@opentelemetry/otlp-transformer": "^0.208.0",
62
- "@opentelemetry/resources": "^2.0.0",
63
- "@opentelemetry/sdk-trace-base": "^2.0.0",
64
- "@opentelemetry/sdk-trace-web": "^2.0.0",
65
- "@opentelemetry/semantic-conventions": "^1.32.0"
57
+ "@opentelemetry/core": "^2.9.0",
58
+ "@opentelemetry/instrumentation": "^0.220.0",
59
+ "@opentelemetry/instrumentation-fetch": "^0.220.0",
60
+ "@opentelemetry/instrumentation-xml-http-request": "^0.220.0",
61
+ "@opentelemetry/otlp-transformer": "^0.220.0",
62
+ "@opentelemetry/resources": "^2.9.0",
63
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
64
+ "@opentelemetry/sdk-trace-web": "^2.9.0",
65
+ "@opentelemetry/semantic-conventions": "^1.43.0"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "react-native": ">=0.70.0"
@@ -74,5 +74,5 @@
74
74
  "publishConfig": {
75
75
  "access": "public"
76
76
  },
77
- "gitHead": "7cd0482f59a7eefad5a812e468d259e17ad39ed6"
77
+ "gitHead": "38d87a3474585e1177c38d2e9acd04860a4236e6"
78
78
  }
@@ -0,0 +1,90 @@
1
+ import type { IResourceSpans } from '@opentelemetry/otlp-transformer/build/src/trace/internal-types';
2
+
3
+ import { initializeFaro, TransportItemType } from '@grafana/faro-core';
4
+ import type { Config, EventEvent } from '@grafana/faro-core';
5
+ import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
6
+
7
+ import { sendFaroEvents as send } from './faroTraceExporter.utils';
8
+
9
+ function fixture(paths = ['/same']): IResourceSpans[] {
10
+ return [
11
+ {
12
+ scopeSpans: [
13
+ {
14
+ scope: { name: '@opentelemetry/instrumentation-fetch' },
15
+ spans: paths.map((path, i) => ({
16
+ kind: 3,
17
+ traceId: 'trace',
18
+ spanId: String(i),
19
+ startTimeUnixNano: '1000000000',
20
+ endTimeUnixNano: '1001000000',
21
+ attributes: Object.entries({
22
+ 'http.request.method': 'GET',
23
+ 'url.full': 'https://example.com' + path,
24
+ 'http.response.status_code': '200',
25
+ 'server.address': 'example.com',
26
+ }).map(([key, value]) => ({ key, value: { stringValue: value } })),
27
+ })),
28
+ },
29
+ ],
30
+ },
31
+ ] as unknown as IResourceSpans[];
32
+ }
33
+
34
+ describe('span-derived event pipeline', () => {
35
+ function setup(beforeSend?: Config['beforeSend']) {
36
+ const transport = new MockTransport();
37
+ const faro = initializeFaro(
38
+ mockConfig({ transports: [transport], dedupe: true, ...(beforeSend ? { beforeSend } : {}) })
39
+ );
40
+ const events = () =>
41
+ transport.items.filter((item) => item.type === TransportItemType.EVENT).map((item) => item.payload as EventEvent);
42
+ return { transport, faro, events };
43
+ }
44
+ it('keeps payment-failed, fetch, payment-failed', () => {
45
+ const { faro, events } = setup();
46
+ faro.api.pushEvent('payment-failed');
47
+ send(fixture());
48
+ faro.api.pushEvent('payment-failed');
49
+ expect(events().map((e) => e.name)).toEqual(['payment-failed', 'faro.tracing.fetch', 'payment-failed']);
50
+ });
51
+ it('keeps fetch, other event, identical fetch', () => {
52
+ const { faro, events } = setup();
53
+ send(fixture());
54
+ faro.api.pushEvent('other-event');
55
+ send(fixture());
56
+ expect(events().map((e) => e.name)).toEqual(['faro.tracing.fetch', 'other-event', 'faro.tracing.fetch']);
57
+ });
58
+ it('continues after a throwing beforeSend hook', () => {
59
+ const { events } = setup((item) => {
60
+ if (
61
+ item.type === TransportItemType.EVENT &&
62
+ (item.payload as EventEvent).attributes?.['http.url'] === 'https://example.com/first'
63
+ )
64
+ throw new Error('test hook failure');
65
+ return item;
66
+ });
67
+ send(fixture(['/first', '/second', '/third']));
68
+ expect(events().map((e) => e.attributes?.['http.url'])).toEqual([
69
+ 'https://example.com/second',
70
+ 'https://example.com/third',
71
+ ]);
72
+ });
73
+ it('continues after a throwing transport', () => {
74
+ const { transport, events } = setup();
75
+ const originalSend = transport.send.bind(transport);
76
+ let first = true;
77
+ transport.send = (...args: Parameters<typeof transport.send>) => {
78
+ if (first) {
79
+ first = false;
80
+ throw new Error('test transport failure');
81
+ }
82
+ return originalSend(...args);
83
+ };
84
+ send(fixture(['/first', '/second', '/third']));
85
+ expect(events().map((e) => e.attributes?.['http.url'])).toEqual([
86
+ 'https://example.com/second',
87
+ 'https://example.com/third',
88
+ ]);
89
+ });
90
+ });
@@ -1,5 +1,6 @@
1
1
  import { ExportResultCode } from '@opentelemetry/core';
2
2
  import type { ExportResult } from '@opentelemetry/core';
3
+ import { JSON_ENCODER } from '@opentelemetry/otlp-transformer/build/src/common/utils';
3
4
  import { createExportTraceServiceRequest } from '@opentelemetry/otlp-transformer/build/src/trace/internal';
4
5
  import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
5
6
 
@@ -35,7 +36,7 @@ export class FaroTraceExporter implements SpanExporter {
35
36
 
36
37
  try {
37
38
  // Convert spans to OTLP format
38
- const traceEvent = createExportTraceServiceRequest(spans, { useHex: true, useLongBits: false });
39
+ const traceEvent = createExportTraceServiceRequest(spans, JSON_ENCODER);
39
40
 
40
41
  // Send traces to Faro
41
42
  this.config.api.pushTraces(traceEvent);
@@ -7,6 +7,34 @@ import type { EventAttributes as FaroEventAttributes } from '@grafana/faro-core'
7
7
  const internalLogger = createInternalLogger();
8
8
 
9
9
  const DURATION_NS_KEY = 'duration_ns';
10
+ /** Map stable OTel span attributes to Faro's fetch event contract. */
11
+ function projectFetchEventAttributes(attributes: FaroEventAttributes): void {
12
+ const fields = {
13
+ 'http.request.method': 'http.method',
14
+ 'url.full': 'http.url',
15
+ 'http.response.status_code': 'http.status_code',
16
+ };
17
+ for (const [spanField, eventField] of Object.entries(fields)) {
18
+ if (attributes[spanField] != null) {
19
+ attributes[eventField] = attributes[spanField];
20
+ delete attributes[spanField];
21
+ }
22
+ }
23
+
24
+ const url = attributes['http.url'];
25
+ if (url) {
26
+ try {
27
+ const parsed = new URL(url);
28
+ attributes['http.host'] = attributes['http.host'] ?? parsed.host;
29
+ attributes['http.scheme'] = attributes['http.scheme'] ?? parsed.protocol.replace(':', '');
30
+ } catch {
31
+ // Keep the captured URL even if it cannot supply host/scheme metadata.
32
+ }
33
+ }
34
+ delete attributes['server.address'];
35
+ delete attributes['server.port'];
36
+ attributes['component'] = attributes['component'] ?? 'fetch';
37
+ }
10
38
 
11
39
  /**
12
40
  * Send Faro events for CLIENT spans (HTTP requests, navigation, etc.)
@@ -48,6 +76,10 @@ export function sendFaroEvents(resourceSpans: IResourceSpans[] = []) {
48
76
  faroEventAttributes[attribute.key] = String(Object.values(attribute.value)[0]);
49
77
  }
50
78
 
79
+ if (scope?.name === '@opentelemetry/instrumentation-fetch') {
80
+ projectFetchEventAttributes(faroEventAttributes);
81
+ }
82
+
51
83
  // Add span duration in nanoseconds
52
84
  if (!Number.isNaN(span.endTimeUnixNano) && !Number.isNaN(span.startTimeUnixNano)) {
53
85
  faroEventAttributes[DURATION_NS_KEY] = String(