Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 48 additions & 5 deletions docs/troubleshooting_no_data.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ it is only printed when `trackResources` is enabled, and its absence is the sing
reason an app reports views but no API calls. (Both strings still carry the upstream name this
SDK was forked from; grep for them verbatim.)

If neither line appears, initialization never ran: check that the provider is actually mounted,
and that no exception is being swallowed around it.
A third line, `Datadog SDK could not start <feature>`, means one instrumentation failed to
install. The others, and the native SDK itself, still start — you lose only that feature's
events. Section 3 covers the most common cause.

If neither of the first two lines appears, initialization never ran: check that the provider is
actually mounted, and that no exception is being swallowed around it.

## 2. Views and crashes arrive, but no API calls

Expand All @@ -43,7 +47,46 @@ in the tree as you can — the provider only covers what renders below it. See
react-native-navigation (Wix) has no single React root to wrap, so it must keep the manual
call. Put it at module scope in your entry file, before any screen is registered.

## 3. Resources arrive but are not linked to backend traces
## 3. Views and API calls arrive, but no tap actions

Check whether your Babel config sets a custom `jsxImportSource`. nativewind does, and so does
any other styling library built on `react-native-css-interop`:

```javascript
// babel.config.js
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }]];
```

With that in place your app's JSX no longer compiles to `react/jsx-runtime` — it compiles to
the library's runtime, which wraps React's element factories **while the bundle is evaluated**,
long before the SDK starts. Patching `react/jsx-runtime` afterwards can no longer reach your
elements, so no `onPress` is instrumented and no action is ever recorded.

The SDK cannot require those modules itself: Metro resolves requires statically, so a
hard-coded one would break bundling for every app that does not depend on it. Pass them in:

```javascript
import * as NativeWindJsxRuntime from 'nativewind/jsx-runtime';

config.jsxRuntimes = [NativeWindJsxRuntime];
```

A runtime's factories may be exposed through accessors rather than plain properties, depending
on how the module was built and how your bundler models the import. The SDK replaces them
either way, and only gives up when the property is also non-configurable — a frozen ES module
namespace, for instance. That case is logged as:

```
Datadog SDK could not instrument a JSX runtime: its element factories are read-only.
No RUM action will be recorded for elements it renders.
```

There is no configuration that recovers from it: the factories have to be instrumented while
the app is built instead. Two things to try, in order — import the runtime with `require()`
rather than `import * as`, which skips the interop layer that may have frozen it, and if that
still fails, open an issue. Views, resources and errors are unaffected either way.

## 4. Resources arrive but are not linked to backend traces

Set `firstPartyHosts`. The SDK adds tracing headers only to requests whose host matches, so
with it unset every resource is a dead end:
Expand All @@ -56,7 +99,7 @@ Pass bare hosts, not URLs — no scheme, port or path. Also check
`resourceTracingSamplingRate`, which defaults to `20`: at that value four out of five matching
requests carry no tracing headers by design.

## 4. Nothing arrives at all
## 5. Nothing arrives at all

- **Wrong destination.** `site` accepts `'CN'` (default) and `'STAGING'`. For a private
deployment leave `site` alone and set `customEndpoints` to your own intake URLs instead;
Expand All @@ -69,7 +112,7 @@ requests carry no tracing headers by design.
- **Consent.** Nothing is collected under `TrackingConsent.NOT_GRANTED`, and events collected
under `PENDING` are discarded unless consent is later granted.

## 5. Only in development
## 6. Only in development

Two request kinds are filtered on purpose in dev builds, and only in dev builds: the Expo
`/logs` endpoint and the React Native packager's `/symbolicate`. Both are noise from the
Expand Down
63 changes: 52 additions & 11 deletions packages/core/src/DdSdkReactNative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ import { GlobalState } from './sdk/GlobalState/GlobalState';
import { UserInfoSingleton } from './sdk/UserInfoSingleton/UserInfoSingleton';
import type { UserInfo } from './sdk/UserInfoSingleton/types';
import { DdSdkConfiguration } from './types';
import {
getErrorMessage,
getErrorName,
getErrorStackTrace
} from './utils/errorUtils';
import { adaptLongTaskThreshold } from './utils/longTasksUtils';
import { version as sdkVersion } from './version';

Expand Down Expand Up @@ -359,6 +364,26 @@ export class DdSdkReactNative {
);
};

private static startFeature(name: string, start: () => void): void {
try {
start();
} catch (error) {
InternalLog.log(
`Datadog SDK could not start ${name}: ${getErrorMessage(
error
)}`,
SdkVerbosity.ERROR
);
DdSdk?.telemetryError?.(
`Failed to start ${name}: ${getErrorMessage(error)}`,
getErrorStackTrace(error),
getErrorName(error)
)?.catch(() => {
// reporting the failure must not become a second failure
});
}
}

private static enableFeatures(
configuration: AutoInstrumentationParameters
) {
Expand All @@ -379,27 +404,43 @@ export class DdSdkReactNative {
return;
}

// Each feature is isolated: one of them failing to install must cost only its own
// events. Before this, an exception here propagated out of enableFeatures, and since
// _initializeFromDatadogProvider calls it before initializeNativeSDK - on a promise
// nobody awaits - it also silently aborted the native initialization, leaving the app
// with no RUM data at all rather than with one missing event type.
if (
configuration.trackInteractions &&
!globalThis.__DD_RN_BABEL_PLUGIN_ENABLED__
) {
DdRumUserInteractionTracking.startTracking({
actionNameAttribute: configuration.actionNameAttribute,
useAccessibilityLabel: configuration.useAccessibilityLabel
});
DdSdkReactNative.startFeature('interaction tracking', () =>
DdRumUserInteractionTracking.startTracking(
{
actionNameAttribute: configuration.actionNameAttribute,
useAccessibilityLabel:
configuration.useAccessibilityLabel
},
configuration.jsxRuntimes
)
);
}

if (configuration.trackResources) {
DdRumResourceTracking.startTracking({
tracingSamplingRate: configuration.resourceTracingSamplingRate,
firstPartyHosts: formatFirstPartyHosts(
configuration.firstPartyHosts
)
});
DdSdkReactNative.startFeature('resource tracking', () =>
DdRumResourceTracking.startTracking({
tracingSamplingRate:
configuration.resourceTracingSamplingRate,
firstPartyHosts: formatFirstPartyHosts(
configuration.firstPartyHosts
)
})
);
}

if (configuration.trackErrors) {
DdRumErrorTracking.startTracking();
DdSdkReactNative.startFeature('error tracking', () =>
DdRumErrorTracking.startTracking()
);
}

if (configuration.logEventMapper) {
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/DdSdkReactNativeConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { LogEventMapper } from './logs/types';
import type { ActionEventMapper } from './rum/eventMappers/actionEventMapper';
import type { ErrorEventMapper } from './rum/eventMappers/errorEventMapper';
import type { ResourceEventMapper } from './rum/eventMappers/resourceEventMapper';
import type { JsxRuntimeModule } from './rum/instrumentation/interactionTracking/DdRumUserInteractionTracking';
import type { FirstPartyHost } from './rum/types';
import { PropagatorType } from './rum/types';

Expand Down Expand Up @@ -126,6 +127,7 @@ export const DEFAULTS = {
nativeViewTracking: false,
nativeInteractionTracking: false,
getFirstPartyHosts: () => [],
getJsxRuntimes: () => [],
getAdditionalConfiguration: () => ({}),
trackingConsent: TrackingConsent.GRANTED,
telemetrySampleRate: 20.0,
Expand Down Expand Up @@ -354,6 +356,23 @@ export class DdSdkReactNativeConfiguration {
*/
public actionNameAttribute?: string;

/**
* Additional JSX runtimes the app compiles its own JSX to, on top of React's.
*
* Set this whenever the app uses a custom `jsxImportSource` - nativewind and other
* css-interop based styling libraries do. Such a runtime wraps React's element factories
* while the bundle is evaluated, long before the SDK starts, so patching
* `react/jsx-runtime` afterwards no longer reaches the app's elements and no RUM action
* is ever recorded. The SDK cannot require these modules itself: Metro resolves requires
* statically, so a hard-coded one would break bundling for apps that do not depend on it.
*
* ```js
* import * as NativeWindJsxRuntime from 'nativewind/jsx-runtime';
* config.jsxRuntimes = [NativeWindJsxRuntime];
* ```
*/
public jsxRuntimes: JsxRuntimeModule[] = DEFAULTS.getJsxRuntimes();

public logEventMapper: LogEventMapper | null = DEFAULTS.logEventMapper;

public errorEventMapper: ErrorEventMapper | null =
Expand Down Expand Up @@ -399,6 +418,7 @@ export type AutoInstrumentationConfiguration = {
readonly actionEventMapper?: ActionEventMapper | null;
readonly useAccessibilityLabel?: boolean;
readonly actionNameAttribute?: string;
readonly jsxRuntimes?: JsxRuntimeModule[];
};

/**
Expand All @@ -416,6 +436,7 @@ export type AutoInstrumentationParameters = {
readonly actionEventMapper: ActionEventMapper | null;
readonly useAccessibilityLabel: boolean;
readonly actionNameAttribute?: string;
readonly jsxRuntimes: JsxRuntimeModule[];
};

/**
Expand Down Expand Up @@ -449,7 +470,8 @@ export const addDefaultValuesToAutoInstrumentationConfiguration = (
features.actionEventMapper === undefined
? DEFAULTS.actionEventMapper
: features.actionEventMapper,
useAccessibilityLabel: DEFAULTS.useAccessibilityLabel
useAccessibilityLabel: DEFAULTS.useAccessibilityLabel,
jsxRuntimes: features.jsxRuntimes || DEFAULTS.getJsxRuntimes()
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"env": "fake-env",
"errorEventMapper": null,
"firstPartyHosts": [],
"jsxRuntimes": [],
"logEventMapper": null,
"longTaskThresholdMs": 0,
"nativeCrashReportEnabled": false,
Expand Down Expand Up @@ -149,6 +150,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"api.com",
],
"initialResourceThreshold": 0.123,
"jsxRuntimes": [],
"logEventMapper": [Function],
"longTaskThresholdMs": 567,
"nativeCrashReportEnabled": true,
Expand Down Expand Up @@ -229,6 +231,7 @@ describe('DdSdkReactNativeConfiguration', () => {
"env": "",
"errorEventMapper": null,
"firstPartyHosts": [],
"jsxRuntimes": [],
"logEventMapper": null,
"longTaskThresholdMs": false,
"nativeCrashReportEnabled": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,100 @@ describe('startTracking memoization', () => {
});
});

describe('startTracking with injected jsx runtimes', () => {
it('M wrap onPress W the app compiles JSX to another runtime', async () => {
const jsx = jest.fn((_type: any, props: any) => props);
const jsxs = jest.fn((_type: any, props: any) => props);
const runtime: Record<string, unknown> = { jsx, jsxs };

DdRumUserInteractionTracking.startTracking({}, [runtime]);

// both factories matter: a single-child element compiles to jsx, several to jsxs
expect(runtime.jsx).not.toBe(jsx);
expect(runtime.jsxs).not.toBe(jsxs);

for (const key of ['jsx', 'jsxs']) {
const onPress = jest.fn();
const props: Record<string, any> = { onPress };
(runtime[key] as any)('View', props);

expect(props.onPress).not.toBe(onPress);
expect(props.__DATADOG_INTERNAL_ORIGINAL_ON_PRESS__).toBe(onPress);
props.onPress();
expect(onPress).toHaveBeenCalledTimes(1);
}
});

it('M keep tracking W a runtime factory is read-only', async () => {
// nativewind's react-native-css-interop exposes its factories as getter-only
// properties; assigning to one throws, and that used to abort the whole SDK startup
const runtime = {};
Object.defineProperty(runtime, 'jsx', {
get: () => () => null,
enumerable: true,
configurable: true
});

expect(() =>
DdRumUserInteractionTracking.startTracking({}, [runtime])
).not.toThrow();
expect(DdRumUserInteractionTracking['isTracking']).toBe(true);
});

it('M patch through an accessor W the property is configurable', async () => {
// a namespace object built by an interop helper can expose accessors rather than
// plain properties; assignment does nothing there, redefining still works
const runtime = {};
const original = jest.fn();
Object.defineProperty(runtime, 'jsx', {
get: () => original,
enumerable: true,
configurable: true
});

DdRumUserInteractionTracking.startTracking({}, [runtime]);

expect((runtime as any).jsx).not.toBe(original);

const onPress = jest.fn();
const props: Record<string, any> = { onPress };
(runtime as any).jsx('View', props);
expect(props.onPress).not.toBe(onPress);
});

it('M say what it costs W a runtime cannot be patched at all', async () => {
const runtime = {};
Object.defineProperty(runtime, 'jsx', {
get: () => jest.fn(),
enumerable: true,
configurable: false
});

expect(() =>
DdRumUserInteractionTracking.startTracking({}, [runtime])
).not.toThrow();
expect(DdRumUserInteractionTracking['isTracking']).toBe(true);
// the integrator must learn the consequence, not just that a property was skipped
expect(DdSdk.telemetryError).toHaveBeenCalledWith(
expect.stringContaining('No RUM action will be recorded'),
'',
'JsxRuntimeNotPatchable'
);
});

it('M restore the injected runtime W stopTracking is called', async () => {
const jsx = jest.fn();
const jsxs = jest.fn();
const runtime: Record<string, unknown> = { jsx, jsxs };

DdRumUserInteractionTracking.startTracking({}, [runtime]);
DdRumUserInteractionTracking.stopTracking();

expect(runtime.jsx).toBe(jsx);
expect(runtime.jsxs).toBe(jsxs);
});
});

describe('startTracking', () => {
/**
* WARNING: Because of caching in the require, the following 2 tests need
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { TrackingConsent } from './TrackingConsent';
import { DdLogs } from './logs/DdLogs';
import { DdRum } from './rum/DdRum';
import { DdBabelInteractionTracking } from './rum/instrumentation/interactionTracking/DdBabelInteractionTracking';
import type { JsxRuntimeModule } from './rum/instrumentation/interactionTracking/DdRumUserInteractionTracking';
import { __ddExtractText } from './rum/instrumentation/interactionTracking/ddBabelUtils';
import { DatadogTracingContext } from './rum/instrumentation/resourceTracking/distributedTracing/DatadogTracingContext';
import { DatadogTracingIdentifier } from './rum/instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier';
Expand Down Expand Up @@ -85,5 +86,6 @@ export type {
Timestamp,
FirstPartyHost,
AutoInstrumentationConfiguration,
PartialInitializationConfiguration
PartialInitializationConfiguration,
JsxRuntimeModule
};
Loading
Loading