@grafana/faro-react-native-tracing 1.0.0-alpha.1
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/LICENSE +201 -0
- package/README.md +833 -0
- package/dist/cjs/exporters/faroTraceExporter.js +101 -0
- package/dist/cjs/exporters/faroTraceExporter.js.map +1 -0
- package/dist/cjs/exporters/faroTraceExporter.utils.js +99 -0
- package/dist/cjs/exporters/faroTraceExporter.utils.js.map +1 -0
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/instrumentation.js +251 -0
- package/dist/cjs/instrumentation.js.map +1 -0
- package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js +77 -0
- package/dist/cjs/instrumentations/getDefaultOTELInstrumentations.js.map +1 -0
- package/dist/cjs/instrumentations/instrumentationUtils.js +89 -0
- package/dist/cjs/instrumentations/instrumentationUtils.js.map +1 -0
- package/dist/cjs/processors/faroMetaAttributesSpanProcessor.js +59 -0
- package/dist/cjs/processors/faroMetaAttributesSpanProcessor.js.map +1 -0
- package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js +98 -0
- package/dist/cjs/processors/httpRequestMonitorSpanProcessor.js.map +1 -0
- package/dist/cjs/semconv.js +29 -0
- package/dist/cjs/semconv.js.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/utils/sampler.js +21 -0
- package/dist/cjs/utils/sampler.js.map +1 -0
- package/dist/esm/exporters/faroTraceExporter.js +65 -0
- package/dist/esm/exporters/faroTraceExporter.js.map +1 -0
- package/dist/esm/exporters/faroTraceExporter.utils.js +88 -0
- package/dist/esm/exporters/faroTraceExporter.utils.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/instrumentation.js +183 -0
- package/dist/esm/instrumentation.js.map +1 -0
- package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js +62 -0
- package/dist/esm/instrumentations/getDefaultOTELInstrumentations.js.map +1 -0
- package/dist/esm/instrumentations/instrumentationUtils.js +83 -0
- package/dist/esm/instrumentations/instrumentationUtils.js.map +1 -0
- package/dist/esm/processors/faroMetaAttributesSpanProcessor.js +54 -0
- package/dist/esm/processors/faroMetaAttributesSpanProcessor.js.map +1 -0
- package/dist/esm/processors/httpRequestMonitorSpanProcessor.js +93 -0
- package/dist/esm/processors/httpRequestMonitorSpanProcessor.js.map +1 -0
- package/dist/esm/semconv.js +26 -0
- package/dist/esm/semconv.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils/sampler.js +17 -0
- package/dist/esm/utils/sampler.js.map +1 -0
- package/dist/types/exporters/faroTraceExporter.d.ts +21 -0
- package/dist/types/exporters/faroTraceExporter.utils.d.ts +17 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/instrumentation.d.ts +46 -0
- package/dist/types/instrumentations/getDefaultOTELInstrumentations.d.ts +17 -0
- package/dist/types/instrumentations/instrumentationUtils.d.ts +26 -0
- package/dist/types/processors/faroMetaAttributesSpanProcessor.d.ts +22 -0
- package/dist/types/processors/httpRequestMonitorSpanProcessor.d.ts +18 -0
- package/dist/types/semconv.d.ts +19 -0
- package/dist/types/types.d.ts +31 -0
- package/dist/types/utils/sampler.d.ts +12 -0
- package/package.json +77 -0
- package/src/exporters/faroTraceExporter.test.ts +110 -0
- package/src/exporters/faroTraceExporter.ts +64 -0
- package/src/exporters/faroTraceExporter.utils.ts +105 -0
- package/src/index.ts +16 -0
- package/src/instrumentation.ts +241 -0
- package/src/instrumentations/getDefaultOTELInstrumentations.test.ts +70 -0
- package/src/instrumentations/getDefaultOTELInstrumentations.ts +83 -0
- package/src/instrumentations/instrumentationUtils.test.ts +107 -0
- package/src/instrumentations/instrumentationUtils.ts +106 -0
- package/src/processors/faroMetaAttributesSpanProcessor.test.ts +127 -0
- package/src/processors/faroMetaAttributesSpanProcessor.ts +71 -0
- package/src/processors/httpRequestMonitorSpanProcessor.ts +106 -0
- package/src/semconv.ts +31 -0
- package/src/types.ts +39 -0
- package/src/utils/sampler.test.ts +51 -0
- package/src/utils/sampler.ts +19 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExportResult } from '@opentelemetry/core';
|
|
2
|
+
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
import type { FaroTraceExporterConfig } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* FaroTraceExporter for React Native
|
|
6
|
+
*
|
|
7
|
+
* Exports OpenTelemetry spans to Faro backend using pushTraces API.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: To avoid infinite loops:
|
|
10
|
+
* - Uses internalLogger instead of console
|
|
11
|
+
* - Does NOT log during export (except errors)
|
|
12
|
+
* - Relies on Faro's internal deduplication
|
|
13
|
+
*/
|
|
14
|
+
export declare class FaroTraceExporter implements SpanExporter {
|
|
15
|
+
private config;
|
|
16
|
+
private _isShutdown;
|
|
17
|
+
constructor(config: FaroTraceExporterConfig);
|
|
18
|
+
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void;
|
|
19
|
+
shutdown(): Promise<void>;
|
|
20
|
+
forceFlush(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type IResourceSpans } from '@opentelemetry/otlp-transformer/build/src/trace/internal-types';
|
|
2
|
+
/**
|
|
3
|
+
* Send Faro events for CLIENT spans (HTTP requests, navigation, etc.)
|
|
4
|
+
*
|
|
5
|
+
* IMPORTANT: This function is called during trace export and must be careful to avoid infinite loops.
|
|
6
|
+
*
|
|
7
|
+
* Infinite loop prevention strategy:
|
|
8
|
+
* 1. Only process SPAN_KIND_CLIENT spans (HTTP requests, etc.)
|
|
9
|
+
* 2. Use faro.api.pushEvent which:
|
|
10
|
+
* - Does NOT trigger console logs if ConsoleInstrumentation is configured properly
|
|
11
|
+
* - Does NOT trigger HTTP instrumentation for collector URLs (they're ignored)
|
|
12
|
+
* 3. Use internalLogger for debugging instead of console
|
|
13
|
+
* 4. Never call console.log/warn/error in this function
|
|
14
|
+
*
|
|
15
|
+
* @param resourceSpans - OTLP resource spans from trace exporter
|
|
16
|
+
*/
|
|
17
|
+
export declare function sendFaroEvents(resourceSpans?: IResourceSpans[]): void;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { FaroTraceExporter } from './exporters/faroTraceExporter';
|
|
2
|
+
export { getDefaultOTELInstrumentations } from './instrumentations/getDefaultOTELInstrumentations';
|
|
3
|
+
export { TracingInstrumentation } from './instrumentation';
|
|
4
|
+
export { getSamplingDecision } from './utils/sampler';
|
|
5
|
+
export type { FaroTraceExporterConfig, TracingInstrumentationOptions } from './types';
|
|
6
|
+
export { setSpanStatusOnFetchError, fetchCustomAttributeFunctionWithDefaults, } from './instrumentations/instrumentationUtils';
|
|
7
|
+
export { FaroMetaAttributesSpanProcessor } from './processors/faroMetaAttributesSpanProcessor';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BaseInstrumentation } from '@grafana/faro-core';
|
|
2
|
+
import type { TracingInstrumentationOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* TracingInstrumentation for React Native
|
|
5
|
+
*
|
|
6
|
+
* Enables distributed tracing with OpenTelemetry for React Native applications.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: Infinite loop prevention
|
|
9
|
+
* - Uses internalLogger for debugging instead of console
|
|
10
|
+
* - Collector URLs are added to ignoreUrls in HTTP instrumentation
|
|
11
|
+
* - BatchSpanProcessor delays span export to avoid blocking
|
|
12
|
+
* - No console logging during trace export
|
|
13
|
+
*
|
|
14
|
+
* Example usage:
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { initializeFaro } from '@grafana/faro-react-native';
|
|
17
|
+
* import { TracingInstrumentation } from '@grafana/faro-react-native-tracing';
|
|
18
|
+
*
|
|
19
|
+
* initializeFaro({
|
|
20
|
+
* // ... other config
|
|
21
|
+
* instrumentations: [
|
|
22
|
+
* new TracingInstrumentation({
|
|
23
|
+
* propagateTraceHeaderCorsUrls: [/https:\\/\\/my-api\\.com/],
|
|
24
|
+
* }),
|
|
25
|
+
* ],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class TracingInstrumentation extends BaseInstrumentation {
|
|
30
|
+
private options;
|
|
31
|
+
name: string;
|
|
32
|
+
version: string;
|
|
33
|
+
static SCHEDULED_BATCH_DELAY_MS: number;
|
|
34
|
+
private provider?;
|
|
35
|
+
constructor(options?: TracingInstrumentationOptions);
|
|
36
|
+
initialize(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Get ignore URLs from all transports to avoid tracing collector requests
|
|
39
|
+
* CRITICAL: This prevents infinite loops where trace exports trigger more traces
|
|
40
|
+
*/
|
|
41
|
+
private getIgnoreUrls;
|
|
42
|
+
/**
|
|
43
|
+
* Shutdown the tracer provider
|
|
44
|
+
*/
|
|
45
|
+
shutdown(): Promise<void>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DefaultInstrumentationsOptions, InstrumentationOption } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Get default OTEL instrumentations for React Native
|
|
4
|
+
*
|
|
5
|
+
* This function creates the default OpenTelemetry instrumentations for React Native:
|
|
6
|
+
* - FetchInstrumentation: Traces fetch() API calls
|
|
7
|
+
* - XMLHttpRequestInstrumentation: Traces XMLHttpRequest and axios (which uses XHR)
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: Infinite loop prevention
|
|
10
|
+
* - ignoreUrls is used to exclude Faro collector URLs
|
|
11
|
+
* - ignoreNetworkEvents is true to avoid duplicate events
|
|
12
|
+
* - No console logging during instrumentation
|
|
13
|
+
*
|
|
14
|
+
* @param options - Configuration options
|
|
15
|
+
* @returns Array of OTEL instrumentations
|
|
16
|
+
*/
|
|
17
|
+
export declare function getDefaultOTELInstrumentations(options?: DefaultInstrumentationsOptions): InstrumentationOption[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Span } from '@opentelemetry/api';
|
|
2
|
+
import type { FetchCustomAttributeFunction } from '@opentelemetry/instrumentation-fetch';
|
|
3
|
+
import type { XHRCustomAttributeFunction } from '@opentelemetry/instrumentation-xml-http-request';
|
|
4
|
+
/**
|
|
5
|
+
* Set span status to ERROR when fetch fails
|
|
6
|
+
*
|
|
7
|
+
* This ensures that failed HTTP requests are marked as errors in traces.
|
|
8
|
+
*/
|
|
9
|
+
export declare function setSpanStatusOnFetchError(span: Span, error: Error | string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Custom attribute function for fetch instrumentation with defaults
|
|
12
|
+
*
|
|
13
|
+
* Combines user-provided custom attributes with default handling.
|
|
14
|
+
*
|
|
15
|
+
* @param userFunction - Optional user-provided custom attribute function
|
|
16
|
+
* @returns Combined custom attribute function
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchCustomAttributeFunctionWithDefaults(userFunction?: FetchCustomAttributeFunction): FetchCustomAttributeFunction;
|
|
19
|
+
/**
|
|
20
|
+
* Set span status to ERROR for XHR failures (status 0 or 4xx/5xx).
|
|
21
|
+
*/
|
|
22
|
+
export declare function setSpanStatusOnXMLHttpRequestError(span: Span, xhr: XMLHttpRequest): void;
|
|
23
|
+
/**
|
|
24
|
+
* Custom attribute function for XHR instrumentation with defaults.
|
|
25
|
+
*/
|
|
26
|
+
export declare function xhrCustomAttributeFunctionWithDefaults(userFunction?: XHRCustomAttributeFunction): XHRCustomAttributeFunction;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Context } from '@opentelemetry/api';
|
|
2
|
+
import type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
import type { Metas } from '@grafana/faro-core';
|
|
4
|
+
/**
|
|
5
|
+
* Span processor that adds Faro meta attributes to spans
|
|
6
|
+
*
|
|
7
|
+
* This processor enriches spans with:
|
|
8
|
+
* - Session ID
|
|
9
|
+
* - User information (email, id, username, etc.)
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: This processor delegates to a wrapped processor and does NOT log
|
|
12
|
+
* to avoid infinite loops.
|
|
13
|
+
*/
|
|
14
|
+
export declare class FaroMetaAttributesSpanProcessor implements SpanProcessor {
|
|
15
|
+
private processor;
|
|
16
|
+
private metas;
|
|
17
|
+
constructor(processor: SpanProcessor, metas: Metas);
|
|
18
|
+
forceFlush(): Promise<void>;
|
|
19
|
+
onStart(span: Span, parentContext: Context): void;
|
|
20
|
+
onEnd(span: ReadableSpan): void;
|
|
21
|
+
shutdown(): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from '@opentelemetry/api';
|
|
2
|
+
import type { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
/**
|
|
4
|
+
* SpanProcessor that notifies httpRequestMonitor when HTTP spans start and end.
|
|
5
|
+
*
|
|
6
|
+
* This enables user action correlation (UserActionController halt logic) when
|
|
7
|
+
* TracingInstrumentation is used instead of HttpInstrumentation.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: Must not use console or trigger any instrumentation to avoid loops.
|
|
10
|
+
*/
|
|
11
|
+
export declare class HttpRequestMonitorSpanProcessor implements SpanProcessor {
|
|
12
|
+
private readonly processor;
|
|
13
|
+
constructor(processor: SpanProcessor);
|
|
14
|
+
onStart(span: Span, parentContext: Context): void;
|
|
15
|
+
onEnd(span: ReadableSpan): void;
|
|
16
|
+
forceFlush(): Promise<void>;
|
|
17
|
+
shutdown(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic convention attributes for React Native tracing
|
|
3
|
+
* Based on OpenTelemetry semantic conventions with React Native-specific additions
|
|
4
|
+
*/
|
|
5
|
+
export declare const ATTR_SESSION_ID = "session.id";
|
|
6
|
+
export declare const ATTR_DEVICE_MODEL = "device.model";
|
|
7
|
+
export declare const ATTR_DEVICE_BRAND = "device.brand";
|
|
8
|
+
export declare const ATTR_DEVICE_PLATFORM = "device.platform";
|
|
9
|
+
export declare const ATTR_DEVICE_OS_VERSION = "device.os.version";
|
|
10
|
+
export declare const ATTR_DEVICE_TYPE = "device.type";
|
|
11
|
+
export declare const ATTR_DEVICE_LOCALE = "device.locale";
|
|
12
|
+
export declare const ATTR_APP_VERSION = "app.version";
|
|
13
|
+
export declare const ATTR_APP_BUILD = "app.build";
|
|
14
|
+
export declare const ATTR_SERVICE_NAMESPACE = "service.namespace";
|
|
15
|
+
export declare const ATTR_DEPLOYMENT_ENVIRONMENT_NAME = "deployment.environment.name";
|
|
16
|
+
export declare const ATTR_TELEMETRY_DISTRO_NAME = "telemetry.distro.name";
|
|
17
|
+
export declare const ATTR_TELEMETRY_DISTRO_VERSION = "telemetry.distro.version";
|
|
18
|
+
export declare const ATTR_PROCESS_RUNTIME_NAME = "process.runtime.name";
|
|
19
|
+
export declare const ATTR_PROCESS_RUNTIME_VERSION = "process.runtime.version";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Attributes, ContextManager, TextMapPropagator } from '@opentelemetry/api';
|
|
2
|
+
import type { Instrumentation } from '@opentelemetry/instrumentation';
|
|
3
|
+
import type { FetchCustomAttributeFunction } from '@opentelemetry/instrumentation-fetch';
|
|
4
|
+
import type { XHRCustomAttributeFunction } from '@opentelemetry/instrumentation-xml-http-request';
|
|
5
|
+
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
6
|
+
import type { API, Patterns } from '@grafana/faro-core';
|
|
7
|
+
export type InstrumentationOption = Instrumentation | Instrumentation[];
|
|
8
|
+
export interface FaroTraceExporterConfig {
|
|
9
|
+
api: API;
|
|
10
|
+
}
|
|
11
|
+
export interface TracingInstrumentationOptions {
|
|
12
|
+
resourceAttributes?: Attributes;
|
|
13
|
+
propagator?: TextMapPropagator;
|
|
14
|
+
contextManager?: ContextManager;
|
|
15
|
+
instrumentations?: InstrumentationOption[];
|
|
16
|
+
spanProcessor?: SpanProcessor;
|
|
17
|
+
instrumentationOptions?: Omit<DefaultInstrumentationsOptions, 'ignoreUrls'>;
|
|
18
|
+
}
|
|
19
|
+
export type MatchUrlDefinitions = Patterns;
|
|
20
|
+
export type DefaultInstrumentationsOptions = {
|
|
21
|
+
ignoreUrls?: MatchUrlDefinitions;
|
|
22
|
+
propagateTraceHeaderCorsUrls?: MatchUrlDefinitions;
|
|
23
|
+
fetchInstrumentationOptions?: {
|
|
24
|
+
applyCustomAttributesOnSpan?: FetchCustomAttributeFunction;
|
|
25
|
+
ignoreNetworkEvents?: boolean;
|
|
26
|
+
};
|
|
27
|
+
xhrInstrumentationOptions?: {
|
|
28
|
+
applyCustomAttributesOnSpan?: XHRCustomAttributeFunction;
|
|
29
|
+
ignoreNetworkEvents?: boolean;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SamplingDecision } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import type { MetaSession } from '@grafana/faro-core';
|
|
3
|
+
/**
|
|
4
|
+
* Get sampling decision based on session configuration
|
|
5
|
+
*
|
|
6
|
+
* If the session is sampled, traces will be collected.
|
|
7
|
+
* If the session is not sampled, traces will be dropped.
|
|
8
|
+
*
|
|
9
|
+
* @param sessionMeta - Current Faro session meta
|
|
10
|
+
* @returns OTEL sampling decision
|
|
11
|
+
*/
|
|
12
|
+
export declare function getSamplingDecision(sessionMeta?: MetaSession): SamplingDecision;
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@grafana/faro-react-native-tracing",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "Faro React Native tracing implementation with OpenTelemetry.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"observability",
|
|
7
|
+
"apm",
|
|
8
|
+
"rum",
|
|
9
|
+
"logs",
|
|
10
|
+
"traces",
|
|
11
|
+
"metrics",
|
|
12
|
+
"react-native",
|
|
13
|
+
"mobile",
|
|
14
|
+
"opentelemetry"
|
|
15
|
+
],
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"author": "Grafana Labs",
|
|
18
|
+
"homepage": "https://github.com/grafana/faro-react-native-sdk",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/grafana/faro-react-native-sdk.git",
|
|
23
|
+
"directory": "packages/react-native-tracing"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/cjs/index.js",
|
|
26
|
+
"module": "./dist/esm/index.js",
|
|
27
|
+
"types": "./dist/types/index.d.ts",
|
|
28
|
+
"react-native": "./src/index.ts",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"start": "yarn watch",
|
|
37
|
+
"build": "run-s 'build:*'",
|
|
38
|
+
"build:compile": "run-p 'build:compile:*'",
|
|
39
|
+
"build:compile:cjs": "tsc --build tsconfig.cjs.json",
|
|
40
|
+
"build:compile:esm": "tsc --build tsconfig.esm.json",
|
|
41
|
+
"watch": "run-p 'watch:compile:*'",
|
|
42
|
+
"watch:compile:cjs": "yarn build:compile:cjs -w",
|
|
43
|
+
"watch:compile:esm": "yarn build:compile:esm -w",
|
|
44
|
+
"clean": "rimraf dist/ yarn-error.log",
|
|
45
|
+
"quality": "run-s 'quality:*'",
|
|
46
|
+
"quality:test": "../../node_modules/.bin/jest",
|
|
47
|
+
"quality:format": "prettier --cache --cache-location=../../.cache/prettier/reactNativeTracing --ignore-path ../../.prettierignore -w \"./**/*.{js,jsx,ts,tsx,css,scss,md,yaml,yml,json}\"",
|
|
48
|
+
"quality:lint": "run-p 'quality:lint:eslint' 'quality:lint:prettier'",
|
|
49
|
+
"quality:lint:eslint": "eslint --cache --cache-location ../../.cache/eslint/reactNativeTracing \"./**/*.{js,jsx,ts,tsx}\"",
|
|
50
|
+
"quality:lint:prettier": "prettier --cache --cache-location=../../.cache/prettier/reactNativeTracing --ignore-path ../../.prettierignore -c \"./**/*.{js,jsx,ts,tsx,css,scss,md,yaml,yml,json}\"",
|
|
51
|
+
"quality:circular-deps": "madge --circular ."
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@grafana/faro-core": "^2.2.3",
|
|
55
|
+
"@grafana/faro-react-native": "^1.0.0-alpha.1",
|
|
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/semantic-conventions": "^1.32.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"react-native": ">=0.70.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/node": "24.10.1",
|
|
71
|
+
"@types/react-native": "^0.73.0"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"gitHead": "8735c51051a17ebaabe49d5d65d729ebda3fa733"
|
|
77
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ExportResultCode } from '@opentelemetry/core';
|
|
2
|
+
import { BasicTracerProvider, ReadableSpan } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
|
|
4
|
+
import { initializeFaro } from '@grafana/faro-core';
|
|
5
|
+
import { mockConfig, MockTransport } from '@grafana/faro-test-utils';
|
|
6
|
+
|
|
7
|
+
import { FaroTraceExporter } from './faroTraceExporter';
|
|
8
|
+
|
|
9
|
+
describe('FaroTraceExporter', () => {
|
|
10
|
+
let transport: MockTransport;
|
|
11
|
+
let provider: BasicTracerProvider;
|
|
12
|
+
let faro: any;
|
|
13
|
+
let consoleErrorSpy: jest.SpiedFunction<typeof console.error>;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
// internalLogger.error forwards here; expected-error tests should not spam CI output
|
|
17
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
+
transport = new MockTransport();
|
|
19
|
+
faro = initializeFaro(
|
|
20
|
+
mockConfig({
|
|
21
|
+
transports: [transport],
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
provider = new BasicTracerProvider();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
consoleErrorSpy.mockRestore();
|
|
29
|
+
transport.items = [];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should export spans successfully', (done) => {
|
|
33
|
+
const exporter = new FaroTraceExporter({ api: faro.api });
|
|
34
|
+
|
|
35
|
+
const tracer = provider.getTracer('test');
|
|
36
|
+
const span = tracer.startSpan('test-span');
|
|
37
|
+
span.end();
|
|
38
|
+
|
|
39
|
+
const readableSpan = span as any as ReadableSpan;
|
|
40
|
+
|
|
41
|
+
exporter.export([readableSpan], (result) => {
|
|
42
|
+
expect(result.code).toBe(ExportResultCode.SUCCESS);
|
|
43
|
+
done();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should call api.pushTraces', (done) => {
|
|
48
|
+
const pushTracesSpy = jest.spyOn(faro.api, 'pushTraces');
|
|
49
|
+
const exporter = new FaroTraceExporter({ api: faro.api });
|
|
50
|
+
|
|
51
|
+
const tracer = provider.getTracer('test');
|
|
52
|
+
const span = tracer.startSpan('test-span');
|
|
53
|
+
span.end();
|
|
54
|
+
|
|
55
|
+
const readableSpan = span as any as ReadableSpan;
|
|
56
|
+
|
|
57
|
+
exporter.export([readableSpan], (result) => {
|
|
58
|
+
expect(pushTracesSpy).toHaveBeenCalled();
|
|
59
|
+
expect(result.code).toBe(ExportResultCode.SUCCESS);
|
|
60
|
+
done();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return FAILED when exporter is shutdown', (done) => {
|
|
65
|
+
const exporter = new FaroTraceExporter({ api: faro.api });
|
|
66
|
+
|
|
67
|
+
exporter.shutdown();
|
|
68
|
+
|
|
69
|
+
const tracer = provider.getTracer('test');
|
|
70
|
+
const span = tracer.startSpan('test-span');
|
|
71
|
+
span.end();
|
|
72
|
+
|
|
73
|
+
const readableSpan = span as any as ReadableSpan;
|
|
74
|
+
|
|
75
|
+
exporter.export([readableSpan], (result) => {
|
|
76
|
+
expect(result.code).toBe(ExportResultCode.FAILED);
|
|
77
|
+
done();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle export errors gracefully', (done) => {
|
|
82
|
+
jest.spyOn(faro.api, 'pushTraces').mockImplementation(() => {
|
|
83
|
+
throw new Error('Export failed');
|
|
84
|
+
});
|
|
85
|
+
const exporter = new FaroTraceExporter({ api: faro.api });
|
|
86
|
+
|
|
87
|
+
const tracer = provider.getTracer('test');
|
|
88
|
+
const span = tracer.startSpan('test-span');
|
|
89
|
+
span.end();
|
|
90
|
+
|
|
91
|
+
const readableSpan = span as any as ReadableSpan;
|
|
92
|
+
|
|
93
|
+
exporter.export([readableSpan], (result) => {
|
|
94
|
+
expect(result.code).toBe(ExportResultCode.FAILED);
|
|
95
|
+
done();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should resolve shutdown promise', async () => {
|
|
100
|
+
const exporter = new FaroTraceExporter({ api: faro.api });
|
|
101
|
+
|
|
102
|
+
await expect(exporter.shutdown()).resolves.toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should resolve forceFlush promise', async () => {
|
|
106
|
+
const exporter = new FaroTraceExporter({ api: faro.api });
|
|
107
|
+
|
|
108
|
+
await expect(exporter.forceFlush()).resolves.toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ExportResultCode } from '@opentelemetry/core';
|
|
2
|
+
import type { ExportResult } from '@opentelemetry/core';
|
|
3
|
+
import { createExportTraceServiceRequest } from '@opentelemetry/otlp-transformer/build/src/trace/internal';
|
|
4
|
+
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
5
|
+
|
|
6
|
+
import { createInternalLogger } from '@grafana/faro-core';
|
|
7
|
+
|
|
8
|
+
const internalLogger = createInternalLogger();
|
|
9
|
+
|
|
10
|
+
import type { FaroTraceExporterConfig } from '../types';
|
|
11
|
+
|
|
12
|
+
import { sendFaroEvents } from './faroTraceExporter.utils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* FaroTraceExporter for React Native
|
|
16
|
+
*
|
|
17
|
+
* Exports OpenTelemetry spans to Faro backend using pushTraces API.
|
|
18
|
+
*
|
|
19
|
+
* IMPORTANT: To avoid infinite loops:
|
|
20
|
+
* - Uses internalLogger instead of console
|
|
21
|
+
* - Does NOT log during export (except errors)
|
|
22
|
+
* - Relies on Faro's internal deduplication
|
|
23
|
+
*/
|
|
24
|
+
export class FaroTraceExporter implements SpanExporter {
|
|
25
|
+
private _isShutdown = false;
|
|
26
|
+
|
|
27
|
+
constructor(private config: FaroTraceExporterConfig) {}
|
|
28
|
+
|
|
29
|
+
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
|
|
30
|
+
if (this._isShutdown) {
|
|
31
|
+
internalLogger.error('FaroTraceExporter: Cannot export spans, exporter is shut down');
|
|
32
|
+
resultCallback({ code: ExportResultCode.FAILED });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Convert spans to OTLP format
|
|
38
|
+
const traceEvent = createExportTraceServiceRequest(spans, { useHex: true, useLongBits: false });
|
|
39
|
+
|
|
40
|
+
// Send traces to Faro
|
|
41
|
+
this.config.api.pushTraces(traceEvent);
|
|
42
|
+
|
|
43
|
+
// Send Faro events for CLIENT spans (HTTP requests, etc.)
|
|
44
|
+
// This is done WITHOUT logging to avoid infinite loops
|
|
45
|
+
sendFaroEvents(traceEvent.resourceSpans);
|
|
46
|
+
|
|
47
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Only log critical errors
|
|
50
|
+
internalLogger.error('FaroTraceExporter: Failed to export spans', error);
|
|
51
|
+
resultCallback({ code: ExportResultCode.FAILED });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async shutdown(): Promise<void> {
|
|
56
|
+
this._isShutdown = true;
|
|
57
|
+
return Promise.resolve(undefined);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async forceFlush(): Promise<void> {
|
|
61
|
+
// No-op for now - spans are sent immediately via pushTraces
|
|
62
|
+
return Promise.resolve(undefined);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { SpanContext } from '@opentelemetry/api';
|
|
2
|
+
import { ESpanKind, type IResourceSpans } from '@opentelemetry/otlp-transformer/build/src/trace/internal-types';
|
|
3
|
+
|
|
4
|
+
import { createInternalLogger, faro, unknownString } from '@grafana/faro-core';
|
|
5
|
+
import type { EventAttributes as FaroEventAttributes } from '@grafana/faro-core';
|
|
6
|
+
|
|
7
|
+
const internalLogger = createInternalLogger();
|
|
8
|
+
|
|
9
|
+
const DURATION_NS_KEY = 'duration_ns';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Send Faro events for CLIENT spans (HTTP requests, navigation, etc.)
|
|
13
|
+
*
|
|
14
|
+
* IMPORTANT: This function is called during trace export and must be careful to avoid infinite loops.
|
|
15
|
+
*
|
|
16
|
+
* Infinite loop prevention strategy:
|
|
17
|
+
* 1. Only process SPAN_KIND_CLIENT spans (HTTP requests, etc.)
|
|
18
|
+
* 2. Use faro.api.pushEvent which:
|
|
19
|
+
* - Does NOT trigger console logs if ConsoleInstrumentation is configured properly
|
|
20
|
+
* - Does NOT trigger HTTP instrumentation for collector URLs (they're ignored)
|
|
21
|
+
* 3. Use internalLogger for debugging instead of console
|
|
22
|
+
* 4. Never call console.log/warn/error in this function
|
|
23
|
+
*
|
|
24
|
+
* @param resourceSpans - OTLP resource spans from trace exporter
|
|
25
|
+
*/
|
|
26
|
+
export function sendFaroEvents(resourceSpans: IResourceSpans[] = []) {
|
|
27
|
+
try {
|
|
28
|
+
for (const resourceSpan of resourceSpans) {
|
|
29
|
+
const { scopeSpans } = resourceSpan;
|
|
30
|
+
|
|
31
|
+
for (const scopeSpan of scopeSpans) {
|
|
32
|
+
const { scope, spans = [] } = scopeSpan;
|
|
33
|
+
|
|
34
|
+
for (const span of spans) {
|
|
35
|
+
// Only process CLIENT spans (HTTP requests, external calls)
|
|
36
|
+
// This avoids processing internal spans which could cause loops
|
|
37
|
+
if (span.kind !== ESpanKind.SPAN_KIND_CLIENT) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const spanContext: Pick<SpanContext, 'traceId' | 'spanId'> = {
|
|
42
|
+
traceId: span.traceId.toString(),
|
|
43
|
+
spanId: span.spanId.toString(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const faroEventAttributes: FaroEventAttributes = {};
|
|
47
|
+
for (const attribute of span.attributes) {
|
|
48
|
+
faroEventAttributes[attribute.key] = String(Object.values(attribute.value)[0]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Add span duration in nanoseconds
|
|
52
|
+
if (!Number.isNaN(span.endTimeUnixNano) && !Number.isNaN(span.startTimeUnixNano)) {
|
|
53
|
+
faroEventAttributes[DURATION_NS_KEY] = String(
|
|
54
|
+
Number(span.endTimeUnixNano) - Number(span.startTimeUnixNano)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const index = (scope?.name ?? '').indexOf('-');
|
|
59
|
+
let eventName = unknownString;
|
|
60
|
+
|
|
61
|
+
if (scope?.name) {
|
|
62
|
+
if (index === -1) {
|
|
63
|
+
eventName = scope.name.split('/')[1] ?? scope.name;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (index > -1) {
|
|
67
|
+
eventName = scope?.name.substring(index + 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Push event to Faro
|
|
72
|
+
// This should NOT cause infinite loops because:
|
|
73
|
+
// 1. The collector URL is in ignoreUrls for HttpInstrumentation
|
|
74
|
+
// 2. ConsoleInstrumentation won't log this event
|
|
75
|
+
// 3. No console.log calls are made here
|
|
76
|
+
faro.api.pushEvent(`faro.tracing.${eventName}`, faroEventAttributes, undefined, {
|
|
77
|
+
spanContext,
|
|
78
|
+
// Convert nanoseconds to milliseconds
|
|
79
|
+
timestampOverwriteMs: Number(span.endTimeUnixNano) / 1_000_000,
|
|
80
|
+
customPayloadTransformer: (payload) => {
|
|
81
|
+
if (
|
|
82
|
+
faroEventAttributes['faro.action.user.name'] != null &&
|
|
83
|
+
faroEventAttributes['faro.action.user.parentId'] != null
|
|
84
|
+
) {
|
|
85
|
+
payload.action = {
|
|
86
|
+
name: faroEventAttributes['faro.action.user.name'],
|
|
87
|
+
parentId: faroEventAttributes['faro.action.user.parentId'],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
delete payload.attributes?.['faro.action.user.name'];
|
|
91
|
+
delete payload.attributes?.['faro.action.user.parentId'];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return payload;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// Only log critical errors using internal logger
|
|
102
|
+
// Do NOT use console.log here as it could cause infinite loops
|
|
103
|
+
internalLogger.error('sendFaroEvents: Failed to process spans', error);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { FaroTraceExporter } from './exporters/faroTraceExporter';
|
|
2
|
+
|
|
3
|
+
export { getDefaultOTELInstrumentations } from './instrumentations/getDefaultOTELInstrumentations';
|
|
4
|
+
|
|
5
|
+
export { TracingInstrumentation } from './instrumentation';
|
|
6
|
+
|
|
7
|
+
export { getSamplingDecision } from './utils/sampler';
|
|
8
|
+
|
|
9
|
+
export type { FaroTraceExporterConfig, TracingInstrumentationOptions } from './types';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
setSpanStatusOnFetchError,
|
|
13
|
+
fetchCustomAttributeFunctionWithDefaults,
|
|
14
|
+
} from './instrumentations/instrumentationUtils';
|
|
15
|
+
|
|
16
|
+
export { FaroMetaAttributesSpanProcessor } from './processors/faroMetaAttributesSpanProcessor';
|