Skip to content

Commit f6c25a5

Browse files
authored
feat: build-time debug flag with script-lifecycle tracing (#760)
1 parent 6f74a6c commit f6c25a5

5 files changed

Lines changed: 76 additions & 0 deletions

File tree

packages/script/src/module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,14 @@ export default defineNuxtModule<ModuleOptions>({
563563
: undefined,
564564
} as any
565565

566+
// Build-time constant: `__NUXT_SCRIPTS_DEBUG__` is replaced inline by the
567+
// bundler, so debug branches DCE away in production when `debug: false`.
568+
const debugConst = JSON.stringify(!!config.debug)
569+
nuxt.options.vite ||= {}
570+
nuxt.options.vite.define = { ...nuxt.options.vite.define, __NUXT_SCRIPTS_DEBUG__: debugConst }
571+
nuxt.options.nitro ||= {}
572+
nuxt.options.nitro.replace = { ...nuxt.options.nitro.replace, __NUXT_SCRIPTS_DEBUG__: debugConst }
573+
566574
// Register proxy handler unconditionally. The handler rejects unknown domains
567575
// at runtime, so it's safe to register even when no scripts use proxy.
568576
const scriptsBase = config.prefix || '/_scripts'

packages/script/src/runtime/composables/useScript.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { injectHead, onNuxtReady, useHead, useNuxtApp, useRuntimeConfig } from '
66
import { markRaw, ref } from 'vue'
77
// @ts-expect-error virtual template
88
import { resolveTrigger } from '#build/nuxt-scripts-trigger-resolver'
9+
import { debugEnabled } from '../debug'
910
import { logger } from '../logger'
1011

1112
type NuxtScriptsApp = ReturnType<typeof useNuxtApp> & {
@@ -293,6 +294,58 @@ export function useScript<T extends Record<symbol | string, any> = Record<symbol
293294
return reloaded.load()
294295
}
295296
nuxtApp.$scripts[id] = instance
297+
298+
// Debug logging: emit a structured log per script lifecycle event when debug
299+
// is enabled at build-time (or in dev). Tagged with registryKey when present
300+
// (e.g. `googleTagManager`), else the script id (src/key).
301+
if (import.meta.client && debugEnabled) {
302+
const registryKey = options?.devtools?.registryKey as string | undefined
303+
const src = (input as any)?.src
304+
const trigger = options?.trigger
305+
const loadedFrom = options?.devtools?.loadedFrom as string | undefined
306+
const ctx = {
307+
id: instance.id,
308+
...(registryKey ? { registryKey } : {}),
309+
...(src ? { src } : {}),
310+
...(loadedFrom ? { loadedFrom } : {}),
311+
}
312+
const log = logger.withTag(registryKey || instance.id)
313+
const t0 = performance.now()
314+
let tLoadStart = 0
315+
log.debug('registered', {
316+
...ctx,
317+
trigger: typeof trigger === 'object' ? (trigger instanceof Promise ? 'promise' : JSON.stringify(trigger)) : trigger,
318+
})
319+
options.head.hooks.hook('script:updated', (entry) => {
320+
if (entry.script.id !== instance.id)
321+
return
322+
const status = entry.script.status
323+
const elapsed = Math.round(performance.now() - t0)
324+
if (status === 'loading')
325+
tLoadStart = performance.now()
326+
const payload: Record<string, any> = { ...ctx, status, elapsedMs: elapsed }
327+
if (status === 'loaded' && tLoadStart)
328+
payload.loadMs = Math.round(performance.now() - tLoadStart)
329+
const fn = status === 'error' ? log.warn : log.debug
330+
fn(`status: ${status}`, payload)
331+
})
332+
const _origLoad = instance.load
333+
instance.load = () => {
334+
log.debug('load() called', ctx)
335+
return _origLoad()
336+
}
337+
const _origRemove = instance.remove
338+
instance.remove = () => {
339+
log.debug('remove() called', ctx)
340+
return _origRemove()
341+
}
342+
const _origReload = instance.reload
343+
instance.reload = async () => {
344+
log.debug('reload() called', ctx)
345+
return _origReload()
346+
}
347+
}
348+
296349
// used for devtools integration
297350
if (import.meta.dev && import.meta.client) {
298351
if (exists) {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare const __NUXT_SCRIPTS_DEBUG__: boolean
2+
3+
export const debugEnabled: boolean
4+
= typeof __NUXT_SCRIPTS_DEBUG__ !== 'undefined' && __NUXT_SCRIPTS_DEBUG__

packages/script/src/runtime/logger.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { createConsola } from 'consola'
2+
import { debugEnabled } from './debug'
23

34
export const logger = createConsola({
5+
// 4 = debug, 3 = info (consola defaults). Lift the threshold so `logger.debug`
6+
// fires when debug is opted-in at build time or in dev.
7+
level: debugEnabled ? 4 : 3,
48
defaults: {
59
tag: 'nuxt-scripts',
610
},

packages/script/src/runtime/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,13 @@ export type NuxtUseScriptOptions<T extends Record<symbol | string, any> = {}> =
168168
* @internal
169169
*/
170170
registryMeta?: Record<string, string>
171+
/**
172+
* Source location (file:line:col) the script was registered from, captured
173+
* via dev-only stack-trace parsing in `useRegistryScript`. Surfaced in
174+
* debug logs and Nuxt DevTools.
175+
* @internal
176+
*/
177+
loadedFrom?: string
171178
/**
172179
* Known third-party domains this script communicates with.
173180
* @internal

0 commit comments

Comments
 (0)