ofetch icon indicating copy to clipboard operation
ofetch copied to clipboard

Attempting to use Datadog RUM with Nuxt in plugin, but fetch is already aliased before RUM can overwrite it.

Open bradlis7 opened this issue 2 years ago • 4 comments

Environment

"@datadog/browser-rum": "^5.0.0",
"nuxt": "^3.7.4",

Reproduction

Not sure if I can set up an example as this might be a paid product only.

Describe the bug

I have the integration set up as a Nuxt plugin, and datadog attempts to override window.fetch, but at that point, fetch has already been stored as the original function in ofetch, so none of the callbacks get intercepted with appropriate headers. I don't see a manual way to override fetch after the fact. Is there a way to override this fetch in Nuxt?

Additional context

No response

Logs

No response

bradlis7 avatar Dec 06 '23 04:12 bradlis7

I also experience this issue.

adam-gipril avatar Jan 06 '24 06:01 adam-gipril

I've found a workaround for my use case: a small Nuxt app generated to a static SPA with no server-side component.

"@datadog/browser-rum": "^5.4.0",
"nuxt": "^3.9.0",
"ofetch": "^1.3.3",
nuxt.config.ts
export default defineNuxtConfig({
  ssr: false,
  nitro: {
   static: true,
    prerender: {
      autoSubfolderIndex: false,
      routes: ['/index.html', '/success.html'],
    },
  },
})

plugins/01.datadogRum.ts

import { datadogRum } from '@datadog/browser-rum'

export default defineNuxtPlugin(nuxtApp => {
  datadogRum.init({
    clientToken: '<CLIENT_TOKEN>',
    applicationId: '<APPLICATION_ID>',
    site: 'datadoghq.com',
  })

  return {
    provide: {
      datadogRum,
    },
  }
})

Invoking datadogRum.init eventually causes createFetchObservable which causes instrumentMethod.

[!IMPORTANT] To capture RUM resource data on fetch calls, the RUM instrumentationWrapper (packaged by the instrumentMethod function as window.fetch) must be invoked. ofetch invokes native fetch directly through its stored reference, bypassing Datadog's instrumentationWrapper.

I wonder if a Datadog Nuxt module could solve this issue as a way to run datadogRum.init before $fetch gets created? 🤔

plugins/02.instrumentedFetch.ts (the workaround)

import { createFetch, type FetchRequest, type FetchOptions } from 'ofetch'

export default defineNuxtPlugin(nuxtApp => {
  /**
   * Create a new ofetch, passing in the version of fetch that is now the RUM instrumentationWrapper
   * put in place by the invocation of datadogRum.init in the 01.datadogRum plugin.
   */
  function instrumentedFetch<
    T = unknown,
    R extends FetchRequest = FetchRequest,
    O extends FetchOptions = FetchOptions,
  >(request: R, opts: O) {
    return <Promise<T>>createFetch({ fetch })(request, opts) // Optionally configure fetch defaults here
  }

  return {
    provide: {
      instrumentedFetch,
    },
  }
})

You can now invoke useNuxtApp().$instrumentedFetch() with mostly the same function signature as $fetch().

[!WARNING] As I mentioned, my application doesn't have a server component. I wonder if useAsyncData could be used with the instrumented fetch? I could be unaware of other server-side functionality this approach lacks from the built-in fetch composables.

adam-gipril avatar Jan 07 '24 08:01 adam-gipril

That said, I don't think this is actually an issue with ofetch.

adam-gipril avatar Jan 07 '24 08:01 adam-gipril

The workaround above did break the functionality of the registerEndpoint helper from @nuxt/test-utils v3.9.0.

For some reason in my runtime, only globalThis.$fetch gets replaced with the vitest-environment-nuxt version assigned here (or maybe globalThis.fetch gets overridden by something else after vitest-environment-nuxt while globalThis.$fetch doesn't?).

Whatever the cause, adding this line to a file included in our Vitest setupFiles got our instrumentedFetch helper to work with registerEndpoint.

__tests__/setup.ts

import type { $Fetch } from 'ofetch'

/* Causes instrumentedFetch to use the fetch that works with registerEndpoint */
globalThis.fetch = (globalThis.$fetch as $Fetch).native
vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    setupFiles: ['__tests__/setup.ts'],
  },
})

[!TIP]

This plugin that runs only in our test environment was a convenient way to mock the RUM SDK.

__tests__/__plugins__/mockDatadogRum.ts
import { vi } from 'vitest'
import { defineNuxtPlugin } from 'nuxt/app'

export default defineNuxtPlugin(() => {
  vi.mock('@datadog/browser-rum', () => ({
    datadogRum: {
      init: vi.fn(),
      addAction: vi.fn(),
      addError: vi.fn(),
    },
  }))
})
vitest.config.ts
import { defineVitestConfig } from '@nuxt/test-utils/config'

export default defineVitestConfig({
  test: {
    environment: 'nuxt',
    environmentOptions: {
      nuxt: {
        overrides: {
          plugins: ['__tests__/__plugins__/mockDatadogRum.ts'],
        },
      },
    },
    setupFiles: ['__tests__/setup.ts'],
  },
})

adam-gipril avatar Jan 07 '24 19:01 adam-gipril