Skip to content

OpenPanelComponent: init script never executes in nested Next.js App Router layouts #290

Description

@stoicamarcus

Problem

OpenPanelComponent uses strategy="beforeInteractive" for the inline init script. In Next.js App Router, when the component is placed in a nested layout (e.g. app/[locale]/[params]/layout.tsx), the init script is serialized as RSC payload data but never rendered as an actual <script> tag in the HTML. The browser never executes it.

Root Cause

In Next.js App Router, beforeInteractive scripts are only promoted to real <script> tags in the <head> when placed in the root app/layout.tsx. In nested layouts, the Script component with beforeInteractive is treated as a regular React component and serialized into the RSC wire format:

self.__next_f.push([1,"...[\"$\",\"$L5c\",null,{\"strategy\":\"beforeInteractive\",\"dangerouslySetInnerHTML\":{\"__html\":\"window.op = ...\"}}]...

This is just JSON data describing the component's props — not an executable <script> tag. When React hydrates on the client, it sees strategy: "beforeInteractive" and does nothing, because beforeInteractive is designed to run before hydration, not after.

Result:

  • <script src="https://openpanel.dev/op1.js"> loads fine (no strategy = afterInteractive default)
  • The inline init script (window.op = ...; window.op('init', ...)) never executes
  • window.op is undefined
  • No events are tracked

Reproduction

  1. Create a Next.js App Router project
  2. Place <OpenPanelComponent clientId="..." trackScreenViews /> in a nested layout (not root app/layout.tsx)
  3. Load the page
  4. Check window.op in browser console → undefined
  5. Check view-source:window.op only appears inside the RSC JSON payload (self.__next_f.push), not as a <script> tag

Suggested Fix

Change the init script strategy from beforeInteractive to afterInteractive:

- r.createElement(p, { strategy: "beforeInteractive", dangerouslySetInnerHTML: { __html: `...` } })
+ r.createElement(p, { id: "openpanel-init", strategy: "afterInteractive", dangerouslySetInnerHTML: { __html: `...` } })

Or allow passing a strategy prop to override the default.

Workaround

Replace OpenPanelComponent with manual scripts:

import Script from 'next/script'

<Script
  id="openpanel-init"
  strategy="afterInteractive"
  dangerouslySetInnerHTML={{
    __html: `window.op = window.op || function(...args) {(window.op.q = window.op.q || []).push(args)};
window.op('init', ${JSON.stringify({ clientId: 'your-client-id', trackScreenViews: true, sdk: 'nextjs', sdkVersion: '1.0.8' })});`,
  }}
/>
<Script src="https://openpanel.dev/op1.js" strategy="afterInteractive" />

Versions

  • @openpanel/nextjs: 1.0.8 (also confirmed in latest 1.1.3)
  • Next.js: 16

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions