Skip to content

fix(nextjs-config): warn when withPostHogConfig is not the outermost wrapper#3585

Open
Ashut0sh-mishra wants to merge 3 commits into
PostHog:mainfrom
Ashut0sh-mishra:fix/nextjs-config-warn-not-outermost
Open

fix(nextjs-config): warn when withPostHogConfig is not the outermost wrapper#3585
Ashut0sh-mishra wants to merge 3 commits into
PostHog:mainfrom
Ashut0sh-mishra:fix/nextjs-config-warn-not-outermost

Conversation

@Ashut0sh-mishra
Copy link
Copy Markdown

Problem

If withPostHogConfig is not the outermost wrapper in next.config.js
— e.g. withNextIntl(withPostHogConfig(config, { ... })) — the PostHog
build plugin silently never runs. No source maps upload, no errors, no
logs even with logLevel: 'debug'. Moving withPostHogConfig to be
the last wrapper makes it work as expected.

Root cause: withPostHogConfig returns an async function that Next.js
calls during build init. If a downstream wrapper consumes that function
and returns its own plain-object config without delegating, Next.js
never invokes our function, so the webpack/compiler hooks the plugin
adds never run.

Closes #3572

Fix

Set an invoked flag inside the returned config function and schedule
a .unref()-ed 5-second setTimeout that prints a clear warning if
the flag is still false. When Next.js (or any properly delegating
outer wrapper) does call our function, the flag flips before the timer
fires and no warning is emitted.

The warning tells the user:

  • what went wrong (the function was never invoked),
  • the likely cause (outer wrapper swallowed it), and
  • the fix (move withPostHogConfig to be the outermost wrapper).

It also links the tracking issue for context.

Notes:

  • .unref() ensures the timer never holds the Node process open by
    itself.
  • The change preserves the existing return type and runtime behaviour
    in the happy path (no extra work, just a flag flip on invocation).

…wrapper

withPostHogConfig returns an async function that Next.js calls during build
init. If a downstream wrapper (e.g. withNextIntl, withSentryConfig) consumes
that function and produces its own plain-object config without delegating,
Next.js never calls our function, so the webpack/compiler hooks that upload
source maps silently never run. Even with debug logging enabled the user sees
zero output — no error, no warning, just a missing build step.

Detect that by setting an invoked flag inside the returned function and
scheduling a 5s unref'd setTimeout that warns if the flag is still false.
When Next.js (or any outer wrapper) does call our function, the flag flips
before the timer fires and no warning is emitted. The timer is .unref()'d
so it never holds the process open by itself.

The warning points the user at the fix (move withPostHogConfig to be the
outermost wrapper) and links the tracking issue.

Closes PostHog#3572

Co-authored-by: nik464 <nik464@users.noreply.github.com>
@Ashut0sh-mishra Ashut0sh-mishra requested a review from a team as a code owner May 12, 2026 11:47
@vercel
Copy link
Copy Markdown

vercel Bot commented May 12, 2026

@Ashut0sh-mishra is attempting to deploy a commit to the PostHog Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/nextjs-config/src/config.ts:59-62
The multi-step runtime type-guard for `unref()` is more complex than necessary (violates simplicity rule 4). Optional chaining via a single cast achieves the same result in one expression and doesn't need a separate `if` branch.

```suggestion
    // Allow the Node process to exit normally even if our timer is pending.
    ;(timer as unknown as { unref?: () => void }).unref?.()
```

### Issue 2 of 2
packages/nextjs-config/src/config.ts:47-58
**False positive when config is imported outside a Next.js build**

If `next.config.js` is evaluated in a context that never goes through the full Next.js build pipeline — e.g. a Jest/Vitest test that imports the config file for assertions, or a custom script that reads the config object directly — `nextConfigFn` is never called and the 5-second warning fires. The user's setup may be perfectly correct, but they'd still see a misleading warning after 5 s of running their test suite. A comment (or an opt-out flag in `posthogConfig`) documenting this known false-positive would help, and the 5-second constant being named (e.g. `INVOCATION_TIMEOUT_MS`) would make the threshold easy to adjust.

Reviews (1): Last reviewed commit: "fix(nextjs-config): warn when withPostHo..." | Re-trigger Greptile

Comment thread packages/nextjs-config/src/config.ts Outdated
Comment thread packages/nextjs-config/src/config.ts Outdated
…tant

Addresses greptile review on PostHog#3585:
- Replace the multi-step runtime type-guard for `unref()` with a single
  optional-chaining call. Same null-safety, simpler code, satisfies the
  repo's simplicity convention (rule PostHog#4).
- Extract the 5000ms literal into `INVOCATION_TIMEOUT_MS` for clarity and
  easy future tuning.
- Document the known false-positive when `next.config.js` is imported
  outside the Next.js build pipeline (e.g. test contexts).

Co-authored-by: nik464 <nik464@users.noreply.github.com>
@Ashut0sh-mishra
Copy link
Copy Markdown
Author

Thanks @greptile-apps! Addressed both:

  • Replaced the multi-step unref() type-guard with a single optional-chaining call -- cleaner and same null-safety.
  • Extracted the 5000ms literal into INVOCATION_TIMEOUT_MS and added a comment about the known false-positive when next.config.js is imported outside the Next.js build (e.g. test contexts). I chose to leave it as a documented limitation rather than add an opt-out flag, since the warning is informational and adding API surface for an edge case felt heavier than the benefit. Happy to revisit if you'd prefer the flag.

Pushed.

1 similar comment
@Ashut0sh-mishra
Copy link
Copy Markdown
Author

Thanks @greptile-apps! Addressed both:

  • Replaced the multi-step unref() type-guard with a single optional-chaining call -- cleaner and same null-safety.
  • Extracted the 5000ms literal into INVOCATION_TIMEOUT_MS and added a comment about the known false-positive when next.config.js is imported outside the Next.js build (e.g. test contexts). I chose to leave it as a documented limitation rather than add an opt-out flag, since the warning is informational and adding API surface for an edge case felt heavier than the benefit. Happy to revisit if you'd prefer the flag.

Pushed.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

The changes look good. Both items are well-handled:

  • The optional-chaining cast (timer as unknown as { unref?: () => void }).unref?.() is cleaner and idiomatic — no complaints there.
  • INVOCATION_TIMEOUT_MS is the right call; magic numbers in timing logic are easy to misread.

The decision to document the false-positive rather than add an opt-out flag is sound. The warning is console.warn, not an error or process exit, so a spurious fire in a test context is low-friction noise at worst. Adding an API knob for it would be premature.

One minor observation: INVOCATION_TIMEOUT_MS is declared inside withPostHogConfig, so it's re-created on every call. Since it's effectively a module-level constant, moving it above the function would be marginally cleaner — but this is genuinely trivial and doesn't affect correctness.

LGTM overall.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Both changes look good. The optional-chain cast (timer as unknown as { unref?: () => void }).unref?.() is clean, and INVOCATION_TIMEOUT_MS with the documented false-positive caveat is a reasonable trade-off — no need for an opt-out flag given the warning is non-fatal.

One minor nit: INVOCATION_TIMEOUT_MS is declared inside the function body rather than at module scope. Since it's a pure configuration constant, hoisting it to module level would make it easier to spot (and tweak) alongside other module-level constants, but it's not a blocker.

Otherwise this looks ready to merge.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Reviews (2): Last reviewed commit: "refactor(nextjs-config): simplify unref(..." | Re-trigger Greptile

Addresses greptile minor nit on PostHog#3585: the constant is configuration,
not per-call state, so move it to module scope alongside the other
module-level declarations. Avoids re-creation on every call and makes
it easier to spot for future tuning.

Co-authored-by: nik464 <nik464@users.noreply.github.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Reviews (3): Last reviewed commit: "refactor(nextjs-config): hoist INVOCATIO..." | Re-trigger Greptile

@Ashut0sh-mishra
Copy link
Copy Markdown
Author

Thanks @greptile-apps -- fair point. Hoisted INVOCATION_TIMEOUT_MS to module scope so it sits with the other module-level declarations and isn't re-created per call. Pushed.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Confirmed — INVOCATION_TIMEOUT_MS is now at module scope alongside the other declarations. The implementation looks good: the invoked flag, the guarded setTimeout, and the .unref?.() call are all clean. LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@posthog/nextjs-config withPostHogConfig silently fails if it is not the outermost config wrapper

1 participant