Skip to content

[FFL-3355] Add JavaScript tracking hooks for DatadogCoreProvider - #1467

Closed
greghuels wants to merge 8 commits into
developfrom
greg.huels/FFL-3355/building-blocks
Closed

greghuels wants to merge 8 commits into
developfrom
greg.huels/FFL-3355/building-blocks

Conversation

@greghuels

@greghuels greghuels commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds exposure, flag evaluation, and RUM tracking hooks for DatadogCoreProvider, written in TypeScript (no native bridge). Import them from @datadog/mobile-react-native-openfeature/rules-based:

const tracking = composeDatadogTrackingHooks(
    createDatadogExposureLoggingHook({ clientToken, site, service, applicationId }),
    createDatadogEvaluationLoggingHook({ clientToken, site, service, applicationId }),
    createDatadogRumTrackingHook()
);
await tracking.initialize();
client.addHooks(...tracking.hooks);
Hook What it does
createDatadogExposureLoggingHook Sends deduplicated exposures to /api/v2/exposures
createDatadogEvaluationLoggingHook Aggregates evaluations and sends them to /api/v2/flagevaluation
createDatadogRumTrackingHook Calls DdRum.addFeatureFlagEvaluation(flagKey, variant)
composeDatadogTrackingHooks Combines hooks and their initialize / shutdown

The hooks are ported from the browser SDK and send requests in the same format.

It also changes core: RUM resource tracking now ignores these uploads, so they don't show up as RUM resources.

Motivation

FFL-3355. This completes the building-blocks model for DatadogCoreProvider without waiting for native SDK changes (the plan in #1456). Each hook can be enabled on its own.

Additional Notes

  • Use these hooks only with DatadogCoreProvider. The other providers already track natively, so adding the hooks would count every evaluation twice.
  • Configure them separately. The hooks take their own options and don't read the DdSdkReactNative configuration.
  • Upgrade core in the same release. With an older @datadog/mobile-react-native, the uploads appear as RUM resources.
  • Known gaps compared with the browser SDK: failed requests aren't retried, exposure deduplication resets when the app restarts, and events have no RUM view URL.
  • Tests: unit tests for the transport and each hook, end-to-end tests with the real OpenFeature client, and tests for the core filter. The openfeature and core suites pass.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)
  • If this PR is auto-generated, please make sure also to manually update the code related to the change

greghuels and others added 3 commits October 2, 2026 09:46
…355)

The OpenFeature package's JavaScript tracking hooks send exposures and
flag evaluations with fetch. RUM resource tracking proxies fetch and XHR,
so those uploads would be reported as RUM resources. Drop resources for
/api/v2/exposures and /api/v2/flagevaluation on the browser intake host,
and for the same paths forwarded through a ddforward proxy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…der (FFL-3355)

Port the browser SDK's building-block tracking hooks to TypeScript and
export them from the /rules-based entry:

- createDatadogExposureLoggingHook sends exposures to the intake, with
  in-memory deduplication scoped to the core configuration identity.
- createDatadogEvaluationLoggingHook aggregates evaluations with
  flagging-core's FlagEvaluationAggregator and sends them to the
  flagevaluation intake.
- createDatadogRumTrackingHook adds evaluated variants to RUM with
  DdRum.addFeatureFlagEvaluation, loading the native SDK lazily so the
  /rules-based entry does not require it.
- composeDatadogTrackingHooks combines hooks and lifecycle methods.

The hooks take explicit options and do not use DdFlags or the native
trackEvaluation bridge. Events are batched as newline-delimited JSON and
sent when a batch fills, after a timeout, when the app leaves the
foreground, and on shutdown.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…FL-3355)

The browser and native Feature Flags SDKs send dd-evp-origin-version with
exposure and flag evaluation uploads, and the intake workers record it as
the event's source version. Send the package version the same way.

The version comes from src/version.ts, generated by genversion from the
package's package.json in the root prepare, test, and lint scripts, as
core's version module is. Publishing runs prepare, so the published
package always reports its own version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@greghuels
greghuels marked this pull request as ready for review October 2, 2026 16:29
@greghuels
greghuels requested review from a team as code owners October 2, 2026 16:29
@greghuels
greghuels requested review from danyal002 and sameerank and a balanced review from Copilot and removed request for a team October 2, 2026 16:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The resource filter can incorrectly suppress unrelated application requests from RUM.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds JavaScript tracking hooks for DatadogCoreProvider and prevents their uploads from appearing as RUM resources.

Changes:

  • Adds exposure, evaluation, and RUM tracking hooks with lifecycle composition.
  • Adds batching, intake configuration, deduplication, and tests.
  • Filters tracking uploads from RUM resource instrumentation.
File Description
package.json Generates the OpenFeature package version.
packages/​react-native-openfeature/​README.md Documents tracking hooks and usage.
packages/​react-native-openfeature/​release-content.txt Updates packaged-file inventory.
packages/​react-native-openfeature/​src/​version.ts Adds generated package version.
packages/​react-native-openfeature/​src/​rules-based.ts Exports tracking APIs.
packages/​react-native-openfeature/​src/​tracking/​configuration.ts Defines tracking configuration.
packages/​react-native-openfeature/​src/​tracking/​exposures.ts Implements exposure tracking.
packages/​react-native-openfeature/​src/​tracking/​flagEvaluations.ts Implements aggregated evaluation tracking.
packages/​react-native-openfeature/​src/​tracking/​index.ts Exports public tracking APIs.
packages/​react-native-openfeature/​src/​tracking/​rumIntegration.ts Implements RUM tracking.
packages/​react-native-openfeature/​src/​tracking/​tracking.ts Manages hook lifecycles and composition.
packages/​react-native-openfeature/​src/​tracking/​transport.ts Implements intake batching and transport.
packages/​react-native-openfeature/​src/​__tests__/​trackingHooks.test.ts Tests hook and transport behavior.
packages/​react-native-openfeature/​src/​__tests__/​trackingHooks.integration.test.ts Tests real provider integration.
packages/​core/​src/​rum/​instrumentation/​resourceTracking/​requestProxy/​FetchProxy/​FetchProxy.ts Adds filtering to fetch instrumentation.
packages/​core/​src/​rum/​instrumentation/​resourceTracking/​requestProxy/​XHRProxy/​XHRProxy.ts Adds filtering to XHR instrumentation.
packages/​core/​src/​rum/​instrumentation/​resourceTracking/​requestProxy/​common/​flaggingIntakeResourceFilter.ts Identifies tracking intake requests.
packages/​core/​src/​rum/​instrumentation/​resourceTracking/​requestProxy/​XHRProxy/​DatadogRumResource/​__tests__/​flaggingIntakeResourceFilter.test.ts Tests intake filtering.
packages/​core/​release-content.txt Updates core packaged-file inventory.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…(FFL-3355)

The flagging intake filter dropped every RUM resource for
/api/v2/exposures and /api/v2/flagevaluation, which could hide
application requests to the same paths. The OpenFeature transport always
sends ddsource=react-native as the first parameter, so require that
marker in both the direct and ddforward patterns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 16:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Non-finite intervals can trigger rapid timers, and the OpenFeature release manifest omits generated version artifacts.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Package manifest omits generated version artifacts

packages/​react-native-openfeature/​release-content.txt:20

This generated package manifest omits every artifact produced by the newly added src/version.ts (lib/commonjs/version.js{,.map}, lib/module/version.js{,.map}, the declaration files, and package/src/version.ts). Since transport.ts imports that module, these files will be present in the packed package, so the checked-in release-content snapshot is incomplete; please regenerate it from the package tarball.

Comment thread packages/react-native-openfeature/src/tracking/configuration.ts
…tervals (FFL-3355)

NaN passed the number type and survived the clamp, so the evaluation
aggregator's timers fired almost immediately. Use the documented 10s
default for NaN and Infinity before clamping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 16:46
…FFL-3355)

The generated src/version.ts adds compiled, declaration, and source files
to the package. Regenerate release-content.txt from a fresh package build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@greghuels

Copy link
Copy Markdown
Contributor Author

Package manifest omits generated version artifacts (packages/react-native-openfeature/release-content.txt)

Fixed in chore(openfeature): add version module artifacts to release content (FFL-3355) (a42f0c4). I regenerated the manifest from a fresh package build. It now lists the version CommonJS and module builds, the declaration files, and src/version.ts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The OpenFeature release-content snapshot omits generated version artifacts and will fail package-content validation.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Comment thread packages/react-native-openfeature/release-content.txt
Copilot AI balanced review requested due to automatic review settings October 2, 2026 16:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Byte-unbounded batching can create oversized requests and drop multiple tracking events.

Review effort: Balanced
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Bound batches by UTF-8 payload size to prevent oversized request drops

packages/​react-native-openfeature/​src/​tracking/​transport.ts:99

The batch is bounded only by event count, so valid evaluations with large context values can produce an arbitrarily large request; one oversized payload can then fail and drop all 50 events. The browser transport being matched also flushes before 16 KiB and discards a single message above 256 KiB. Please track UTF-8 payload bytes and flush/drop at equivalent limits rather than relying only on maxEvents.

Batches were bounded only by event count, so events with large evaluation
contexts could build an oversized request, and one failed request dropped
up to 50 events. Match browser-core's batch limits:

- Send the batch before an event would bring it to 16 KiB or more, and
  once it reaches 16 KiB.
- Drop single events of 256 KiB or more.
- Count UTF-8 bytes without TextEncoder, which older Hermes and JSC lack.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 17:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The new telemetry transport, lifecycle handling, and cross-package RUM filtering warrant final human validation despite comprehensive tests.

Review effort: Balanced
Findings: None

@sameerank sameerank left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the limitation discussed in #1456: the current native call (FlagsClient.track() → NativeDdFlags.trackEvaluation() → native iOS/Android tracking) combines tracking behaviors, controlled by native configuration. Calling it from multiple independent hooks could double-track. So we either need to expose a single combined native tracking hook or extend the native API to support independently selectable tracking operations.

Overall, not loving introducing a second upload pipeline as this duplicates existing functionality, but I think I'm okay with this JS implementation as a temporary measure until we extend the native API to separate tracking operations. It's hard to say right now if a single combined native tracking hook might become problematic in the future, so I'm less inclined in that direction

Comment on lines +117 to +118
globalThis
.fetch(url, {

@sameerank sameerank Oct 2, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JavaScript uploads bypass tracking consent, which the native SDK normally enforces, and the hook options provide no separate consent API. An application using PENDING or NOT_GRANTED can still upload targeting IDs and attributes. shutdown() also flushes pending data, so it cannot safely serve as consent revocation.

Is there a way for these hooks to reuse native consent handling or provide an explicit equivalent? Pending events should also be discarded when consent is revoked, rather than flushed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to watch and observe the track consent via the RN bridge. I'll update.

Comment thread packages/react-native-openfeature/src/tracking/flagEvaluations.ts Outdated
…(FFL-3355)

OpenFeature skips the `after` stage for failed evaluations, such as
FLAG_NOT_FOUND and TYPE_MISMATCH, so the evaluation logging hook never
recorded them. Record evaluations in `finally`, which runs for both
successful and failed evaluations, as the Datadog server SDKs do. Pass
the error message, or the error code when there is no message, to the
aggregator. createTrackingHookController now forwards `finally` as well
as `after`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 2, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The tracking lifecycle, transport behavior, resource filtering, public exports, documentation, and edge cases are consistently implemented and covered by tests.

Review effort: Balanced
Findings: None

@greghuels

greghuels commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

I understand the limitation discussed in #1456: the current native call (FlagsClient.track() → NativeDdFlags.trackEvaluation() → native iOS/Android tracking) combines tracking behaviors, controlled by native configuration. Calling it from multiple independent hooks could double-track. So we either need to expose a single combined native tracking hook or extend the native API to support independently selectable tracking operations.

Overall, not loving introducing a second upload pipeline as this duplicates existing functionality, but I think I'm okay with this JS implementation as a temporary measure until we extend the native API to separate tracking operations. It's hard to say right now if a single combined native tracking hook might become problematic in the future, so I'm less inclined in that direction

@sameerank Yeah, I'm also less inclined to have a single hook. I think feature parity with browser is important from a dev experience perspective. We can treat this as a stop gap until granular track functionality is implemented in ios and android.

@greghuels

Copy link
Copy Markdown
Contributor Author

Closing this out. Creating a way to observe consent through the react native bridge is getting a little messy. We'll just implement the granular hooks in the native SDKs first

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants