@datadog/mobile-react-native
Version:
A client-side React Native module to interact with Datadog
505 lines (466 loc) • 16.1 kB
text/typescript
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
import type { GestureResponderEvent } from 'react-native';
import { InternalLog } from '../InternalLog';
import { SdkVerbosity } from '../SdkVerbosity';
import type { DdNativeRumType } from '../nativeModulesTypes';
import { bufferVoidNativeCall } from '../sdk/DatadogProvider/Buffer/bufferNativeCall';
import { DdSdk } from '../sdk/DdSdk';
import { GlobalState } from '../sdk/GlobalState/GlobalState';
import { validateContext } from '../utils/argsUtils';
import { getErrorContext } from '../utils/errorUtils';
import { DefaultTimeProvider } from '../utils/time-provider/DefaultTimeProvider';
import type { TimeProvider } from '../utils/time-provider/TimeProvider';
import { DdAttributes } from './DdAttributes';
import { generateActionEventMapper } from './eventMappers/actionEventMapper';
import type { ActionEventMapper } from './eventMappers/actionEventMapper';
import { generateErrorEventMapper } from './eventMappers/errorEventMapper';
import type { ErrorEventMapper } from './eventMappers/errorEventMapper';
import { generateResourceEventMapper } from './eventMappers/resourceEventMapper';
import type { ResourceEventMapper } from './eventMappers/resourceEventMapper';
import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext';
import { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier';
import { TracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/TracingIdentifier';
import {
getTracingContext,
getTracingContextForPropagators
} from './instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders';
import {
getCachedSessionId,
setCachedSessionId
} from './sessionId/sessionIdHelper';
import type {
ErrorSource,
DdRumType,
RumActionType,
ResourceKind,
FirstPartyHost,
PropagatorType
} from './types';
const generateEmptyPromise = () => new Promise<void>(resolve => resolve());
class DdRumWrapper implements DdRumType {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
private nativeRum: DdNativeRumType = require('../specs/NativeDdRum')
.default;
private lastActionData?: { type: RumActionType; name: string };
private errorEventMapper = generateErrorEventMapper(undefined);
private resourceEventMapper = generateResourceEventMapper(undefined);
private actionEventMapper = generateActionEventMapper(undefined);
private timeProvider: TimeProvider = new DefaultTimeProvider();
startView = (
key: string,
name: string,
context: object = {},
timestampMs: number = this.timeProvider.now()
): Promise<void> => {
InternalLog.log(
`Starting RUM View “${name}” #${key}`,
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() =>
this.nativeRum.startView(
key,
name,
validateContext(context),
timestampMs
)
);
};
stopView = (
key: string,
context: object = {},
timestampMs: number = this.timeProvider.now()
): Promise<void> => {
InternalLog.log(`Stopping RUM View #${key}`, SdkVerbosity.DEBUG);
return bufferVoidNativeCall(() =>
this.nativeRum.stopView(key, validateContext(context), timestampMs)
);
};
startAction = (
type: RumActionType,
name: string,
context: object = {},
timestampMs: number = this.timeProvider.now()
): Promise<void> => {
InternalLog.log(
`Starting RUM Action “${name}” (${type})`,
SdkVerbosity.DEBUG
);
this.lastActionData = { type, name };
return bufferVoidNativeCall(() =>
this.nativeRum.startAction(
type,
name,
validateContext(context),
timestampMs
)
);
};
stopAction = (
...args:
| [
type: RumActionType,
name: string,
context?: object,
timestampMs?: number
]
| [context?: object, timestampMs?: number]
): Promise<void> => {
InternalLog.log('Stopping current RUM Action', SdkVerbosity.DEBUG);
const nativeCallArgs = this.getStopActionNativeCallArgs(args);
this.lastActionData = undefined;
if (!nativeCallArgs) {
return generateEmptyPromise();
}
return this.callNativeStopAction(...nativeCallArgs);
};
setTimeProvider = (timeProvider: TimeProvider): void => {
this.timeProvider = timeProvider;
};
addAction = (
type: RumActionType,
name: string,
context: object = {},
timestampMs: number = this.timeProvider.now(),
actionContext?: GestureResponderEvent
): Promise<void> => {
const mappedEvent = this.actionEventMapper.applyEventMapper({
type,
name,
context: validateContext(context),
timestampMs,
actionContext
});
if (!mappedEvent) {
return generateEmptyPromise();
}
InternalLog.log(
`Adding RUM Action “${name}” (${type})`,
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() =>
this.nativeRum.addAction(
mappedEvent.type,
mappedEvent.name,
mappedEvent.context,
mappedEvent.timestampMs
)
);
};
startResource = (
key: string,
method: string,
url: string,
context: object = {},
timestampMs: number = this.timeProvider.now()
): Promise<void> => {
InternalLog.log(
`Starting RUM Resource #${key} ${method}: ${url}`,
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() =>
this.nativeRum.startResource(
key,
method,
url,
validateContext(context),
timestampMs
)
);
};
stopResource = (
key: string,
statusCode: number,
kind: ResourceKind,
size: number = -1,
context: object = {},
timestampMs: number = this.timeProvider.now(),
resourceContext?: XMLHttpRequest
): Promise<void> => {
const mappedEvent = this.resourceEventMapper.applyEventMapper({
key,
statusCode,
kind,
size,
context: validateContext(context),
timestampMs,
resourceContext
});
if (!mappedEvent) {
/**
* To drop the resource we call `stopResource` and pass the `_dd.drop_resource` attribute in the context.
* It will be picked up by the resource mappers we implement on the native side that will drop the resource.
* This ensures we don't have any "started" resource left in memory on the native side.
*/
return bufferVoidNativeCall(() =>
this.nativeRum.stopResource(
key,
statusCode,
kind,
size,
{
'_dd.resource.drop_resource': true
},
timestampMs
)
);
}
InternalLog.log(
`Stopping RUM Resource #${key} status:${statusCode}`,
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() =>
this.nativeRum.stopResource(
mappedEvent.key,
mappedEvent.statusCode,
mappedEvent.kind,
mappedEvent.size,
mappedEvent.context,
mappedEvent.timestampMs
)
);
};
addError = (
message: string,
source: ErrorSource,
stacktrace: string,
context: object = {},
timestampMs: number = this.timeProvider.now(),
fingerprint?: string
): Promise<void> => {
const mappedEvent = this.errorEventMapper.applyEventMapper({
message,
source,
stacktrace,
context: getErrorContext(validateContext(context)),
timestampMs,
fingerprint: fingerprint ?? ''
});
if (!mappedEvent) {
return generateEmptyPromise();
}
InternalLog.log(`Adding RUM Error “${message}”`, SdkVerbosity.DEBUG);
const updatedContext: any = mappedEvent.context;
updatedContext[DdAttributes.errorSourceType] = 'react-native';
return bufferVoidNativeCall(() =>
this.nativeRum.addError(
mappedEvent.message,
mappedEvent.source,
mappedEvent.stacktrace,
updatedContext,
mappedEvent.timestampMs,
mappedEvent.fingerprint
)
);
};
addTiming = (name: string): Promise<void> => {
InternalLog.log(
`Adding timing “${name}” to RUM View`,
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() => this.nativeRum.addTiming(name));
};
addViewLoadingTime = (overwrite: boolean): Promise<void> => {
InternalLog.log(
overwrite
? 'Adding and overwriting view loading to RUM View'
: 'Adding view loading to RUM View',
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() =>
this.nativeRum.addViewLoadingTime(overwrite)
);
};
stopSession = (): Promise<void> => {
InternalLog.log('Stopping RUM Session', SdkVerbosity.DEBUG);
return bufferVoidNativeCall(() => this.nativeRum.stopSession());
};
addFeatureFlagEvaluation = (
name: string,
value: unknown
): Promise<void> => {
InternalLog.log(
`Adding feature flag evaluation for name: ${name} with value: ${JSON.stringify(
value
)}`,
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() =>
this.nativeRum.addFeatureFlagEvaluation(name, { value })
);
};
async getCurrentSessionId(): Promise<string | undefined> {
if (!GlobalState.instance.isInitialized) {
return undefined;
}
const sessionId = await this.nativeRum.getCurrentSessionId();
if (sessionId) {
setCachedSessionId(sessionId);
}
return sessionId;
}
getTracingContext = (
url: string,
tracingSamplingRate: number,
firstPartyHosts: FirstPartyHost[]
): DatadogTracingContext => {
return getTracingContext(
url,
tracingSamplingRate,
firstPartyHosts,
getCachedSessionId()
);
};
getTracingContextForPropagators = (
propagators: PropagatorType[],
tracingSamplingRate: number
): DatadogTracingContext => {
return getTracingContextForPropagators(
propagators,
tracingSamplingRate,
getCachedSessionId()
);
};
generateTraceId(): DatadogTracingIdentifier {
return new DatadogTracingIdentifier(TracingIdentifier.createTraceId());
}
generateSpanId(): DatadogTracingIdentifier {
return new DatadogTracingIdentifier(TracingIdentifier.createSpanId());
}
registerErrorEventMapper(errorEventMapper: ErrorEventMapper) {
this.errorEventMapper = generateErrorEventMapper(errorEventMapper);
}
unregisterErrorEventMapper() {
this.errorEventMapper = generateErrorEventMapper(undefined);
}
registerResourceEventMapper(resourceEventMapper: ResourceEventMapper) {
this.resourceEventMapper = generateResourceEventMapper(
resourceEventMapper
);
}
unregisterResourceEventMapper() {
this.resourceEventMapper = generateResourceEventMapper(undefined);
}
registerActionEventMapper(actionEventMapper: ActionEventMapper) {
this.actionEventMapper = generateActionEventMapper(actionEventMapper);
}
unregisterActionEventMapper() {
this.actionEventMapper = generateActionEventMapper(undefined);
}
private callNativeStopAction = (
type: RumActionType,
name: string,
context: object,
timestampMs: number
): Promise<void> => {
const mappedEvent = this.actionEventMapper.applyEventMapper({
type,
name,
context: validateContext(context),
timestampMs
});
if (!mappedEvent) {
return bufferVoidNativeCall(() =>
this.nativeRum.stopAction(
type,
name,
{
'_dd.action.drop_action': true
},
timestampMs
)
);
}
return bufferVoidNativeCall(() =>
this.nativeRum.stopAction(
mappedEvent.type,
mappedEvent.name,
mappedEvent.context,
mappedEvent.timestampMs
)
);
};
private getStopActionNativeCallArgs = (
args:
| [
type: RumActionType,
name: string,
context?: object,
timestampMs?: number
]
| [context?: object, timestampMs?: number]
):
| [
type: RumActionType,
name: string,
context: object,
timestampMs: number
]
| null => {
if (isNewStopActionAPI(args)) {
return [
args[0],
args[1],
validateContext(args[2]),
args[3] || this.timeProvider.now()
];
}
if (isOldStopActionAPI(args)) {
if (this.lastActionData) {
DdSdk.telemetryDebug(
'DDdRum.stopAction called with the old signature'
);
const { type, name } = this.lastActionData;
return [
type,
name,
validateContext(args[0]),
args[1] || this.timeProvider.now()
];
}
InternalLog.log(
'DdRum.startAction needs to be called before DdRum.stopAction',
SdkVerbosity.WARN
);
} else {
InternalLog.log(
'DdRum.stopAction was called with wrong arguments',
SdkVerbosity.WARN
);
}
return null;
};
}
const isNewStopActionAPI = (
args:
| [
type: RumActionType,
name: string,
context?: object,
timestampMs?: number
]
| [context?: object, timestampMs?: number]
): args is [
type: RumActionType,
name: string,
context?: object,
timestampMs?: number
] => {
return typeof args[0] === 'string';
};
const isOldStopActionAPI = (
args:
| [
type: RumActionType,
name: string,
context?: object,
timestampMs?: number
]
| [context?: object, timestampMs?: number]
): args is [context?: object, timestampMs?: number] => {
return typeof args[0] === 'object' || typeof args[0] === 'undefined';
};
export const DdRum = new DdRumWrapper();