thomaspoignant / go-feature-flag

GO Feature Flag is a simple, complete and lightweight self-hosted feature flag solution 100% Open Source. 🎛️
https://gofeatureflag.org/
MIT License
1.18k stars 117 forks source link

(feature) Integration with Datadog RUM #1877

Open thomaspoignant opened 1 month ago

thomaspoignant commented 1 month ago

Requirements

Datadog is now supporting RUM integration for feature flags (https://docs.datadoghq.com/real_user_monitoring/guide/setup-feature-flag-data-collection/?tab=browser).

It will be great to be able to integrate GO Feature Flag with Datadog. This will probably be through an OpenFeature Hook.

mbezhanov commented 2 weeks ago

Hello there!

To better understand, are you looking into adding this as an extra initialization option in the JavaScript / TypeScript client provider, or did you have something else in mind?

e.g. something like:

const goFeatureFlagWebProvider = new GoFeatureFlagWebProvider({
  endpoint: endpoint,
  listener: (key, value) => {
     datadogRum.addFeatureFlagEvaluation(key, value);
  },
}, logger);
thomaspoignant commented 2 weeks ago

Hello @mbezhanov, Yes, something like that could work. I haven't looked at it in detail yet, but this sounds like an elegant solution.

mbezhanov commented 2 weeks ago

I'll give it a shot.

mbezhanov commented 2 weeks ago

/assign-me

github-actions[bot] commented 2 weeks ago

👋 Hey @mbezhanov, thanks for your interest in this issue! 🎉

⚠ Note that this issue will become unassigned if it isn't closed within 10 days.

🔧 A maintainer can also add the 📌 Pinned label to prevent it from being unassigned automatically.

mbezhanov commented 2 weeks ago

Just want to make sure I'm on the right track here. Consider the following potential changes made to the go-feature-flag-web-provider.ts file in the open-telemetry/js-sdk-contrib repo: https://github.com/mbezhanov/js-sdk-contrib/commit/9308bff9d37ef187f0d3496b6d9e01bf0e9542cb

This allows us to initialize the provider in the following way:

const goFeatureFlagWebProvider = new GoFeatureFlagWebProvider({
    endpoint,
    listener: (key, value) => {
      datadogRum.addFeatureFlagEvaluation(key, value)
    }
  }, logger);

Then, if we assume we have the following flags defined in the relay proxy:

some-boolean-flag:
  variations:
    A: true
    B: false
  targeting:
    - query: ff eq true
      percentage:
        A: 50
        B: 50
  defaultRule:
    variation: A
some-numeric-flag:
  variations:
    A: 3
    B: 5
    C: 7
  targeting:
    - query: ff eq true
      percentage:
        A: 30
        B: 30
        C: 40
  defaultRule:
    variation: A
some-string-flag:
  variations:
    A: "foo"
    B: "bar"
    C: "baz"
  targeting:
    - query: ff eq true
      percentage:
        A: 30
        B: 30
        C: 40
  defaultRule:
    variation: A

And we perform similar evaluations in our code:

if (client.getBooleanValue('some-boolean-flag', false)) {
  // ...
}

if (client.getStringValue('some-string-flag', 'qux')) {
  // ...
}

if (client.getNumberValue('some-numeric-flag', 9))) {
  // ...
}

The following information gets displayed in Datadog:

image

Is that sufficient?

thomaspoignant commented 1 week ago

That looks great regarding results, but I was wondering if we should not leverage hooks for that.

Having a built-in provider hook can probably make it work. But in the same way I don't want to tight the provider to a Datadog dependency. So I am not 100% sure of the best solution here 🤔

mbezhanov commented 1 week ago

To clarify this a little bit, there is no direct dependency on @datadog/browser-rum in @openfeature/go-feature-flag-web-provider itself.

So in a JS app, the entire code goes somewhat like this:

import {datadogRum} from "@datadog/browser-rum";
import {GoFeatureFlagWebProvider} from "@openfeature/go-feature-flag-web-provider";
import {OpenFeature} from "@openfeature/web-sdk";

// init the RUM browser SDK
datadogRum.init({
  // . . .
  enableExperimentalFeatures: ['feature_flags'],
});

// . . .

// init  the GO Feature Flag web provider
const goFeatureFlagWebProvider = new GoFeatureFlagWebProvider({
  // . . .
  listener: (key, value) => {
    datadogRum.addFeatureFlagEvaluation(key, value)
  }
}, logger);

// . . .

// init OpenFeature client
await OpenFeature.setContext(ctx);
OpenFeature.setProvider(goFeatureFlagWebProvider);
const client = OpenFeature.getClient();

// . . .

// evaluate some feature flag
if (client.getBooleanValue('some-boolean-flag', false)) {
  // . . .
}

This mirrors some of the other listeners listed on the Datadog integrations page in the documentation.

listener can effectively be anything:

const goFeatureFlagWebProvider = new GoFeatureFlagWebProvider({
  endpoint: endpoint,
  listener: (key, value) => {
    console.log(key, value)
  }
}, logger);

The way I understand it, with hooks we would define a new Hook class somewhere in our application:

import {EvaluationDetails, FlagValue, Hook, HookContext} from '@openfeature/web-sdk';
import {RumPublicApi} from "@datadog/browser-rum-core";

export class DatadogHook implements Hook {
  private _datadogRum;

  constructor(datadogRum: RumPublicApi ) {
    this._datadogRum = datadogRum;
  }

  after(hookContext: HookContext, evaluationDetails: EvaluationDetails<FlagValue>) {
    this._datadogRum.addFeatureFlagEvaluation(evaluationDetails.flagKey, evaluationDetails.value)
  }
}

And then we'll instruct the OpenFeature SDK to use that hook as follows:

import {datadogRum} from "@datadog/browser-rum";
import {DatadogHook} from "./datadog-hook";
import {GoFeatureFlagWebProvider} from "@openfeature/go-feature-flag-web-provider";
import {OpenFeature} from "@openfeature/web-sdk";

// init the RUM browser SDK
datadogRum.init({
  // . . .
  enableExperimentalFeatures: ['feature_flags'],
});

// . . .

// init  the GO Feature Flag web provider
const goFeatureFlagWebProvider = new GoFeatureFlagWebProvider({
  // . . .
}, logger);

// . . .

// init OpenFeature client and add hook
await OpenFeature.setContext(ctx);
OpenFeature.setProvider(goFeatureFlagWebProvider);
const client = OpenFeature.getClient();
client.addHooks(new DatadogHook(datadogRum))

// . . .

// evaluate some feature flag
if (client.getBooleanValue('some-boolean-flag', false)) {
  // . . .
}

In that case, it doesn't seem that any changes are necessary in the @openfeature/go-feature-flag-web-provider package itself.

Am I understanding this correctly, or did you have something else in mind?

mbezhanov commented 2 days ago

I'll unassign myself for now, but I'll be ready to take over again whenever you need me :slightly_smiling_face: