fix(rybbit): queue custom events before script loads#585
Conversation
Combined commits: - fix(plausible): use consistent window reference in clientInit stub
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
commit: |
| const flushQueue = () => { | ||
| const state = getRybbitState() | ||
| if (!state || state.flushed || !isRybbitReady()) return | ||
| state.flushed = true | ||
| while (state.queue.length > 0) { | ||
| const [method, ...args] = state.queue.shift()! | ||
| const fn = (window.rybbit as any)[method] | ||
| if (typeof fn === 'function') { | ||
| fn.apply(window.rybbit, args) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Queued calls will never be flushed because flushQueue() is only called in use(), which is executed once during initialization before the script loads. When the script later becomes ready, there's no mechanism to flush the queued calls.
View Details
📝 Patch Details
diff --git a/src/runtime/registry/rybbit-analytics.ts b/src/runtime/registry/rybbit-analytics.ts
index fcf02c1..1c7d634 100644
--- a/src/runtime/registry/rybbit-analytics.ts
+++ b/src/runtime/registry/rybbit-analytics.ts
@@ -80,16 +80,17 @@ function getRybbitState(): RybbitQueueState | undefined {
}
export function useScriptRybbitAnalytics<T extends RybbitAnalyticsApi>(_options?: RybbitAnalyticsInput) {
- // Check if real Rybbit is loaded
- const isRybbitReady = () => import.meta.client
+ // Check if real Rybbit is loaded (not our stub)
+ const isRealRybbit = () => import.meta.client
&& typeof window !== 'undefined'
&& window.rybbit
&& typeof window.rybbit.event === 'function'
+ && !('_isStub' in window.rybbit)
// Flush queued calls to real implementation
const flushQueue = () => {
const state = getRybbitState()
- if (!state || state.flushed || !isRybbitReady()) return
+ if (!state || state.flushed || !isRealRybbit()) return
state.flushed = true
while (state.queue.length > 0) {
const [method, ...args] = state.queue.shift()!
@@ -102,7 +103,7 @@ export function useScriptRybbitAnalytics<T extends RybbitAnalyticsApi>(_options?
// Wrapper that queues or calls directly
const callOrQueue = (method: string, ...args: any[]) => {
- if (isRybbitReady()) {
+ if (isRealRybbit()) {
const fn = (window.rybbit as any)[method]
if (typeof fn === 'function') {
fn.apply(window.rybbit, args)
@@ -133,7 +134,7 @@ export function useScriptRybbitAnalytics<T extends RybbitAnalyticsApi>(_options?
schema: import.meta.dev ? RybbitAnalyticsOptions : undefined,
scriptOptions: {
use() {
- // Flush queue when use() is called (happens on status changes)
+ // Flush queue when use() is called (when script becomes ready)
flushQueue()
// Return wrappers that queue if not ready
return {
@@ -141,11 +142,30 @@ export function useScriptRybbitAnalytics<T extends RybbitAnalyticsApi>(_options?
event: (name: string, properties?: Record<string, any>) => callOrQueue('event', name, properties),
identify: (userId: string) => callOrQueue('identify', userId),
clearUserId: () => callOrQueue('clearUserId'),
- getUserId: () => window.rybbit?.getUserId?.() ?? null,
+ getUserId: () => (window.rybbit as any)?._isStub ? null : window.rybbit?.getUserId?.() ?? null,
rybbit: window.rybbit,
} as RybbitAnalyticsApi
},
},
+ // Create a stub that queues calls until the real script loads
+ clientInit: import.meta.server
+ ? undefined
+ : () => {
+ const w = window as any
+ if (!w.rybbit) {
+ const state = getRybbitState()
+ if (state) {
+ w.rybbit = {
+ _isStub: true,
+ pageview: function () { state.queue.push(['pageview', ...arguments]) },
+ event: function () { state.queue.push(['event', ...arguments]) },
+ identify: function () { state.queue.push(['identify', ...arguments]) },
+ clearUserId: function () { state.queue.push(['clearUserId', ...arguments]) },
+ getUserId: () => null,
+ }
+ }
+ }
+ },
}
}, _options)
}
Analysis
Queued Rybbit analytics calls never flushed before script loads
What fails: Events tracked immediately after page load (before the Rybbit script loads) are silently lost. Calls to proxy.event(), proxy.identify(), and proxy.pageview() made before the external script initializes are queued but never processed.
How to reproduce:
- Refresh page on the Rybbit Analytics test page:
playground/pages/third-parties/rybbit-analytics.vue - Before the status changes to "loaded", click "Track Immediate Event" button
- Check browser console or network tab - the event is never sent to Rybbit's servers
- Navigate away and back (SPA navigation) - subsequent events work correctly
Result: Queued events remain in globalThis[Symbol.for('nuxt-scripts.rybbit-queue')] forever and are never executed. The queue state persists but has no mechanism to trigger flushing after the real script loads.
Expected: Events queued before the script loads should be automatically flushed and sent to Rybbit when the script becomes ready.
Root cause: The previous fix (commit 6720a6a) created a stub with clientInit hook that initialized the queue, but the recent change (commit aa50bd0) removed clientInit and replaced the stub detection with globalThis-based queueing. However, without clientInit, there's no mechanism to create the stub before user code runs. The use() function is only called once during initialization (before the script loads), and flushQueue() returns early because isRealRybbit() checks fail. When the real script loads later, there's no mechanism to call flushQueue() again.
Solution: Restore the clientInit hook to create a stub with _isStub: true marker. The stub's methods queue calls, and isRealRybbit() now checks for the absence of _isStub to detect when the real script has loaded. When real script loads, it replaces the stub, and subsequent calls or the next use() invocation will trigger flushQueue() successfully.
🔗 Linked issue
Resolves #461
❓ Type of change
📚 Description
Custom events (like
proxy.event()) failed silently after page refresh but worked after SPA navigation. The root cause was thatuse()returnednullwhenwindow.rybbitwas undefined, breaking the proxy's ability to queue calls before the script loaded.Added
clientInitto create a stub that queues calls, andflushQueue()that replays them once the real script loads. This follows the same pattern used by Google Analytics and Plausible.