analytics-next icon indicating copy to clipboard operation
analytics-next copied to clipboard

How to write an enrichment plugin that fires after a remote integration?

Open kwalker3690 opened this issue 1 year ago • 4 comments

I'm writing an 'enrichment' plugin that should add a property to any event that comes through track. However, the value of this property relies on a value added to events via an integration (the Amplitude Actions integration to be specific). Here is the code I'm using to register my plugin:

const testPlugin: Plugin = {
  name: "Session Replay Events",
  type: "enrichment",
  version: "1.0.0",

  isLoaded: () => true,
  load: loadMethod,
  track: trackMethod,
};

SegmentAnalytics.load({
  writeKey: <api key>,
  plugins: [testPlugin],
});

When debugging this, I've inspected the code and observed that testPlugin (red arrow in below screenshot) is always added before the Actions enrichment plugin (blue arrow in below screenshot)

Screenshot 2024-07-22 at 2 20 39 PM

I believe the ordering is occurring due to the ordering of plugins vs remotePlugins in this code snippet.

I'd like to have my plugin be registered in load() so that we can enrich all events, including those emitted immediately on page load (as opposed to registering my plugin after load(), which means it will not capture those initial events).

Because of the ordering, when I console log via my track method, the ctx.event.integrations object is empty, as the Amplitude Actions integration has not yet been processed: image

Is there any way I can have my testPlugin be registered in load(), but have its track calls execute after those from remotely loaded plugins?

kwalker3690 avatar Jul 22 '24 18:07 kwalker3690

@kwalker3690 thanks for the level of detail. That's odd -- that looks like a bug, as passing it into plugins and calling register should behave identically. Regardless, .register does not get less priority than passing it directly into the plugins array. Both should capture page calls and other buffered events -- no events should be dispatched (whether buffered events or not) until enrichment plugin's .load method has resolved.

I just tested:

export const analytics = new AnalyticsBrowser();
analytics.register({
  type: "enrichment",
  name: "My Enrichment Plugin",
  version: "1.0.0",
  load: () => {
    console.log("My Enrichment Plugin loaded");
    return Promise.resolve();
  },
  page: (ctx) => {
    console.log("page");
    console.log(ctx.event.integrations);
    return ctx;
  },
  track: (ctx) => {
    console.log(ctx.event.integrations);
    return ctx;
  },
  isLoaded: () => true,
});


analytics.page();

analytics.load({ writeKey: "9lSrez3BlfLAJ7NOChrqWtILiATiycoc" });


image

silesky avatar Jul 23 '24 03:07 silesky

@silesky thanks so much for your prompt and thorough response! You're right, rewriting my test plugin to register in advance of load solves the issue of the ordering of the plugins. There is another complication here I don't think I spelled out well in my initial post, which is that in our use case, we have an async operation in load that takes a bit of time (we are making an API request). This means that we need to wait for our async load to complete before any events should be processed, otherwise we don't have the right data for manipulating the event properties (in addition to the Actions Amplitude data received through the integration). I've made this gist to better illustrate our issue: https://gist.github.com/kwalker3690/b59e7c00be8285be98b314a58b43bd30

Here's the console output for calling register before load: image

Oberve that the plugin load doesn't end until after the Immediate Event has fired. In comparison, when the plugin is registered as a part of the main SDK load method, the plugin load method is awaited correctly, but the integrations data is not populated: image

Please let me know if I'm missing a nuance here! I'd be very happy if this isn't a bug I'm bothering you all with and instead is something I can solve with a tweak in ordering. Thanks!

kwalker3690 avatar Jul 23 '24 13:07 kwalker3690

@kwalker3690 That is odd behavior -- while load is always invoked before events are dispatched, at least for a given plugin, you would expect .load to always resolve before any of its own methods are called. Otherwise, there's no point in .load being an async function at all. Will make a note to look into this.

In the meantime, this pattern should still work for your use case:

  class MyPlugin implements Plugin {
    readonly type = "enrichment";
    readonly name = "Enrichment Plugin";
    readonly version = "0.0.0";
    private data!: Promise<string> 
    
    async load() {
      
      // fetch async data
      this.data = new Promise((resolve) => setTimeout(() => resolve('data'), 1000));
      
      return Promise.resolve();
    }
    
    private async enrich(ctx: Context) {
      ctx.event.context!.data = await this.data;
      console.log('enriched context:', ctx.event.integrations)
      return ctx
    }

    isLoaded() {
      return true;
    }
    
    track = this.enrich;
    page = this.enrich;
    alias = this.enrich;
    group = this.enrich;
    identify = this.enrich;
    screen = this.enrich;
  }
  
 analytics.register(new MyPlugin())

silesky avatar Jul 23 '24 20:07 silesky

Amazing - I need to do a little more validation but at first glance I think this approach should do the trick! Thanks for working through this with me

kwalker3690 avatar Jul 23 '24 20:07 kwalker3690