unjs / ofetch

😱 A better fetch API. Works on node, browser and workers.
MIT License
4.02k stars 127 forks source link

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

Open bradlis7 opened 10 months ago

bradlis7 commented 10 months ago

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

adam-gipril commented 9 months ago

I also experience this issue.

adam-gipril commented 9 months ago

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 ```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 commented 9 months ago

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

adam-gipril commented 9 months ago

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 ```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 ```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 ```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'], }, }) ```