Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .claude/plans/543-plausible-broken.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## DONE

# Fix: Plausible Analytics new script not working (#543)

## Problem
Users report that the new Plausible script format (with `scriptId`) loads correctly in DOM but no events are sent.

## Analysis

### Root Cause
The `clientInit` stub uses bare `plausible` identifier inconsistently with `window.plausible`:

```js
// Current code (plausible-analytics.ts:187)
window.plausible = window.plausible || function () { (plausible.q = plausible.q || []).push(arguments) }, plausible.init = plausible.init || function (i) { plausible.o = i || {} }
```

Issues:
1. **Inconsistent window reference**: First part uses `window.plausible`, second part uses bare `plausible`
2. **Module scope**: In ES modules (strict mode), bare identifier resolution differs from non-module scripts
3. **Compare to GA**: Google Analytics uses `w` (window) consistently throughout its clientInit

### How Plausible's new script works
The `pa-{scriptId}.js` script:
1. Checks `plausible.o && S(plausible.o)` on load to pick up pre-init options
2. The stub's `plausible.init()` stores options in `plausible.o`
3. Script has domain hardcoded, doesn't need `data-domain` attribute

### Verification
Plausible script expected stub format:
```js
window.plausible = window.plausible || {}
plausible.o && S(plausible.o) // If .o exists, initialize with those options
```

Our stub needs to set `plausible.o` before script loads, which it does via:
```js
plausible.init = function(i) { plausible.o = i || {} }
window.plausible.init(initOptions)
```

## Fix

Update `clientInit` to use `window.plausible` consistently (like GA does):

```ts
clientInit() {
const w = window as any
w.plausible = w.plausible || function () { (w.plausible.q = w.plausible.q || []).push(arguments) }
w.plausible.init = w.plausible.init || function (i: PlausibleInitOptions) { w.plausible.o = i || {} }
w.plausible.init(initOptions)
}
```

## Files to modify
- `src/runtime/registry/plausible-analytics.ts`: Fix clientInit stub pattern

## Test plan
1. Run existing tests
2. Test playground with plausible-analytics-v2.vue
3. Verify script loads and init options are picked up
147 changes: 136 additions & 11 deletions playground/pages/third-parties/rybbit-analytics.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<script lang="ts" setup>
import { ref, useHead } from '#imports'
import { ref, useHead, watch } from '#imports'

useHead({
title: 'Rybbit Analytics',
})

// Rybbit Analytics with demo configuration
// Rybbit Analytics with real site ID
const { proxy, status } = useScriptRybbitAnalytics({
siteId: 'demo-site-123',
siteId: '874',
autoTrackPageview: true,
trackSpa: true,
trackOutbound: true,
Expand All @@ -23,37 +23,78 @@ proxy.pageview()

const eventCounter = ref(0)
const userId = ref('')
const eventLog = ref<Array<{ time: string, event: string, status: string }>>([])

// Log status changes
watch(status, (newStatus, oldStatus) => {
logEvent(`Status: ${oldStatus} -> ${newStatus}`, newStatus)
}, { immediate: true })

// Fire events immediately on component setup (before script loads)
// This is the actual test for issue #461 - all these should queue and flush
const initialStatus = status.value
proxy.event('mount_event', { fired_at: 'component_setup' })
proxy.identify('test-user-on-mount')
proxy.pageview()
logEvent(`Queued on mount: event, identify, pageview`, initialStatus)

function logEvent(event: string, scriptStatus: string) {
eventLog.value.unshift({
time: new Date().toLocaleTimeString(),
event,
status: scriptStatus,
})
// Keep only last 20 events
if (eventLog.value.length > 20) {
eventLog.value.pop()
}
}

function trackCustomEvent() {
eventCounter.value++
const currentStatus = status.value
proxy.event('button_click', {
button_name: 'Custom Event Button',
click_count: eventCounter.value,
timestamp: Date.now(),
})
logEvent(`proxy.event('button_click', { count: ${eventCounter.value} })`, currentStatus)
}

// Test event that runs immediately without checking status
// This is what the issue #461 tests - events called before script is ready
function trackImmediateEvent() {
const currentStatus = status.value
proxy.event('immediate_test', { timestamp: Date.now() })
logEvent(`proxy.event('immediate_test') - NO STATUS CHECK`, currentStatus)
}

function trackConversion() {
const currentStatus = status.value
proxy.event('conversion', {
conversion_type: 'demo_conversion',
value: 25.99,
currency: 'USD',
})
logEvent(`proxy.event('conversion')`, currentStatus)
}

function identifyUser() {
if (userId.value) {
proxy.identify(userId.value)
logEvent(`proxy.identify('${userId.value}')`, status.value)
}
}

function clearUser() {
proxy.clearUserId()
logEvent(`proxy.clearUserId()`, status.value)
userId.value = ''
}

function getCurrentUserId() {
const currentId = proxy.getUserId()
logEvent(`proxy.getUserId() => ${currentId}`, status.value)
if (currentId) {
userId.value = currentId
}
Expand All @@ -71,32 +112,116 @@ function getCurrentUserId() {
</p>
<UAlert
icon="i-heroicons-information-circle"
color="blue"
color="info"
variant="soft"
class="mt-4"
title="Demo Configuration"
description="This example uses a demo site ID. Replace with your actual Rybbit site ID for production use."
/>
</div>

<!-- Issue #461 Test Section -->
<UCard>
<template #header>
<h2 class="text-xl font-semibold">
Script Status
<h2 class="text-xl font-semibold text-amber-600">
Issue #461 Test: Refresh Behavior
</h2>
</template>

<div class="space-y-4">
<UAlert
icon="i-heroicons-exclamation-triangle"
color="warning"
variant="soft"
title="How to Test"
>
<ol class="list-decimal list-inside space-y-1 text-sm mt-2">
<li>Refresh this page (Cmd+R / F5)</li>
<li>Immediately click "Track Immediate Event" before status becomes "loaded"</li>
<li>Check the Event Log - event should be queued and sent when script loads</li>
<li>Compare: Navigate away and back (SPA nav) - events should work immediately</li>
</ol>
</UAlert>

<div>
<span class="font-medium">Current Status:</span>
<UBadge
:color="status === 'loaded' ? 'green' : status === 'loading' ? 'yellow' : 'gray'"
:color="status === 'loaded' ? 'success' : status === 'loading' ? 'warning' : 'neutral'"
class="ml-2"
>
{{ status }}
</UBadge>
</div>

<div class="flex flex-wrap gap-3">
<UButton
color="warning"
@click="trackImmediateEvent"
>
Track Event (no status check)
</UButton>
<UButton
color="warning"
variant="outline"
@click="() => { proxy.identify(`user-${Date.now()}`); logEvent('proxy.identify() - NO STATUS CHECK', status) }"
>
Identify (no status check)
</UButton>
<UButton
color="warning"
variant="outline"
@click="() => { proxy.pageview(); logEvent('proxy.pageview() - NO STATUS CHECK', status) }"
>
Pageview (no status check)
</UButton>
</div>

<p class="text-sm text-gray-500">
These buttons call proxy methods without checking status === 'loaded'.
Events are queued and flushed when script loads.
</p>
</div>
</UCard>

<!-- Event Log -->
<UCard>
<template #header>
<h2 class="text-xl font-semibold">
Event Log
</h2>
</template>

<div class="space-y-2 max-h-64 overflow-y-auto">
<div
v-for="(entry, i) in eventLog"
:key="i"
class="text-sm font-mono p-2 rounded"
:class="entry.status === 'loaded' ? 'bg-green-50 dark:bg-green-900/20' : 'bg-yellow-50 dark:bg-yellow-900/20'"
>
<span class="text-gray-500">{{ entry.time }}</span>
<UBadge
:color="entry.status === 'loaded' ? 'success' : 'warning'"
size="xs"
class="mx-2"
>
{{ entry.status }}
</UBadge>
<span>{{ entry.event }}</span>
</div>
<div v-if="eventLog.length === 0" class="text-gray-400 text-sm">
No events logged yet
</div>
</div>
</UCard>

<UCard>
<template #header>
<h2 class="text-xl font-semibold">
Script Status
</h2>
</template>

<div class="space-y-4">
<div>
<span class="font-medium">Features Enabled:</span>
<ul class="list-disc list-inside mt-2 space-y-1 text-sm">
Expand Down Expand Up @@ -128,15 +253,15 @@ function getCurrentUserId() {

<UButton
:disabled="status !== 'loaded'"
color="green"
color="success"
@click="trackConversion"
>
Track Conversion
</UButton>

<UButton
:disabled="status !== 'loaded'"
color="blue"
color="info"
@click="proxy.pageview()"
>
Manual Page View
Expand Down Expand Up @@ -174,15 +299,15 @@ function getCurrentUserId() {
<div class="flex gap-3">
<UButton
:disabled="status !== 'loaded'"
color="orange"
color="warning"
@click="getCurrentUserId"
>
Get Current User ID
</UButton>

<UButton
:disabled="status !== 'loaded'"
color="red"
color="error"
@click="clearUser"
>
Clear User ID
Expand Down
Loading
Loading