[PULSE-51] Visitor ID storage: optional localStorage for cross-tab unique visitors #21

Merged
uz1mani merged 17 commits from staging into main 2026-02-11 15:14:13 +00:00
2 changed files with 46 additions and 10 deletions
Showing only changes of commit c827e82376 - Show all commits

View File

@@ -89,6 +89,9 @@ export default function ScriptSetupBlock({
<code className="text-xs text-neutral-900 dark:text-white break-all font-mono block pr-10">
{`<script defer data-domain="${site.domain}" data-api="${API_URL}" src="${APP_URL}/script.js"></script>`}
</code>
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage=&quot;local&quot;</code> for cross-tab unique visitors; <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage-ttl=&quot;24&quot;</code> (hours) to expire the ID.
</p>
greptile-apps[bot] commented 2026-02-11 13:04:49 +00:00 (Migrated from github.com)
Review

Documentation says data-storage="session" is "Optional" but according to the implementation, this should be the default. If localStorage is now the default, this text should clarify that data-storage="session" opts OUT of cross-tab tracking.

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/sites/ScriptSetupBlock.tsx
Line: 92:94

Comment:
Documentation says `data-storage="session"` is "Optional" but according to the implementation, this should be the default. If localStorage is now the default, this text should clarify that `data-storage="session"` opts OUT of cross-tab tracking.

How can I resolve this? If you propose a fix, please make it concise.
Documentation says `data-storage="session"` is "Optional" but according to the implementation, this should be the default. If localStorage is now the default, this text should clarify that `data-storage="session"` opts OUT of cross-tab tracking. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/sites/ScriptSetupBlock.tsx Line: 92:94 Comment: Documentation says `data-storage="session"` is "Optional" but according to the implementation, this should be the default. If localStorage is now the default, this text should clarify that `data-storage="session"` opts OUT of cross-tab tracking. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-11 13:13:00 +00:00 (Migrated from github.com)
Review

Issue: Copy said “Optional: data-storage="session"” without stating that the default is cross-tab and that session opts out.
Fix: Copy updated to: “Default: cross-tab (localStorage). Optional: data-storage="session" to opt out (per-tab, ephemeral).”
Why: Makes it explicit that the default is cross-tab and that data-storage="session" opts out of it.

Issue: Copy said “Optional: data-storage=\"session\"” without stating that the default is cross-tab and that session opts out. Fix: Copy updated to: “Default: cross-tab (localStorage). Optional: data-storage=\"session\" to opt out (per-tab, ephemeral).” Why: Makes it explicit that the default is cross-tab and that data-storage="session" opts out of it.
greptile-apps[bot] commented 2026-02-11 13:35:14 +00:00 (Migrated from github.com)
Review

Missing documentation for data-storage-ttl attribute. The script supports an optional TTL parameter (default 24 hours) but this isn't mentioned here. Consider adding: "Optional: data-storage-ttl=\"48\" to set expiry in hours (default: 24)."

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: components/sites/ScriptSetupBlock.tsx
Line: 92:94

Comment:
Missing documentation for `data-storage-ttl` attribute. The script supports an optional TTL parameter (default 24 hours) but this isn't mentioned here. Consider adding: "Optional: `data-storage-ttl=\"48\"` to set expiry in hours (default: 24)."

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.
Missing documentation for `data-storage-ttl` attribute. The script supports an optional TTL parameter (default 24 hours) but this isn't mentioned here. Consider adding: "Optional: `data-storage-ttl=\"48\"` to set expiry in hours (default: 24)." <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: components/sites/ScriptSetupBlock.tsx Line: 92:94 Comment: Missing documentation for `data-storage-ttl` attribute. The script supports an optional TTL parameter (default 24 hours) but this isn't mentioned here. Consider adding: "Optional: `data-storage-ttl=\"48\"` to set expiry in hours (default: 24)." <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-11 14:34:22 +00:00 (Migrated from github.com)
Review

Fix: Already added on line 93: “Optional: data-storage-ttl="48" to set expiry in hours (default: 24).”
Why: So users know they can change the cross-tab visitor ID expiry.
If the comment was from a branch that doesn’t have this yet, merge or cherry-pick the commit that added it, or re-apply that sentence in the same paragraph.

Fix: Already added on line 93: “Optional: data-storage-ttl=\"48\" to set expiry in hours (default: 24).” Why: So users know they can change the cross-tab visitor ID expiry. If the comment was from a branch that doesn’t have this yet, merge or cherry-pick the commit that added it, or re-apply that sentence in the same paragraph.
<button
type="button"
onClick={copyScript}

View File

@@ -1,6 +1,8 @@
/**
greptile-apps[bot] commented 2026-02-11 13:04:45 +00:00 (Migrated from github.com)
Review

Default behavior changed from sessionStorage to localStorage, contradicting PR description which states "keep sessionStorage as default". Comments say "Default: cross-tab visitor ID (localStorage)" but PR says "Default remains sessionStorage".

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 4:5

Comment:
Default behavior changed from sessionStorage to localStorage, contradicting PR description which states "keep sessionStorage as default". Comments say "Default: cross-tab visitor ID (localStorage)" but PR says "Default remains sessionStorage".

How can I resolve this? If you propose a fix, please make it concise.
Default behavior changed from sessionStorage to localStorage, contradicting PR description which states "keep sessionStorage as default". Comments say "Default: cross-tab visitor ID (localStorage)" but PR says "Default remains sessionStorage". <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 4:5 Comment: Default behavior changed from sessionStorage to localStorage, contradicting PR description which states "keep sessionStorage as default". Comments say "Default: cross-tab visitor ID (localStorage)" but PR says "Default remains sessionStorage". How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-11 13:12:32 +00:00 (Migrated from github.com)
Review

Issue: Default was documented as sessionStorage while the PR said “keep sessionStorage as default.”
Fix: Default is cross-tab (localStorage). File header now says: “Default: cross-tab visitor ID (localStorage), optional data-storage-ttl in hours. Optional: data-storage="session" for per-tab (ephemeral) counting.”
Why: Product decision is to use cross-tab as the default; docs and code are aligned with that.

Issue: Default was documented as sessionStorage while the PR said “keep sessionStorage as default.” Fix: Default is cross-tab (localStorage). File header now says: “Default: cross-tab visitor ID (localStorage), optional data-storage-ttl in hours. Optional: data-storage=\"session\" for per-tab (ephemeral) counting.” Why: Product decision is to use cross-tab as the default; docs and code are aligned with that.
* Pulse - Privacy-First Tracking Script
* Lightweight, no cookies, GDPR compliant
* Lightweight, no cookies, GDPR compliant.
* Default: ephemeral session ID (sessionStorage, per-tab). Optional: data-storage="local"
* and data-storage-ttl (hours) for a persistent cross-tab visitor ID with optional expiry.
*/
(function() {
@@ -19,6 +21,11 @@
greptile-apps[bot] commented 2026-02-11 13:27:08 +00:00 (Migrated from github.com)
Review

parseFloat with two arguments (second argument is radix) is incorrect - parseFloat only accepts one argument. The radix parameter is for parseInt, not parseFloat.

  const ttlHours = storageMode === 'local' ? parseFloat(script.getAttribute('data-storage-ttl') || '24') : 0;
Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 27:27

Comment:
`parseFloat` with two arguments (second argument is radix) is incorrect - `parseFloat` only accepts one argument. The radix parameter is for `parseInt`, not `parseFloat`.

```suggestion
  const ttlHours = storageMode === 'local' ? parseFloat(script.getAttribute('data-storage-ttl') || '24') : 0;
```

How can I resolve this? If you propose a fix, please make it concise.
`parseFloat` with two arguments (second argument is radix) is incorrect - `parseFloat` only accepts one argument. The radix parameter is for `parseInt`, not `parseFloat`. ```suggestion const ttlHours = storageMode === 'local' ? parseFloat(script.getAttribute('data-storage-ttl') || '24') : 0; ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 27:27 Comment: `parseFloat` with two arguments (second argument is radix) is incorrect - `parseFloat` only accepts one argument. The radix parameter is for `parseInt`, not `parseFloat`. ```suggestion const ttlHours = storageMode === 'local' ? parseFloat(script.getAttribute('data-storage-ttl') || '24') : 0; ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-11 13:29:33 +00:00 (Migrated from github.com)
Review

Issue: parseFloat was called with two arguments; only parseInt accepts a radix.
Fix: Removed the second argument so the call is parseFloat(script.getAttribute('data-storage-ttl') || '24').
Why: Correct use of the API.

Issue: parseFloat was called with two arguments; only parseInt accepts a radix. Fix: Removed the second argument so the call is parseFloat(script.getAttribute('data-storage-ttl') || '24'). Why: Correct use of the API.
const domain = script.getAttribute('data-domain');
const apiUrl = script.getAttribute('data-api') || 'https://pulse-api.ciphera.net';
// * Visitor ID storage: "session" (default, ephemeral per-tab) or "local" (persistent, cross-tab)
const storageMode = (script.getAttribute('data-storage') || 'session').toLowerCase() === 'local' ? 'local' : 'session';
// * When storage is "local", optional TTL in hours; after TTL the ID is regenerated (e.g. 24 = one day)
const ttlHours = storageMode === 'local' ? parseFloat(script.getAttribute('data-storage-ttl') || '24', 10) : 0;
const ttlMs = ttlHours > 0 ? ttlHours * 60 * 60 * 1000 : 0;
// * Performance Monitoring (Core Web Vitals) State
let currentEventId = null;
@@ -102,25 +109,51 @@
}
greptile-apps[bot] commented 2026-02-11 13:04:47 +00:00 (Migrated from github.com)
Review

Comment states "Default: ephemeral (sessionStorage, per-tab)" but the actual default in line 25 is 'local' (localStorage). This is a contradiction - either the comment or the code is incorrect.

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 119:120

Comment:
Comment states "Default: ephemeral (sessionStorage, per-tab)" but the actual default in line 25 is `'local'` (localStorage). This is a contradiction - either the comment or the code is incorrect.

How can I resolve this? If you propose a fix, please make it concise.
Comment states "Default: ephemeral (sessionStorage, per-tab)" but the actual default in line 25 is `'local'` (localStorage). This is a contradiction - either the comment or the code is incorrect. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 119:120 Comment: Comment states "Default: ephemeral (sessionStorage, per-tab)" but the actual default in line 25 is `'local'` (localStorage). This is a contradiction - either the comment or the code is incorrect. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-11 13:12:41 +00:00 (Migrated from github.com)
Review

Issue: Comment said “Default: ephemeral (sessionStorage, per-tab)” but the code default was 'local'.
Fix: Comment updated to: “Default: persistent (localStorage, cross-tab), optional TTL in hours. With data-storage="session": ephemeral (sessionStorage, per-tab).” Default in code remains 'local'.
Why: Comment and code now both describe the cross-tab default.

Issue: Comment said “Default: ephemeral (sessionStorage, per-tab)” but the code default was 'local'. Fix: Comment updated to: “Default: persistent (localStorage, cross-tab), optional TTL in hours. With data-storage=\"session\": ephemeral (sessionStorage, per-tab).” Default in code remains 'local'. Why: Comment and code now both describe the cross-tab default.
greptile-apps[bot] commented 2026-02-11 13:40:18 +00:00 (Migrated from github.com)
Review

regenerating ID after all re-reads means two tabs opening simultaneously will create different IDs even when they shouldn't - the second generateId() call creates a new ID rather than reusing the one from line 146, causing both tabs to write their own unique IDs

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 176:176

Comment:
regenerating ID after all re-reads means two tabs opening simultaneously will create different IDs even when they shouldn't - the second `generateId()` call creates a new ID rather than reusing the one from line 146, causing both tabs to write their own unique IDs

How can I resolve this? If you propose a fix, please make it concise.
regenerating ID after all re-reads means two tabs opening simultaneously will create different IDs even when they shouldn't - the second `generateId()` call creates a new ID rather than reusing the one from line 146, causing both tabs to write their own unique IDs <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 176:176 Comment: regenerating ID after all re-reads means two tabs opening simultaneously will create different IDs even when they shouldn't - the second `generateId()` call creates a new ID rather than reusing the one from line 146, causing both tabs to write their own unique IDs How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-11 14:31:41 +00:00 (Migrated from github.com)
Review

Issue: Regenerating the ID before the write meant two tabs could each get a new ID and both write, so we could end up with two different IDs and duplicate counts.
Fix: Removed the second generateId(); we now use the ID from line 146 when writing.
Why: Avoids introducing a second ID in the write path so we don’t make the simultaneous-open case worse.

Issue: Regenerating the ID before the write meant two tabs could each get a new ID and both write, so we could end up with two different IDs and duplicate counts. Fix: Removed the second generateId(); we now use the ID from line 146 when writing. Why: Avoids introducing a second ID in the write path so we don’t make the simultaneous-open case worse.
});
// * Memory cache for session ID (fallback if sessionStorage is unavailable)
// * Memory cache for session ID (fallback if storage is unavailable)
let cachedSessionId = null;
// * Generate ephemeral session ID (not persistent)
function generateId() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
// * Returns session/visitor ID. Default: ephemeral (sessionStorage, per-tab).
// * With data-storage="local": persistent (localStorage, cross-tab), optional TTL in hours.
function getSessionId() {
if (cachedSessionId) {
return cachedSessionId;
}
// * Use a static key for session storage to ensure consistency across pages
const key = 'ciphera_session_id';
// * Legacy key support for migration (strip whitespace just in case)
const legacyKey = 'plausible_session_' + (domain ? domain.trim() : '');
try {
// * Try to get existing session ID
cachedSessionId = sessionStorage.getItem(key);
if (storageMode === 'local') {
try {
const raw = localStorage.getItem(key);
if (raw) {
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed.id === 'string') {
const expired = ttlMs > 0 && typeof parsed.created === 'number' && (Date.now() - parsed.created > ttlMs);
if (!expired) {
cachedSessionId = parsed.id;
return cachedSessionId;
}
}
} catch (e) {
// * Invalid JSON or old string format: treat as expired and regenerate
}
}
cachedSessionId = generateId();
localStorage.setItem(key, JSON.stringify({ id: cachedSessionId, created: Date.now() }));
} catch (e) {
cachedSessionId = generateId();
}
return cachedSessionId;
}
// * If not found in new key, try legacy key and migrate
// * Default: session storage (ephemeral, per-tab)
try {
cachedSessionId = sessionStorage.getItem(key);
if (!cachedSessionId && legacyKey) {
cachedSessionId = sessionStorage.getItem(legacyKey);
greptile-apps[bot] commented 2026-02-11 14:48:36 +00:00 (Migrated from github.com)
Review

Legacy value causes churn
In the localStorage mode, localStorage.getItem(key) is always JSON.parse’d; if parsing fails (including the “old string format” case mentioned in the comment), the code regenerates a new ID instead of reusing the existing string value. If any existing installs already have a plain-string ID under ciphera_session_id in localStorage, this will force an unexpected visitor-id reset on upgrade. A small migration path (e.g., if raw is a non-empty string and not JSON, treat it as the ID and wrap it into the JSON format with created) would avoid that churn.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 129:132

Comment:
**Legacy value causes churn**
In the `localStorage` mode, `localStorage.getItem(key)` is always `JSON.parse`’d; if parsing fails (including the “old string format” case mentioned in the comment), the code regenerates a new ID instead of reusing the existing string value. If any existing installs already have a plain-string ID under `ciphera_session_id` in `localStorage`, this will force an unexpected visitor-id reset on upgrade. A small migration path (e.g., if `raw` is a non-empty string and not JSON, treat it as the ID and wrap it into the JSON format with `created`) would avoid that churn.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.
**Legacy value causes churn** In the `localStorage` mode, `localStorage.getItem(key)` is always `JSON.parse`’d; if parsing fails (including the “old string format” case mentioned in the comment), the code regenerates a new ID instead of reusing the existing string value. If any existing installs already have a plain-string ID under `ciphera_session_id` in `localStorage`, this will force an unexpected visitor-id reset on upgrade. A small migration path (e.g., if `raw` is a non-empty string and not JSON, treat it as the ID and wrap it into the JSON format with `created`) would avoid that churn. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 129:132 Comment: **Legacy value causes churn** In the `localStorage` mode, `localStorage.getItem(key)` is always `JSON.parse`’d; if parsing fails (including the “old string format” case mentioned in the comment), the code regenerates a new ID instead of reusing the existing string value. If any existing installs already have a plain-string ID under `ciphera_session_id` in `localStorage`, this will force an unexpected visitor-id reset on upgrade. A small migration path (e.g., if `raw` is a non-empty string and not JSON, treat it as the ID and wrap it into the JSON format with `created`) would avoid that churn. <sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub> How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani commented 2026-02-11 14:52:23 +00:00 (Migrated from github.com)
Review

Issue: In localStorage mode, a plain-string value in ciphera_session_id (e.g. from an older script) was treated as invalid and caused a new ID to be generated on upgrade.
Fix: If JSON.parse(raw) throws and raw is a non-empty string, we treat it as a legacy ID: set cachedSessionId = raw.trim(), write { id: cachedSessionId, created: Date.now() } to localStorage (first read only), and return. The same legacy handling is used in the two re-read paths (use the string as ID and return; no write there to avoid extra writes).
Why: Existing plain-string IDs are kept across upgrades and migrated to the new JSON shape in one step.

Issue: In localStorage mode, a plain-string value in ciphera_session_id (e.g. from an older script) was treated as invalid and caused a new ID to be generated on upgrade. Fix: If JSON.parse(raw) throws and raw is a non-empty string, we treat it as a legacy ID: set cachedSessionId = raw.trim(), write { id: cachedSessionId, created: Date.now() } to localStorage (first read only), and return. The same legacy handling is used in the two re-read paths (use the string as ID and return; no write there to avoid extra writes). Why: Existing plain-string IDs are kept across upgrades and migrated to the new JSON shape in one step.
if (cachedSessionId) {
@@ -133,7 +166,7 @@
}
if (!cachedSessionId) {
cachedSessionId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
cachedSessionId = generateId();
try {
sessionStorage.setItem(key, cachedSessionId);
} catch (e) {