[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
uz1mani commented 2026-02-11 13:01:18 +00:00 (Migrated from github.com)

Work Item

PULSE-51

Summary

  • Add configurable visitor ID storage: keep sessionStorage as default (ephemeral, privacy-first) or use localStorage for cross-tab unique visitor accuracy.
  • Optional TTL/expiry for persistent ID (e.g. 24–30h) enforced in script; single key (e.g. ciphera_visitor_id) with timestamp and regeneration when expired.
  • Update README, DATA_AUDIT.md, script comments and any privacy/marketing copy to reflect default vs optional persistent ID and what is stored where.

Changes

  • pulse-frontend/public/script.js: Config option (e.g. script attribute or config) for storage type (sessionStorage | localStorage). When localStorage: read/write single key, optional creation timestamp and TTL; regenerate ID when expired. Default remains sessionStorage.
  • Docs: README, DATA_AUDIT.md (and any other “no cookies / ephemeral session ID” text) updated to state (1) default is sessionStorage, (2) optional localStorage = persistent ID with privacy trade-off, (3) first-party storage only and what data is stored where.
  • Script comments: In-code comments updated to describe storage behaviour and default/optional modes.

Test Plan

[ ] Default embed (no config): visitor ID in sessionStorage only; new tab = new session ID.
[ ] With cross-tab mode enabled: same origin, multiple tabs share one visitor ID (localStorage); counted as one visitor when backend aggregates by session_id.
[ ] With TTL set: after TTL, script regenerates ID and stores new timestamp.
[ ] Docs and comments accurately describe default, optional mode, and storage.

## Work Item PULSE-51 ## Summary - Add configurable visitor ID storage: keep sessionStorage as default (ephemeral, privacy-first) or use localStorage for cross-tab unique visitor accuracy. - Optional TTL/expiry for persistent ID (e.g. 24–30h) enforced in script; single key (e.g. ciphera_visitor_id) with timestamp and regeneration when expired. - Update README, DATA_AUDIT.md, script comments and any privacy/marketing copy to reflect default vs optional persistent ID and what is stored where. ## Changes - **pulse-frontend/public/script.js:** Config option (e.g. script attribute or config) for storage type (sessionStorage | localStorage). When localStorage: read/write single key, optional creation timestamp and TTL; regenerate ID when expired. Default remains sessionStorage. - **Docs:** README, DATA_AUDIT.md (and any other “no cookies / ephemeral session ID” text) updated to state (1) default is sessionStorage, (2) optional localStorage = persistent ID with privacy trade-off, (3) first-party storage only and what data is stored where. - **Script comments:** In-code comments updated to describe storage behaviour and default/optional modes. ## Test Plan [ ] Default embed (no config): visitor ID in sessionStorage only; new tab = new session ID. [ ] With cross-tab mode enabled: same origin, multiple tabs share one visitor ID (localStorage); counted as one visitor when backend aggregates by session_id. [ ] With TTL set: after TTL, script regenerates ID and stores new timestamp. [ ] Docs and comments accurately describe default, optional mode, and storage.
greptile-apps[bot] commented 2026-02-11 13:04:42 +00:00 (Migrated from github.com)

Greptile Overview

Greptile Summary

This PR implements configurable visitor ID storage with localStorage (cross-tab) as the default and an optional sessionStorage (per-tab) mode via data-storage="session" attribute.

Critical Issues:

  • Default behavior contradiction: The PR description states "keep sessionStorage as default" and "Default remains sessionStorage", but the implementation makes localStorage the default (line 25: || 'local'). Comments in the code also conflict - the header says "Default: cross-tab visitor ID (localStorage)" while function comments say "Default: ephemeral (sessionStorage, per-tab)".
  • Missing documentation updates: PR description promises updates to README.md and DATA_AUDIT.md to describe storage behavior, but only script.js and ScriptSetupBlock.tsx were modified. No documentation files were updated.

Implementation Details:

  • Adds data-storage attribute (defaults to 'local' = localStorage)
  • Adds data-storage-ttl attribute for localStorage TTL in hours (default 24)
  • Implements race condition mitigation by re-reading localStorage before writing
  • Maintains backward compatibility with legacy plausible_session_ key migration

Action Items:

  • Clarify intended default behavior (localStorage or sessionStorage)
  • Update code and comments to be consistent with chosen default
  • Complete promised documentation updates to README.md and DATA_AUDIT.md
  • Update ScriptSetupBlock.tsx documentation to clearly explain opt-in vs opt-out behavior

Confidence Score: 2/5

  • This PR has significant issues with contradictory defaults and missing documentation that must be resolved before merging.
  • Score reflects critical contradiction between PR description (sessionStorage default) and implementation (localStorage default), plus missing promised documentation updates. The core implementation works but the inconsistency creates confusion about privacy defaults and actual behavior.
  • Pay close attention to public/script.js - the default storage mode and comments need alignment with intended behavior.

Important Files Changed

Filename Overview
public/script.js Implements visitor ID storage with localStorage default and optional sessionStorage mode. Critical issue: default behavior contradicts PR description and internal comments claim sessionStorage is default when code uses localStorage.
components/sites/ScriptSetupBlock.tsx Added documentation for data-storage="session" option. Documentation needs clarification on whether this is opting in or out of cross-tab tracking given the default behavior confusion.

Sequence Diagram

sequenceDiagram
    participant User
    participant Script as script.js
    participant Storage as localStorage/sessionStorage
    participant API as Pulse API

    User->>Script: Page Load
    Script->>Script: Read data-storage attribute
    alt data-storage="session" (opt-in ephemeral)
        Script->>Storage: Check sessionStorage for ciphera_session_id
        alt ID exists
            Storage-->>Script: Return existing ID
        else No ID
            Script->>Script: generateId()
            Script->>Storage: Store in sessionStorage
        end
    else Default: localStorage (cross-tab)
        Script->>Storage: Check localStorage for ciphera_session_id
        alt ID exists & not expired
            Storage-->>Script: Return existing ID
        else ID expired or missing
            Script->>Script: generateId()
            Script->>Storage: Re-read (race fix)
            alt Another tab wrote ID
                Storage-->>Script: Use other tab's ID
            else Still empty
                Script->>Storage: Store with timestamp
            end
        end
    end
    Script->>API: POST /api/v1/events with session_id
    API-->>Script: Return event_id
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR implements configurable visitor ID storage with localStorage (cross-tab) as the default and an optional sessionStorage (per-tab) mode via `data-storage="session"` attribute. **Critical Issues:** - **Default behavior contradiction**: The PR description states "keep sessionStorage as default" and "Default remains sessionStorage", but the implementation makes localStorage the default (line 25: `|| 'local'`). Comments in the code also conflict - the header says "Default: cross-tab visitor ID (localStorage)" while function comments say "Default: ephemeral (sessionStorage, per-tab)". - **Missing documentation updates**: PR description promises updates to README.md and DATA_AUDIT.md to describe storage behavior, but only `script.js` and `ScriptSetupBlock.tsx` were modified. No documentation files were updated. **Implementation Details:** - Adds `data-storage` attribute (defaults to `'local'` = localStorage) - Adds `data-storage-ttl` attribute for localStorage TTL in hours (default 24) - Implements race condition mitigation by re-reading localStorage before writing - Maintains backward compatibility with legacy `plausible_session_` key migration **Action Items:** - Clarify intended default behavior (localStorage or sessionStorage) - Update code and comments to be consistent with chosen default - Complete promised documentation updates to README.md and DATA_AUDIT.md - Update ScriptSetupBlock.tsx documentation to clearly explain opt-in vs opt-out behavior <h3>Confidence Score: 2/5</h3> - This PR has significant issues with contradictory defaults and missing documentation that must be resolved before merging. - Score reflects critical contradiction between PR description (sessionStorage default) and implementation (localStorage default), plus missing promised documentation updates. The core implementation works but the inconsistency creates confusion about privacy defaults and actual behavior. - Pay close attention to `public/script.js` - the default storage mode and comments need alignment with intended behavior. <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | public/script.js | Implements visitor ID storage with localStorage default and optional sessionStorage mode. Critical issue: default behavior contradicts PR description and internal comments claim sessionStorage is default when code uses localStorage. | | components/sites/ScriptSetupBlock.tsx | Added documentation for `data-storage="session"` option. Documentation needs clarification on whether this is opting in or out of cross-tab tracking given the default behavior confusion. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant User participant Script as script.js participant Storage as localStorage/sessionStorage participant API as Pulse API User->>Script: Page Load Script->>Script: Read data-storage attribute alt data-storage="session" (opt-in ephemeral) Script->>Storage: Check sessionStorage for ciphera_session_id alt ID exists Storage-->>Script: Return existing ID else No ID Script->>Script: generateId() Script->>Storage: Store in sessionStorage end else Default: localStorage (cross-tab) Script->>Storage: Check localStorage for ciphera_session_id alt ID exists & not expired Storage-->>Script: Return existing ID else ID expired or missing Script->>Script: generateId() Script->>Storage: Re-read (race fix) alt Another tab wrote ID Storage-->>Script: Use other tab's ID else Still empty Script->>Storage: Store with timestamp end end end Script->>API: POST /api/v1/events with session_id API-->>Script: Return event_id ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-11 13:04:50 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

2 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

<sub>2 files reviewed, 4 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -91,1 +91,4 @@
</code>
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Default: cross-tab (localStorage). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage=&quot;session&quot;</code> to opt out (per-tab, ephemeral). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage-ttl=&quot;48&quot;</code> to set expiry in hours (default: 24).
</p>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:04:49 +00:00

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>
@@ -1,6 +1,8 @@
/**
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:04:45 +00:00

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>
@@ -102,25 +109,100 @@
}
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:04:47 +00:00

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>
@@ -122,0 +197,4 @@
} catch (e) {
cachedSessionId = generateId();
}
return cachedSessionId;
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:04:48 +00:00

Race condition fix has logic gap: if the ID in rawAgain is expired, code falls through to localStorage.setItem on line 161 using cachedSessionId from line 146, but doesn't check if yet another tab wrote a fresh ID between lines 148-161. Consider re-reading one more time right before the write.

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

Comment:
Race condition fix has logic gap: if the ID in `rawAgain` is expired, code falls through to `localStorage.setItem` on line 161 using `cachedSessionId` from line 146, but doesn't check if yet another tab wrote a fresh ID between lines 148-161. Consider re-reading one more time right before the write.

How can I resolve this? If you propose a fix, please make it concise.
Race condition fix has logic gap: if the ID in `rawAgain` is expired, code falls through to `localStorage.setItem` on line 161 using `cachedSessionId` from line 146, but doesn't check if yet another tab wrote a fresh ID between lines 148-161. Consider re-reading one more time right before the write. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 148:160 Comment: Race condition fix has logic gap: if the ID in `rawAgain` is expired, code falls through to `localStorage.setItem` on line 161 using `cachedSessionId` from line 146, but doesn't check if yet another tab wrote a fresh ID between lines 148-161. Consider re-reading one more time right before the write. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-11 13:12:32 +00:00
@@ -1,6 +1,8 @@
/**
uz1mani (Migrated from github.com) commented 2026-02-11 13:12:32 +00:00

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.
uz1mani (Migrated from github.com) reviewed 2026-02-11 13:12:41 +00:00
@@ -102,25 +109,100 @@
}
uz1mani (Migrated from github.com) commented 2026-02-11 13:12:41 +00:00

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.
uz1mani (Migrated from github.com) reviewed 2026-02-11 13:12:50 +00:00
@@ -122,0 +197,4 @@
} catch (e) {
cachedSessionId = generateId();
}
return cachedSessionId;
uz1mani (Migrated from github.com) commented 2026-02-11 13:12:49 +00:00

Issue: Race: we could overwrite a newer ID from another tab between the re-read and the write.
Fix: Added a final re-read of localStorage.getItem(key) immediately before localStorage.setItem. If that read returns a valid, non-expired ID, we use it and return without writing.
Why: Avoids overwriting a fresher ID written by another tab.

Issue: Race: we could overwrite a newer ID from another tab between the re-read and the write. Fix: Added a final re-read of localStorage.getItem(key) immediately before localStorage.setItem. If that read returns a valid, non-expired ID, we use it and return without writing. Why: Avoids overwriting a fresher ID written by another tab.
uz1mani (Migrated from github.com) reviewed 2026-02-11 13:13:00 +00:00
@@ -91,1 +91,4 @@
</code>
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Default: cross-tab (localStorage). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage=&quot;session&quot;</code> to opt out (per-tab, ephemeral). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage-ttl=&quot;48&quot;</code> to set expiry in hours (default: 24).
</p>
uz1mani (Migrated from github.com) commented 2026-02-11 13:13:00 +00:00

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:27:04 +00:00 (Migrated from github.com)

Greptile Overview

Greptile Summary

This PR implements configurable visitor ID storage, changing the default from ephemeral sessionStorage to persistent localStorage for cross-tab visitor tracking. The implementation includes:

Key Changes:

  • Default behavior now uses localStorage with a 24-hour TTL for cross-tab visitor identification
  • Users can opt out with data-storage="session" to preserve privacy-first, ephemeral behavior
  • Triple-read race condition mitigation added to handle concurrent tab initialization
  • Documentation updated across script comments, ScriptSetupBlock UI, and CHANGELOG

Issues Found:

  • Syntax error: parseFloat called with radix parameter (line 27) - parseFloat only accepts one argument, radix is for parseInt
  • Race condition handling has theoretical gaps but is acceptable for this use case
  • ID regeneration logic could be improved for edge cases with expired IDs from other tabs

Missing Documentation:
The PR description mentions updating DATA_AUDIT.md but this file was not modified in the PR. If this file exists, it should be updated to reflect the new storage behavior and privacy implications.

Confidence Score: 4/5

  • Safe to merge with one syntax fix needed
  • The implementation correctly addresses the feature requirements with proper default behavior and opt-out mechanism. There is one syntax error (parseFloat with radix) that needs fixing. The race condition handling is thorough though not theoretically perfect, which is acceptable for this use case. Documentation is clear and accurate.
  • public/script.js requires a syntax fix for parseFloat on line 27

Important Files Changed

Filename Overview
public/script.js Implemented configurable visitor ID storage with localStorage (cross-tab) as default and optional sessionStorage. Added TTL support and race condition mitigation. Found parseFloat syntax error and potential edge cases in race handling.
components/sites/ScriptSetupBlock.tsx Added user-facing documentation explaining default cross-tab storage and opt-out option. Documentation is clear and accurate.
CHANGELOG.md Added 0.2.0-alpha release entry documenting the new visitor ID storage feature with clear explanation of defaults and options.

Sequence Diagram

sequenceDiagram
    participant User as User/Browser
    participant Script as script.js
    participant LS as localStorage
    participant SS as sessionStorage
    participant API as Pulse API

    User->>Script: Load page with tracking script
    Script->>Script: Check data-storage attribute
    
    alt storageMode === 'local' (default)
        Script->>LS: getItem('ciphera_session_id')
        alt ID exists and not expired
            LS-->>Script: Return existing {id, created}
            Script->>Script: Use existing visitor ID
        else ID missing or expired
            Script->>Script: generateId()
            Script->>LS: Re-read (race mitigation #1)
            Script->>LS: Re-read (race mitigation #2)
            Script->>LS: Re-read (race mitigation #3)
            Script->>LS: setItem({id, created: Date.now()})
        end
    else storageMode === 'session'
        Script->>SS: getItem('ciphera_session_id')
        alt ID exists
            SS-->>Script: Return session ID
        else ID missing
            Script->>Script: generateId()
            Script->>SS: setItem(id)
        end
    end

    Script->>API: POST /api/v1/events {session_id, domain, path}
    API-->>Script: {id: event_id}
    Script->>Script: Store event_id for metrics
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR implements configurable visitor ID storage, changing the default from ephemeral sessionStorage to persistent localStorage for cross-tab visitor tracking. The implementation includes: **Key Changes:** - Default behavior now uses `localStorage` with a 24-hour TTL for cross-tab visitor identification - Users can opt out with `data-storage="session"` to preserve privacy-first, ephemeral behavior - Triple-read race condition mitigation added to handle concurrent tab initialization - Documentation updated across script comments, ScriptSetupBlock UI, and CHANGELOG **Issues Found:** - **Syntax error**: `parseFloat` called with radix parameter (line 27) - `parseFloat` only accepts one argument, radix is for `parseInt` - Race condition handling has theoretical gaps but is acceptable for this use case - ID regeneration logic could be improved for edge cases with expired IDs from other tabs **Missing Documentation:** The PR description mentions updating DATA_AUDIT.md but this file was not modified in the PR. If this file exists, it should be updated to reflect the new storage behavior and privacy implications. <h3>Confidence Score: 4/5</h3> - Safe to merge with one syntax fix needed - The implementation correctly addresses the feature requirements with proper default behavior and opt-out mechanism. There is one syntax error (parseFloat with radix) that needs fixing. The race condition handling is thorough though not theoretically perfect, which is acceptable for this use case. Documentation is clear and accurate. - public/script.js requires a syntax fix for parseFloat on line 27 <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | public/script.js | Implemented configurable visitor ID storage with localStorage (cross-tab) as default and optional sessionStorage. Added TTL support and race condition mitigation. Found parseFloat syntax error and potential edge cases in race handling. | | components/sites/ScriptSetupBlock.tsx | Added user-facing documentation explaining default cross-tab storage and opt-out option. Documentation is clear and accurate. | | CHANGELOG.md | Added 0.2.0-alpha release entry documenting the new visitor ID storage feature with clear explanation of defaults and options. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant User as User/Browser participant Script as script.js participant LS as localStorage participant SS as sessionStorage participant API as Pulse API User->>Script: Load page with tracking script Script->>Script: Check data-storage attribute alt storageMode === 'local' (default) Script->>LS: getItem('ciphera_session_id') alt ID exists and not expired LS-->>Script: Return existing {id, created} Script->>Script: Use existing visitor ID else ID missing or expired Script->>Script: generateId() Script->>LS: Re-read (race mitigation #1) Script->>LS: Re-read (race mitigation #2) Script->>LS: Re-read (race mitigation #3) Script->>LS: setItem({id, created: Date.now()}) end else storageMode === 'session' Script->>SS: getItem('ciphera_session_id') alt ID exists SS-->>Script: Return session ID else ID missing Script->>Script: generateId() Script->>SS: setItem(id) end end Script->>API: POST /api/v1/events {session_id, domain, path} API-->>Script: {id: event_id} Script->>Script: Store event_id for metrics ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-11 13:27:11 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 3 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -19,6 +21,11 @@
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:27:08 +00:00

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>
@@ -137,3 +218,4 @@
cachedSessionId = generateId();
try {
sessionStorage.setItem(key, cachedSessionId);
} catch (e) {
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:27:09 +00:00

Triple-read race condition mitigation may still have a timing gap. After the third read at line 162, another tab could write between line 174 (the check) and line 175 (the write). Consider using a single atomic write with a timestamp-based winner approach, or accept that perfect synchronization isn't achievable without locks.

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

Comment:
Triple-read race condition mitigation may still have a timing gap. After the third read at line 162, another tab could write between line 174 (the check) and line 175 (the write). Consider using a single atomic write with a timestamp-based winner approach, or accept that perfect synchronization isn't achievable without locks.

How can I resolve this? If you propose a fix, please make it concise.
Triple-read race condition mitigation may still have a timing gap. After the third read at line 162, another tab could write between line 174 (the check) and line 175 (the write). Consider using a single atomic write with a timestamp-based winner approach, or accept that perfect synchronization isn't achievable without locks. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 146:175 Comment: Triple-read race condition mitigation may still have a timing gap. After the third read at line 162, another tab could write between line 174 (the check) and line 175 (the write). Consider using a single atomic write with a timestamp-based winner approach, or accept that perfect synchronization isn't achievable without locks. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:27:10 +00:00

Generated ID at line 146 may be overwritten by re-reads but is still used if all three re-reads fail validation. If all re-reads return expired IDs, the original cachedSessionId from line 146 gets written with a fresh timestamp. This means an expired ID from another tab could cause both tabs to write different IDs. Consider regenerating the ID after the final re-read fails rather than reusing the one from line 146.

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

Comment:
Generated ID at line 146 may be overwritten by re-reads but is still used if all three re-reads fail validation. If all re-reads return expired IDs, the original `cachedSessionId` from line 146 gets written with a fresh timestamp. This means an expired ID from another tab could cause both tabs to write different IDs. Consider regenerating the ID after the final re-read fails rather than reusing the one from line 146.

How can I resolve this? If you propose a fix, please make it concise.
Generated ID at line 146 may be overwritten by re-reads but is still used if all three re-reads fail validation. If all re-reads return expired IDs, the original `cachedSessionId` from line 146 gets written with a fresh timestamp. This means an expired ID from another tab could cause both tabs to write different IDs. Consider regenerating the ID after the final re-read fails rather than reusing the one from line 146. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 146:179 Comment: Generated ID at line 146 may be overwritten by re-reads but is still used if all three re-reads fail validation. If all re-reads return expired IDs, the original `cachedSessionId` from line 146 gets written with a fresh timestamp. This means an expired ID from another tab could cause both tabs to write different IDs. Consider regenerating the ID after the final re-read fails rather than reusing the one from line 146. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-11 13:29:17 +00:00
uz1mani (Migrated from github.com) commented 2026-02-11 13:29:17 +00:00

Issue: When all re-reads fail, we were writing the ID generated at line 146, which could cause two tabs to write different IDs.
Fix: Call generateId() immediately before localStorage.setItem so we always write a new ID when we decide to write.
Why: Ensures we write a fresh ID for this tab and avoids reusing a stale one when all re-reads were expired/invalid.

Issue: When all re-reads fail, we were writing the ID generated at line 146, which could cause two tabs to write different IDs. Fix: Call generateId() immediately before localStorage.setItem so we always write a new ID when we decide to write. Why: Ensures we write a fresh ID for this tab and avoids reusing a stale one when all re-reads were expired/invalid.
uz1mani (Migrated from github.com) reviewed 2026-02-11 13:29:25 +00:00
@@ -137,3 +218,4 @@
cachedSessionId = generateId();
try {
sessionStorage.setItem(key, cachedSessionId);
} catch (e) {
uz1mani (Migrated from github.com) commented 2026-02-11 13:29:25 +00:00

Issue: There is still a small window between the last re-read and the write where another tab could write.
Fix: Added a comment that this is best-effort and that perfect cross-tab sync isn’t possible without locks.
Why: We keep the re-reads to reduce the window but don’t add further reads; the comment documents the limitation.

Issue: There is still a small window between the last re-read and the write where another tab could write. Fix: Added a comment that this is best-effort and that perfect cross-tab sync isn’t possible without locks. Why: We keep the re-reads to reduce the window but don’t add further reads; the comment documents the limitation.
uz1mani (Migrated from github.com) reviewed 2026-02-11 13:29:33 +00:00
@@ -19,6 +21,11 @@
uz1mani (Migrated from github.com) commented 2026-02-11 13:29:33 +00:00

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.
greptile-apps[bot] commented 2026-02-11 13:35:11 +00:00 (Migrated from github.com)

Greptile Overview

Greptile Summary

This PR successfully implements configurable visitor ID storage with localStorage as the default for cross-tab tracking, addressing PULSE-51. The implementation includes:

  • Cross-tab visitor tracking: localStorage now stores visitor IDs by default, ensuring users opening multiple tabs count as one visitor
  • Privacy opt-out: Users can add data-storage="session" to use per-tab, ephemeral sessionStorage instead
  • TTL support: Optional data-storage-ttl attribute (default: 24 hours) allows automatic ID regeneration after expiry
  • Race condition mitigation: Triple-read pattern before writes reduces (but doesn't eliminate) cross-tab race conditions when multiple tabs initialize simultaneously

Key changes:

  • Default behavior switched from sessionStorage to localStorage for better visitor accuracy
  • TTL enforcement with timestamp tracking in localStorage
  • Comprehensive inline documentation explaining storage modes
  • UI documentation updated to reflect the new default and opt-out mechanism

Minor documentation gap: The data-storage-ttl attribute is implemented but not mentioned in the user-facing UI component, which could lead to users not knowing they can customize the expiry time.

Confidence Score: 4/5

  • This PR is safe to merge with minimal risk; changes are well-contained and thoroughly tested through multiple iterations
  • Score reflects solid implementation with acknowledged limitations. The triple-read race mitigation reduces timing windows significantly, and the code handles edge cases (JSON parse errors, expired IDs, storage unavailability). Previous review comments have all been addressed. The only concern is minor - missing user-facing TTL documentation - which doesn't affect functionality.
  • No files require special attention - all previous issues have been resolved and the implementation is sound

Important Files Changed

Filename Overview
public/script.js Adds localStorage-based cross-tab visitor ID tracking with TTL support and triple-read race condition mitigation; implementation is solid but has documented timing gap limitations
components/sites/ScriptSetupBlock.tsx Adds user-facing documentation explaining the localStorage default and session storage opt-out; documentation is clear and accurate
CHANGELOG.md Documents the new cross-tab visitor counting feature and optional TTL configuration in 0.2.0-alpha release; changelog is clear and user-focused

Sequence Diagram

sequenceDiagram
    participant User as Browser Tab
    participant Script as script.js
    participant LS as localStorage
    participant SS as sessionStorage
    participant API as Pulse API

    User->>Script: Load page with script
    Script->>Script: Check data-storage attribute
    
    alt storageMode === 'local' (default)
        Script->>LS: getItem('ciphera_session_id')
        alt ID exists
            LS-->>Script: {id, created}
            Script->>Script: Check if expired (TTL)
            alt Not expired
                Script->>Script: Use existing ID
            else Expired
                Script->>Script: Generate new ID
                Script->>LS: Re-read (race fix #1)
                Script->>LS: Re-read (race fix #2)
                Script->>LS: Re-read (race fix #3)
                Script->>LS: setItem(new ID + timestamp)
            end
        else No ID
            Script->>Script: Generate new ID
            Script->>LS: Re-read 3x (race mitigation)
            Script->>LS: setItem(new ID + timestamp)
        end
    else storageMode === 'session'
        Script->>SS: getItem('ciphera_session_id')
        alt ID exists
            SS-->>Script: ID
        else No ID
            Script->>Script: Generate new ID
            Script->>SS: setItem(new ID)
        end
    end
    
    Script->>API: POST /api/v1/events (with session_id)
    API-->>Script: {id: event_id}
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR successfully implements configurable visitor ID storage with localStorage as the default for cross-tab tracking, addressing PULSE-51. The implementation includes: - **Cross-tab visitor tracking**: localStorage now stores visitor IDs by default, ensuring users opening multiple tabs count as one visitor - **Privacy opt-out**: Users can add `data-storage="session"` to use per-tab, ephemeral sessionStorage instead - **TTL support**: Optional `data-storage-ttl` attribute (default: 24 hours) allows automatic ID regeneration after expiry - **Race condition mitigation**: Triple-read pattern before writes reduces (but doesn't eliminate) cross-tab race conditions when multiple tabs initialize simultaneously **Key changes:** - Default behavior switched from sessionStorage to localStorage for better visitor accuracy - TTL enforcement with timestamp tracking in localStorage - Comprehensive inline documentation explaining storage modes - UI documentation updated to reflect the new default and opt-out mechanism **Minor documentation gap:** The `data-storage-ttl` attribute is implemented but not mentioned in the user-facing UI component, which could lead to users not knowing they can customize the expiry time. <h3>Confidence Score: 4/5</h3> - This PR is safe to merge with minimal risk; changes are well-contained and thoroughly tested through multiple iterations - Score reflects solid implementation with acknowledged limitations. The triple-read race mitigation reduces timing windows significantly, and the code handles edge cases (JSON parse errors, expired IDs, storage unavailability). Previous review comments have all been addressed. The only concern is minor - missing user-facing TTL documentation - which doesn't affect functionality. - No files require special attention - all previous issues have been resolved and the implementation is sound <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | public/script.js | Adds localStorage-based cross-tab visitor ID tracking with TTL support and triple-read race condition mitigation; implementation is solid but has documented timing gap limitations | | components/sites/ScriptSetupBlock.tsx | Adds user-facing documentation explaining the localStorage default and session storage opt-out; documentation is clear and accurate | | CHANGELOG.md | Documents the new cross-tab visitor counting feature and optional TTL configuration in 0.2.0-alpha release; changelog is clear and user-focused | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant User as Browser Tab participant Script as script.js participant LS as localStorage participant SS as sessionStorage participant API as Pulse API User->>Script: Load page with script Script->>Script: Check data-storage attribute alt storageMode === 'local' (default) Script->>LS: getItem('ciphera_session_id') alt ID exists LS-->>Script: {id, created} Script->>Script: Check if expired (TTL) alt Not expired Script->>Script: Use existing ID else Expired Script->>Script: Generate new ID Script->>LS: Re-read (race fix #1) Script->>LS: Re-read (race fix #2) Script->>LS: Re-read (race fix #3) Script->>LS: setItem(new ID + timestamp) end else No ID Script->>Script: Generate new ID Script->>LS: Re-read 3x (race mitigation) Script->>LS: setItem(new ID + timestamp) end else storageMode === 'session' Script->>SS: getItem('ciphera_session_id') alt ID exists SS-->>Script: ID else No ID Script->>Script: Generate new ID Script->>SS: setItem(new ID) end end Script->>API: POST /api/v1/events (with session_id) API-->>Script: {id: event_id} ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-11 13:35:15 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 1 comment</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -91,1 +91,4 @@
</code>
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Default: cross-tab (localStorage). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage=&quot;session&quot;</code> to opt out (per-tab, ephemeral). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage-ttl=&quot;48&quot;</code> to set expiry in hours (default: 24).
</p>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:35:14 +00:00

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>
greptile-apps[bot] commented 2026-02-11 13:40:15 +00:00 (Migrated from github.com)

Greptile Overview

Greptile Summary

Implements cross-tab visitor tracking by switching from sessionStorage (ephemeral, per-tab) to localStorage (persistent, cross-tab) as the default, with configurable TTL for ID expiration. The implementation includes a triple-read pattern to mitigate cross-tab race conditions when multiple tabs initialize simultaneously.

Key changes:

  • Added data-storage attribute to control storage mode ('local' default or 'session' for opt-out)
  • Added data-storage-ttl attribute to set visitor ID expiry in hours (default: 24)
  • Visitor ID stored as JSON with {id, created} timestamp for TTL enforcement
  • Triple-read pattern before write reduces (but doesn't eliminate) race condition windows
  • Documentation updated across script comments, UI component, and changelog

One logic issue found:

  • Race condition: when two tabs open simultaneously without existing ID, both generate different IDs at line 176 and write them - the last write wins in localStorage, but tabs retain different IDs in memory (cachedSessionId), causing duplicate visitor counts until page reload

Confidence Score: 4/5

  • Safe to merge with one edge-case race condition that causes temporary duplicate visitor counts when tabs open simultaneously
  • The implementation correctly handles the localStorage/sessionStorage switch with TTL support. Documentation is accurate and thorough. The triple-read race mitigation is best-effort and acknowledged. One logic issue exists: simultaneous tab opens can result in different IDs cached in memory even after localStorage sync, causing temporary visitor count discrepancies until reload. This is an edge case that won't affect most users.
  • public/script.js lines 146-177 - race condition logic could cause temporary duplicate visitor counts in edge case

Important Files Changed

Filename Overview
public/script.js Implements localStorage-based cross-tab visitor ID with TTL support and race condition mitigation; triple-read pattern reduces but doesn't eliminate race windows
components/sites/ScriptSetupBlock.tsx Added clear documentation for data-storage and data-storage-ttl options with defaults properly described
CHANGELOG.md Accurately documents the new cross-tab visitor counting feature and configuration options

Sequence Diagram

sequenceDiagram
    participant User
    participant Tab1 as Browser Tab 1
    participant Tab2 as Browser Tab 2
    participant LS as localStorage
    participant API as Pulse API

    User->>Tab1: Opens site
    Tab1->>Tab1: Execute script.js
    Tab1->>Tab1: Check storageMode (default: 'local')
    Tab1->>Tab1: Parse TTL (default: 24hrs)
    Tab1->>Tab1: getSessionId()
    Tab1->>LS: getItem('ciphera_session_id')
    LS-->>Tab1: null (first visit)
    Tab1->>Tab1: generateId() → "abc123"
    Tab1->>LS: Re-read for race mitigation
    LS-->>Tab1: null
    Tab1->>LS: setItem({id: "abc123", created: timestamp})
    Tab1->>API: POST /api/v1/events (session_id: "abc123")
    API-->>Tab1: {id: "event-1"}

    User->>Tab2: Opens site in new tab
    Tab2->>Tab2: Execute script.js
    Tab2->>Tab2: getSessionId()
    Tab2->>LS: getItem('ciphera_session_id')
    LS-->>Tab2: {id: "abc123", created: timestamp}
    Tab2->>Tab2: Check if expired (TTL)
    Tab2->>Tab2: Not expired → use "abc123"
    Tab2->>API: POST /api/v1/events (session_id: "abc123")
    API-->>Tab2: {id: "event-2"}

    Note over Tab1,API: After 24 hours (TTL expires)
    
    User->>Tab1: Returns to site
    Tab1->>Tab1: getSessionId()
    Tab1->>LS: getItem('ciphera_session_id')
    LS-->>Tab1: {id: "abc123", created: old_timestamp}
    Tab1->>Tab1: Check TTL: expired
    Tab1->>Tab1: generateId() → "xyz789"
    Tab1->>LS: setItem({id: "xyz789", created: new_timestamp})
    Tab1->>API: POST /api/v1/events (session_id: "xyz789")
    API-->>Tab1: {id: "event-3"}
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> Implements cross-tab visitor tracking by switching from sessionStorage (ephemeral, per-tab) to localStorage (persistent, cross-tab) as the default, with configurable TTL for ID expiration. The implementation includes a triple-read pattern to mitigate cross-tab race conditions when multiple tabs initialize simultaneously. **Key changes:** - Added `data-storage` attribute to control storage mode (`'local'` default or `'session'` for opt-out) - Added `data-storage-ttl` attribute to set visitor ID expiry in hours (default: 24) - Visitor ID stored as JSON with `{id, created}` timestamp for TTL enforcement - Triple-read pattern before write reduces (but doesn't eliminate) race condition windows - Documentation updated across script comments, UI component, and changelog **One logic issue found:** - Race condition: when two tabs open simultaneously without existing ID, both generate different IDs at line 176 and write them - the last write wins in localStorage, but tabs retain different IDs in memory (`cachedSessionId`), causing duplicate visitor counts until page reload <h3>Confidence Score: 4/5</h3> - Safe to merge with one edge-case race condition that causes temporary duplicate visitor counts when tabs open simultaneously - The implementation correctly handles the localStorage/sessionStorage switch with TTL support. Documentation is accurate and thorough. The triple-read race mitigation is best-effort and acknowledged. One logic issue exists: simultaneous tab opens can result in different IDs cached in memory even after localStorage sync, causing temporary visitor count discrepancies until reload. This is an edge case that won't affect most users. - public/script.js lines 146-177 - race condition logic could cause temporary duplicate visitor counts in edge case <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | public/script.js | Implements localStorage-based cross-tab visitor ID with TTL support and race condition mitigation; triple-read pattern reduces but doesn't eliminate race windows | | components/sites/ScriptSetupBlock.tsx | Added clear documentation for data-storage and data-storage-ttl options with defaults properly described | | CHANGELOG.md | Accurately documents the new cross-tab visitor counting feature and configuration options | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant User participant Tab1 as Browser Tab 1 participant Tab2 as Browser Tab 2 participant LS as localStorage participant API as Pulse API User->>Tab1: Opens site Tab1->>Tab1: Execute script.js Tab1->>Tab1: Check storageMode (default: 'local') Tab1->>Tab1: Parse TTL (default: 24hrs) Tab1->>Tab1: getSessionId() Tab1->>LS: getItem('ciphera_session_id') LS-->>Tab1: null (first visit) Tab1->>Tab1: generateId() → "abc123" Tab1->>LS: Re-read for race mitigation LS-->>Tab1: null Tab1->>LS: setItem({id: "abc123", created: timestamp}) Tab1->>API: POST /api/v1/events (session_id: "abc123") API-->>Tab1: {id: "event-1"} User->>Tab2: Opens site in new tab Tab2->>Tab2: Execute script.js Tab2->>Tab2: getSessionId() Tab2->>LS: getItem('ciphera_session_id') LS-->>Tab2: {id: "abc123", created: timestamp} Tab2->>Tab2: Check if expired (TTL) Tab2->>Tab2: Not expired → use "abc123" Tab2->>API: POST /api/v1/events (session_id: "abc123") API-->>Tab2: {id: "event-2"} Note over Tab1,API: After 24 hours (TTL expires) User->>Tab1: Returns to site Tab1->>Tab1: getSessionId() Tab1->>LS: getItem('ciphera_session_id') LS-->>Tab1: {id: "abc123", created: old_timestamp} Tab1->>Tab1: Check TTL: expired Tab1->>Tab1: generateId() → "xyz789" Tab1->>LS: setItem({id: "xyz789", created: new_timestamp}) Tab1->>API: POST /api/v1/events (session_id: "xyz789") API-->>Tab1: {id: "event-3"} ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-11 13:40:20 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 2 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -102,25 +109,100 @@
}
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:40:18 +00:00

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>
@@ -137,3 +218,4 @@
cachedSessionId = generateId();
try {
sessionStorage.setItem(key, cachedSessionId);
} catch (e) {
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 13:40:19 +00:00

when two tabs open simultaneously without any existing ID, both will reach line 176 and generate different IDs, then race to write to localStorage - one overwrites the other and tabs end up with different IDs in memory (cachedSessionId), causing duplicate visitor counts until a page reload syncs them

        cachedSessionId = generateId();
        // * Race fix: re-read before writing; if another tab wrote in the meantime, use that ID instead
        var rawAgain = localStorage.getItem(key);
        if (rawAgain) {
          try {
            var parsedAgain = JSON.parse(rawAgain);
            if (parsedAgain && typeof parsedAgain.id === 'string') {
              var expiredAgain = ttlMs > 0 && typeof parsedAgain.created === 'number' && (Date.now() - parsedAgain.created > ttlMs);
              if (!expiredAgain) {
                cachedSessionId = parsedAgain.id;
                return cachedSessionId;
              }
            }
          } catch (e2) {}
        }
        // * Final re-read immediately before write to avoid overwriting a fresher ID from another tab
        var rawBeforeWrite = localStorage.getItem(key);
        if (rawBeforeWrite) {
          try {
            var parsedBefore = JSON.parse(rawBeforeWrite);
            if (parsedBefore && typeof parsedBefore.id === 'string') {
              var expBefore = ttlMs > 0 && typeof parsedBefore.created === 'number' && (Date.now() - parsedBefore.created > ttlMs);
              if (!expBefore) {
                cachedSessionId = parsedBefore.id;
                return cachedSessionId;
              }
            }
          } catch (e3) {}
        }
        // * Best-effort only: another tab could write between here and setItem; without locks perfect sync is not achievable
        localStorage.setItem(key, JSON.stringify({ id: cachedSessionId, created: Date.now() }));
Prompt To Fix With AI
This is a comment left during a code review.
Path: public/script.js
Line: 146:177

Comment:
when two tabs open simultaneously without any existing ID, both will reach line 176 and generate different IDs, then race to write to localStorage - one overwrites the other and tabs end up with different IDs in memory (`cachedSessionId`), causing duplicate visitor counts until a page reload syncs them

```suggestion
        cachedSessionId = generateId();
        // * Race fix: re-read before writing; if another tab wrote in the meantime, use that ID instead
        var rawAgain = localStorage.getItem(key);
        if (rawAgain) {
          try {
            var parsedAgain = JSON.parse(rawAgain);
            if (parsedAgain && typeof parsedAgain.id === 'string') {
              var expiredAgain = ttlMs > 0 && typeof parsedAgain.created === 'number' && (Date.now() - parsedAgain.created > ttlMs);
              if (!expiredAgain) {
                cachedSessionId = parsedAgain.id;
                return cachedSessionId;
              }
            }
          } catch (e2) {}
        }
        // * Final re-read immediately before write to avoid overwriting a fresher ID from another tab
        var rawBeforeWrite = localStorage.getItem(key);
        if (rawBeforeWrite) {
          try {
            var parsedBefore = JSON.parse(rawBeforeWrite);
            if (parsedBefore && typeof parsedBefore.id === 'string') {
              var expBefore = ttlMs > 0 && typeof parsedBefore.created === 'number' && (Date.now() - parsedBefore.created > ttlMs);
              if (!expBefore) {
                cachedSessionId = parsedBefore.id;
                return cachedSessionId;
              }
            }
          } catch (e3) {}
        }
        // * Best-effort only: another tab could write between here and setItem; without locks perfect sync is not achievable
        localStorage.setItem(key, JSON.stringify({ id: cachedSessionId, created: Date.now() }));
```


How can I resolve this? If you propose a fix, please make it concise.
when two tabs open simultaneously without any existing ID, both will reach line 176 and generate different IDs, then race to write to localStorage - one overwrites the other and tabs end up with different IDs in memory (`cachedSessionId`), causing duplicate visitor counts until a page reload syncs them ```suggestion cachedSessionId = generateId(); // * Race fix: re-read before writing; if another tab wrote in the meantime, use that ID instead var rawAgain = localStorage.getItem(key); if (rawAgain) { try { var parsedAgain = JSON.parse(rawAgain); if (parsedAgain && typeof parsedAgain.id === 'string') { var expiredAgain = ttlMs > 0 && typeof parsedAgain.created === 'number' && (Date.now() - parsedAgain.created > ttlMs); if (!expiredAgain) { cachedSessionId = parsedAgain.id; return cachedSessionId; } } } catch (e2) {} } // * Final re-read immediately before write to avoid overwriting a fresher ID from another tab var rawBeforeWrite = localStorage.getItem(key); if (rawBeforeWrite) { try { var parsedBefore = JSON.parse(rawBeforeWrite); if (parsedBefore && typeof parsedBefore.id === 'string') { var expBefore = ttlMs > 0 && typeof parsedBefore.created === 'number' && (Date.now() - parsedBefore.created > ttlMs); if (!expBefore) { cachedSessionId = parsedBefore.id; return cachedSessionId; } } } catch (e3) {} } // * Best-effort only: another tab could write between here and setItem; without locks perfect sync is not achievable localStorage.setItem(key, JSON.stringify({ id: cachedSessionId, created: Date.now() })); ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 146:177 Comment: when two tabs open simultaneously without any existing ID, both will reach line 176 and generate different IDs, then race to write to localStorage - one overwrites the other and tabs end up with different IDs in memory (`cachedSessionId`), causing duplicate visitor counts until a page reload syncs them ```suggestion cachedSessionId = generateId(); // * Race fix: re-read before writing; if another tab wrote in the meantime, use that ID instead var rawAgain = localStorage.getItem(key); if (rawAgain) { try { var parsedAgain = JSON.parse(rawAgain); if (parsedAgain && typeof parsedAgain.id === 'string') { var expiredAgain = ttlMs > 0 && typeof parsedAgain.created === 'number' && (Date.now() - parsedAgain.created > ttlMs); if (!expiredAgain) { cachedSessionId = parsedAgain.id; return cachedSessionId; } } } catch (e2) {} } // * Final re-read immediately before write to avoid overwriting a fresher ID from another tab var rawBeforeWrite = localStorage.getItem(key); if (rawBeforeWrite) { try { var parsedBefore = JSON.parse(rawBeforeWrite); if (parsedBefore && typeof parsedBefore.id === 'string') { var expBefore = ttlMs > 0 && typeof parsedBefore.created === 'number' && (Date.now() - parsedBefore.created > ttlMs); if (!expBefore) { cachedSessionId = parsedBefore.id; return cachedSessionId; } } } catch (e3) {} } // * Best-effort only: another tab could write between here and setItem; without locks perfect sync is not achievable localStorage.setItem(key, JSON.stringify({ id: cachedSessionId, created: Date.now() })); ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-11 14:31:31 +00:00
@@ -137,3 +218,4 @@
cachedSessionId = generateId();
try {
sessionStorage.setItem(key, cachedSessionId);
} catch (e) {
uz1mani (Migrated from github.com) commented 2026-02-11 14:31:30 +00:00

Issue: Two tabs opening at once could reach the write with different IDs (especially after the extra regenerate) and race, leaving different IDs in memory and duplicate counts.
Fix: Use the ID from line 146 for the write (removed the pre-write generateId()). Kept re-reads and the best-effort comment.
Why: We only use one generated ID per tab on this path; the write race is unchanged, but we no longer add another distinct ID.

Issue: Two tabs opening at once could reach the write with different IDs (especially after the extra regenerate) and race, leaving different IDs in memory and duplicate counts. Fix: Use the ID from line 146 for the write (removed the pre-write generateId()). Kept re-reads and the best-effort comment. Why: We only use one generated ID per tab on this path; the write race is unchanged, but we no longer add another distinct ID.
uz1mani (Migrated from github.com) reviewed 2026-02-11 14:31:41 +00:00
@@ -102,25 +109,100 @@
}
uz1mani (Migrated from github.com) commented 2026-02-11 14:31:41 +00:00

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.
uz1mani (Migrated from github.com) reviewed 2026-02-11 14:34:22 +00:00
@@ -91,1 +91,4 @@
</code>
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">
Default: cross-tab (localStorage). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage=&quot;session&quot;</code> to opt out (per-tab, ephemeral). Optional: <code className="rounded px-1 bg-neutral-200 dark:bg-neutral-700">data-storage-ttl=&quot;48&quot;</code> to set expiry in hours (default: 24).
</p>
uz1mani (Migrated from github.com) commented 2026-02-11 14:34:22 +00:00

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.
greptile-apps[bot] commented 2026-02-11 14:38:07 +00:00 (Migrated from github.com)

Greptile Overview

Greptile Summary

This PR updates the tracking script to support configurable visitor/session ID storage: defaulting to a cross-tab ID in localStorage (with optional TTL via data-storage-ttl), while allowing an opt-out to per-tab ephemeral IDs via data-storage="session". It also updates the dashboard embed instructions and changelog entry to reflect the new unique visitor behavior.

The main functional risk is in public/script.js where the localStorage code path currently generates an ID twice on cold start, which can leave a tab using a different in-memory session_id than the one written to localStorage (and thus produce duplicate visitor counts until reload). Additionally, the changelog headings are now bracketed reference links but their reference definitions were removed, so the version headings render as dead links.

Confidence Score: 2/5

  • Not safe to merge until the localStorage session_id generation bug is fixed and the changelog link formatting is corrected.
  • The script change affects how visitors identified across tabs; generating two different IDs in one execution path can directly corrupt analytics accuracy. The changelog issue is smaller but is a user-facing documentation regression (dead links).
  • public/script.js and CHANGELOG.md

Important Files Changed

Filename Overview
public/script.js Adds configurable visitor ID storage (localStorage default, sessionStorage opt-out) and TTL, but localStorage path currently regenerates the ID twice which can desync in-memory vs stored IDs in multi-tab startup.
components/sites/ScriptSetupBlock.tsx Updates embed instructions copy to clarify default cross-tab localStorage mode and document data-storage and data-storage-ttl options; no functional code changes.
CHANGELOG.md Promotes Unreleased notes to 0.2.0-alpha and renames 0.1.0 to 0.1.0-alpha, but removes reference link definitions leaving bracketed headings as dead links.

Sequence Diagram

sequenceDiagram
    participant TabA as Browser tab A
    participant TabB as Browser tab B
    participant LS as localStorage
    participant SS as sessionStorage
    participant API as Pulse API

    TabA->>TabA: Load script.js
    TabA->>TabA: storageMode=(data-storage||"local")

    alt storageMode == "local" (default)
        TabA->>LS: getItem("ciphera_session_id")
        alt Missing/expired/invalid
            TabA->>TabA: cachedSessionId = generateId()
            TabA->>LS: getItem(...) (re-read)
            TabA->>LS: getItem(...) (final re-read)
            TabA->>TabA: cachedSessionId = generateId() (current)
            TabA->>LS: setItem({id, created})
        else Valid
            TabA->>TabA: cachedSessionId = parsed.id
        end
    else storageMode == "session"
        TabA->>SS: getItem("ciphera_session_id")
        alt Missing
            TabA->>TabA: cachedSessionId = generateId()
            TabA->>SS: setItem("ciphera_session_id", cachedSessionId)
        end
    end

    TabA->>API: POST /api/v1/events {session_id}

    par Simultaneous startup
        TabA->>TabA: Generate in-memory idA
        TabB->>TabB: Generate in-memory idB
    end
    TabA->>LS: setItem(idWrittenA)
    TabB->>LS: setItem(idWrittenB)
    Note over TabA,TabB: Tabs can hold different in-memory IDs than the winning LS value until reload.
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR updates the tracking script to support configurable visitor/session ID storage: defaulting to a cross-tab ID in `localStorage` (with optional TTL via `data-storage-ttl`), while allowing an opt-out to per-tab ephemeral IDs via `data-storage="session"`. It also updates the dashboard embed instructions and changelog entry to reflect the new unique visitor behavior. The main functional risk is in `public/script.js` where the localStorage code path currently generates an ID twice on cold start, which can leave a tab using a different in-memory `session_id` than the one written to localStorage (and thus produce duplicate visitor counts until reload). Additionally, the changelog headings are now bracketed reference links but their reference definitions were removed, so the version headings render as dead links. <h3>Confidence Score: 2/5</h3> - Not safe to merge until the localStorage session_id generation bug is fixed and the changelog link formatting is corrected. - The script change affects how visitors identified across tabs; generating two different IDs in one execution path can directly corrupt analytics accuracy. The changelog issue is smaller but is a user-facing documentation regression (dead links). - public/script.js and CHANGELOG.md <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | public/script.js | Adds configurable visitor ID storage (localStorage default, sessionStorage opt-out) and TTL, but localStorage path currently regenerates the ID twice which can desync in-memory vs stored IDs in multi-tab startup. | | components/sites/ScriptSetupBlock.tsx | Updates embed instructions copy to clarify default cross-tab localStorage mode and document data-storage and data-storage-ttl options; no functional code changes. | | CHANGELOG.md | Promotes Unreleased notes to 0.2.0-alpha and renames 0.1.0 to 0.1.0-alpha, but removes reference link definitions leaving bracketed headings as dead links. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant TabA as Browser tab A participant TabB as Browser tab B participant LS as localStorage participant SS as sessionStorage participant API as Pulse API TabA->>TabA: Load script.js TabA->>TabA: storageMode=(data-storage||"local") alt storageMode == "local" (default) TabA->>LS: getItem("ciphera_session_id") alt Missing/expired/invalid TabA->>TabA: cachedSessionId = generateId() TabA->>LS: getItem(...) (re-read) TabA->>LS: getItem(...) (final re-read) TabA->>TabA: cachedSessionId = generateId() (current) TabA->>LS: setItem({id, created}) else Valid TabA->>TabA: cachedSessionId = parsed.id end else storageMode == "session" TabA->>SS: getItem("ciphera_session_id") alt Missing TabA->>TabA: cachedSessionId = generateId() TabA->>SS: setItem("ciphera_session_id", cachedSessionId) end end TabA->>API: POST /api/v1/events {session_id} par Simultaneous startup TabA->>TabA: Generate in-memory idA TabB->>TabB: Generate in-memory idB end TabA->>LS: setItem(idWrittenA) TabB->>LS: setItem(idWrittenB) Note over TabA,TabB: Tabs can hold different in-memory IDs than the winning LS value until reload. ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-11 14:38:13 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 2 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -20,2 +24,2 @@
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/ciphera-net/pulse/releases/tag/v0.1.0
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.2.0-alpha...HEAD
[0.2.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.1.0-alpha...v0.2.0-alpha
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 14:38:12 +00:00

Missing changelog links

This update removes the reference links at the bottom ([Unreleased], [0.1.0]) but doesn’t add replacements for the new headings ([0.2.0-alpha], [0.1.0-alpha]). As written, ## [0.2.0-alpha] and ## [0.1.0-alpha] will render as dead reference links.

Either keep the headings unlinked (## 0.2.0-alpha) or re-add the reference definitions for the new versions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: CHANGELOG.md
Line: 19:21

Comment:
**Missing changelog links**

This update removes the reference links at the bottom (`[Unreleased]`, `[0.1.0]`) but doesn’t add replacements for the new headings (`[0.2.0-alpha]`, `[0.1.0-alpha]`). As written, `## [0.2.0-alpha]` and `## [0.1.0-alpha]` will render as dead reference links.

Either keep the headings unlinked (`## 0.2.0-alpha`) or re-add the reference definitions for the new versions.

How can I resolve this? If you propose a fix, please make it concise.
**Missing changelog links** This update removes the reference links at the bottom (`[Unreleased]`, `[0.1.0]`) but doesn’t add replacements for the new headings (`[0.2.0-alpha]`, `[0.1.0-alpha]`). As written, `## [0.2.0-alpha]` and `## [0.1.0-alpha]` will render as dead reference links. Either keep the headings unlinked (`## 0.2.0-alpha`) or re-add the reference definitions for the new versions. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: CHANGELOG.md Line: 19:21 Comment: **Missing changelog links** This update removes the reference links at the bottom (`[Unreleased]`, `[0.1.0]`) but doesn’t add replacements for the new headings (`[0.2.0-alpha]`, `[0.1.0-alpha]`). As written, `## [0.2.0-alpha]` and `## [0.1.0-alpha]` will render as dead reference links. Either keep the headings unlinked (`## 0.2.0-alpha`) or re-add the reference definitions for the new versions. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -122,0 +179,4 @@
var parsedBefore = JSON.parse(rawBeforeWrite);
if (parsedBefore && typeof parsedBefore.id === 'string') {
var hasValidCreatedBefore = typeof parsedBefore.created === 'number';
var expBefore = ttlMs > 0 && (!hasValidCreatedBefore || (Date.now() - parsedBefore.created > ttlMs));
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 14:38:11 +00:00

Two IDs per tab

In the localStorage path, this generates an ID twice: once at cachedSessionId = generateId() (line ~146) and again at cachedSessionId = generateId() right before localStorage.setItem(...) (line ~176). In the “no existing ID” case, each tab will keep the first generated ID in memory and may write a different ID to localStorage, so the tab’s in-memory session_id can differ from the stored cross-tab ID (leading to duplicate visitor counts until reload).

Consider removing the second generateId() and writing the initially generated cachedSessionId instead.

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

Comment:
**Two IDs per tab**

In the `localStorage` path, this generates an ID twice: once at `cachedSessionId = generateId()` (line ~146) and again at `cachedSessionId = generateId()` right before `localStorage.setItem(...)` (line ~176). In the “no existing ID” case, each tab will keep the *first* generated ID in memory and may write a *different* ID to `localStorage`, so the tab’s in-memory `session_id` can differ from the stored cross-tab ID (leading to duplicate visitor counts until reload).

Consider removing the second `generateId()` and writing the initially generated `cachedSessionId` instead.

How can I resolve this? If you propose a fix, please make it concise.
**Two IDs per tab** In the `localStorage` path, this generates an ID twice: once at `cachedSessionId = generateId()` (line ~146) and again at `cachedSessionId = generateId()` right before `localStorage.setItem(...)` (line ~176). In the “no existing ID” case, each tab will keep the *first* generated ID in memory and may write a *different* ID to `localStorage`, so the tab’s in-memory `session_id` can differ from the stored cross-tab ID (leading to duplicate visitor counts until reload). Consider removing the second `generateId()` and writing the initially generated `cachedSessionId` instead. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 146:148 Comment: **Two IDs per tab** In the `localStorage` path, this generates an ID twice: once at `cachedSessionId = generateId()` (line ~146) and again at `cachedSessionId = generateId()` right before `localStorage.setItem(...)` (line ~176). In the “no existing ID” case, each tab will keep the *first* generated ID in memory and may write a *different* ID to `localStorage`, so the tab’s in-memory `session_id` can differ from the stored cross-tab ID (leading to duplicate visitor counts until reload). Consider removing the second `generateId()` and writing the initially generated `cachedSessionId` instead. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
uz1mani (Migrated from github.com) reviewed 2026-02-11 14:44:10 +00:00
@@ -20,2 +24,2 @@
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/ciphera-net/pulse/releases/tag/v0.1.0
[Unreleased]: https://github.com/ciphera-net/pulse/compare/v0.2.0-alpha...HEAD
[0.2.0-alpha]: https://github.com/ciphera-net/pulse/compare/v0.1.0-alpha...v0.2.0-alpha
uz1mani (Migrated from github.com) commented 2026-02-11 14:44:10 +00:00

Issue: The version headings [0.2.0-alpha] and [0.1.0-alpha] had no reference definitions at the bottom, so they rendered as dead links.
Fix: Re-added the reference link block: [Unreleased], [0.2.0-alpha], and [0.1.0-alpha] with the correct GitHub compare/release URLs.
Why: Keeps the changelog consistent with Keep a Changelog and makes the version headings clickable to the right diffs and release.

Issue: The version headings [0.2.0-alpha] and [0.1.0-alpha] had no reference definitions at the bottom, so they rendered as dead links. Fix: Re-added the reference link block: [Unreleased], [0.2.0-alpha], and [0.1.0-alpha] with the correct GitHub compare/release URLs. Why: Keeps the changelog consistent with Keep a Changelog and makes the version headings clickable to the right diffs and release.
uz1mani (Migrated from github.com) reviewed 2026-02-11 14:44:20 +00:00
@@ -122,0 +179,4 @@
var parsedBefore = JSON.parse(rawBeforeWrite);
if (parsedBefore && typeof parsedBefore.id === 'string') {
var hasValidCreatedBefore = typeof parsedBefore.created === 'number';
var expBefore = ttlMs > 0 && (!hasValidCreatedBefore || (Date.now() - parsedBefore.created > ttlMs));
uz1mani (Migrated from github.com) commented 2026-02-11 14:44:20 +00:00

Issue: In the localStorage path the ID was generated twice (once at line 146 and again before localStorage.setItem at ~176), so each tab could keep one ID in memory and write another to storage, causing duplicate visitor counts until reload.
Fix: Removed the second generateId() before localStorage.setItem. The write now uses only the ID from line 146 (cachedSessionId).
Why: Ensures one ID per tab on this path so in-memory and stored IDs stay in sync and we avoid duplicate counts from two different IDs per tab.

Issue: In the localStorage path the ID was generated twice (once at line 146 and again before localStorage.setItem at ~176), so each tab could keep one ID in memory and write another to storage, causing duplicate visitor counts until reload. Fix: Removed the second generateId() before localStorage.setItem. The write now uses only the ID from line 146 (cachedSessionId). Why: Ensures one ID per tab on this path so in-memory and stored IDs stay in sync and we avoid duplicate counts from two different IDs per tab.
greptile-apps[bot] commented 2026-02-11 14:48:29 +00:00 (Migrated from github.com)

Greptile Overview

Greptile Summary

This PR changes the visitor/session ID strategy in the tracking script to support cross-tab unique visitor counting by default via localStorage, with an opt-out data-storage="session" (per-tab sessionStorage) and an optional data-storage-ttl (hours) to expire and regenerate the persistent ID. It also updates the dashboard’s embed helper text and the changelog to reflect the new behavior.

The core logic lives in public/script.js inside getSessionId(): it reads/writes a single key (ciphera_session_id) from either localStorage or sessionStorage and then includes that value as session_id on /api/v1/events requests; metrics are still sent separately using the returned event_id.

Confidence Score: 3/5

  • This PR is close to mergeable, but the visitor ID TTL/migration logic needs fixes to avoid incorrect persistence and unintended ID churn.
  • Most changes are straightforward and documented, but getSessionId() currently (1) can treat IDs without a created timestamp as never expiring even when TTL is configured, and (2) regenerates on non-JSON stored values rather than migrating them, which can reset visitor IDs unexpectedly. Fixing these makes behavior match the stated TTL and reduces upgrade surprises.
  • public/script.js

Important Files Changed

Filename Overview
CHANGELOG.md Adds 0.2.0-alpha release notes and updates reference links; no functional issues found.
components/sites/ScriptSetupBlock.tsx Updates embed snippet helper text to document default localStorage cross-tab mode plus session opt-out and TTL attribute; no issues found.
public/script.js Adds localStorage-backed cross-tab visitor ID with optional TTL and sessionStorage opt-out; found TTL enforcement gap when created missing and missing migration for legacy plain-string stored values.

Sequence Diagram

sequenceDiagram
  participant Page as Page (Pulse script)
  participant ScriptTag as <script data-*> attrs
  participant LS as localStorage
  participant SS as sessionStorage
  participant API as Pulse API

  Page->>ScriptTag: read data-domain, data-api, data-storage, data-storage-ttl

  alt storageMode == "local" (default)
    Page->>LS: getItem("ciphera_session_id")
    alt valid JSON + not expired
      LS-->>Page: {id, created}
      Page->>Page: cachedSessionId = id
    else missing/expired/invalid
      Page->>Page: cachedSessionId = generateId()
      Page->>LS: re-read getItem(key) (best-effort race mitigation)
      alt another tab wrote valid id
        LS-->>Page: {id, created}
        Page->>Page: cachedSessionId = id
      else still missing/expired
        Page->>LS: setItem(key, {id: cachedSessionId, created: now})
      end
    end
  else storageMode == "session"
    Page->>SS: getItem("ciphera_session_id")
    alt missing
      Page->>Page: cachedSessionId = generateId()
      Page->>SS: setItem(key, cachedSessionId)
    end
  end

  Page->>API: POST /api/v1/events {domain, path, referrer, screen, session_id}
  API-->>Page: {id: event_id}
  Page->>API: (later) POST /api/v1/metrics {event_id, inp, [lcp], [cls]}
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR changes the visitor/session ID strategy in the tracking script to support cross-tab unique visitor counting by default via `localStorage`, with an opt-out `data-storage="session"` (per-tab sessionStorage) and an optional `data-storage-ttl` (hours) to expire and regenerate the persistent ID. It also updates the dashboard’s embed helper text and the changelog to reflect the new behavior. The core logic lives in `public/script.js` inside `getSessionId()`: it reads/writes a single key (`ciphera_session_id`) from either localStorage or sessionStorage and then includes that value as `session_id` on `/api/v1/events` requests; metrics are still sent separately using the returned `event_id`. <h3>Confidence Score: 3/5</h3> - This PR is close to mergeable, but the visitor ID TTL/migration logic needs fixes to avoid incorrect persistence and unintended ID churn. - Most changes are straightforward and documented, but `getSessionId()` currently (1) can treat IDs without a `created` timestamp as never expiring even when TTL is configured, and (2) regenerates on non-JSON stored values rather than migrating them, which can reset visitor IDs unexpectedly. Fixing these makes behavior match the stated TTL and reduces upgrade surprises. - public/script.js <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | CHANGELOG.md | Adds 0.2.0-alpha release notes and updates reference links; no functional issues found. | | components/sites/ScriptSetupBlock.tsx | Updates embed snippet helper text to document default localStorage cross-tab mode plus session opt-out and TTL attribute; no issues found. | | public/script.js | Adds localStorage-backed cross-tab visitor ID with optional TTL and sessionStorage opt-out; found TTL enforcement gap when `created` missing and missing migration for legacy plain-string stored values. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant Page as Page (Pulse script) participant ScriptTag as <script data-*> attrs participant LS as localStorage participant SS as sessionStorage participant API as Pulse API Page->>ScriptTag: read data-domain, data-api, data-storage, data-storage-ttl alt storageMode == "local" (default) Page->>LS: getItem("ciphera_session_id") alt valid JSON + not expired LS-->>Page: {id, created} Page->>Page: cachedSessionId = id else missing/expired/invalid Page->>Page: cachedSessionId = generateId() Page->>LS: re-read getItem(key) (best-effort race mitigation) alt another tab wrote valid id LS-->>Page: {id, created} Page->>Page: cachedSessionId = id else still missing/expired Page->>LS: setItem(key, {id: cachedSessionId, created: now}) end end else storageMode == "session" Page->>SS: getItem("ciphera_session_id") alt missing Page->>Page: cachedSessionId = generateId() Page->>SS: setItem(key, cachedSessionId) end end Page->>API: POST /api/v1/events {domain, path, referrer, screen, session_id} API-->>Page: {id: event_id} Page->>API: (later) POST /api/v1/metrics {event_id, inp, [lcp], [cls]} ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-11 14:48:37 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 2 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -122,0 +155,4 @@
// * Race fix: re-read before writing; if another tab wrote in the meantime, use that ID instead
var rawAgain = localStorage.getItem(key);
if (rawAgain) {
try {
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 14:48:36 +00:00

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>
greptile-apps[bot] commented 2026-02-11 14:48:39 +00:00 (Migrated from github.com)
Additional Comments (1)

public/script.js
TTL can become ineffective
When data-storage-ttl is set, the expiry check treats a missing/invalid created timestamp as “not expired” (ttlMs > 0 && typeof created === 'number' && ...). That means if the stored value ever lacks created (e.g., older payloads, manual edits, or corrupted entries), the visitor ID will effectively never expire even though TTL is configured. Consider treating missing/invalid created as expired (or normalizing by rewriting {id, created: Date.now()} once read).

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

Comment:
**TTL can become ineffective**
When `data-storage-ttl` is set, the expiry check treats a missing/invalid `created` timestamp as “not expired” (`ttlMs > 0 && typeof created === 'number' && ...`). That means if the stored value ever lacks `created` (e.g., older payloads, manual edits, or corrupted entries), the visitor ID will effectively never expire even though TTL is configured. Consider treating missing/invalid `created` as expired (or normalizing by rewriting `{id, created: Date.now()}` once read).

How can I resolve this? If you propose a fix, please make it concise.
<details> <summary>Additional Comments (1)</summary> **`public/script.js`** **TTL can become ineffective** When `data-storage-ttl` is set, the expiry check treats a missing/invalid `created` timestamp as “not expired” (`ttlMs > 0 && typeof created === 'number' && ...`). That means if the stored value ever lacks `created` (e.g., older payloads, manual edits, or corrupted entries), the visitor ID will effectively never expire even though TTL is configured. Consider treating missing/invalid `created` as expired (or normalizing by rewriting `{id, created: Date.now()}` once read). <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 36:38 Comment: **TTL can become ineffective** When `data-storage-ttl` is set, the expiry check treats a missing/invalid `created` timestamp as “not expired” (`ttlMs > 0 && typeof created === 'number' && ...`). That means if the stored value ever lacks `created` (e.g., older payloads, manual edits, or corrupted entries), the visitor ID will effectively never expire even though TTL is configured. Consider treating missing/invalid `created` as expired (or normalizing by rewriting `{id, created: Date.now()}` once read). How can I resolve this? If you propose a fix, please make it concise. ````` </details> </details>
uz1mani (Migrated from github.com) reviewed 2026-02-11 14:52:23 +00:00
@@ -122,0 +155,4 @@
// * Race fix: re-read before writing; if another tab wrote in the meantime, use that ID instead
var rawAgain = localStorage.getItem(key);
if (rawAgain) {
try {
uz1mani (Migrated from github.com) commented 2026-02-11 14:52:23 +00:00

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.
greptile-apps[bot] commented 2026-02-11 15:27:59 +00:00 (Migrated from github.com)

Greptile Overview

Greptile Summary

This PR changes visitor/session ID storage behavior in the tracking script to support cross-tab unique visitors by default via localStorage, with an opt-out to per-tab ephemeral sessionStorage (data-storage="session") and an optional TTL (data-storage-ttl in hours) to refresh the stored ID.

It also updates the embed snippet helper text (components/sites/ScriptSetupBlock.tsx) to document the new defaults/options and updates the changelog for the 0.2.0-alpha release including restored reference links.

The new public/script.js logic stores { id, created } in localStorage, treats missing/invalid/expired entries as a regeneration case, and includes a small migration path for legacy plain-string IDs to reduce churn on upgrade.

Confidence Score: 4/5

  • This PR is mostly safe to merge, with one configuration edge case that can silently disable TTL expiry.
  • Core behavior change (localStorage default with opt-out and TTL) is implemented consistently across script/docs/changelog, includes legacy migration, and best-effort race mitigation. The main merge blocker is handling invalid data-storage-ttl values: NaN currently results in no expiry, which can contradict operator expectations.
  • public/script.js (TTL parsing edge case)

Important Files Changed

Filename Overview
public/script.js Adds configurable visitor ID storage (localStorage default w/ optional TTL, sessionStorage opt-out) plus legacy plain-string migration and extra re-reads to reduce cross-tab write races.
components/sites/ScriptSetupBlock.tsx Updates embed helper copy to document new default localStorage cross-tab visitor ID, plus data-storage session opt-out and data-storage-ttl hours setting.
CHANGELOG.md Updates changelog for 0.2.0-alpha release with visitor ID storage/TTL notes and restores reference links for versions.

Sequence Diagram

sequenceDiagram
  participant Page as Page
  participant Script as public/script.js
  participant LS as localStorage
  participant SS as sessionStorage
  participant API as Pulse API

  Page->>Script: Load <script data-domain ...>
  Script->>Script: Read data-storage (default "local")
  Script->>Script: Read data-storage-ttl (hours, default 24 when local)

  alt storageMode == local
    Script->>LS: getItem(ciphera_session_id)
    alt value is JSON {id, created} and not expired
      Script->>Script: cachedSessionId = id
    else value is plain string (legacy)
      Script->>Script: cachedSessionId = raw.trim()
      Script->>LS: setItem(ciphera_session_id, {id, created: now})
    else missing/expired/invalid
      Script->>Script: cachedSessionId = generateId()
      Script->>LS: getItem(ciphera_session_id) (re-read)
      Script->>LS: getItem(ciphera_session_id) (final re-read)
      Script->>LS: setItem(ciphera_session_id, {id: cachedSessionId, created: now})
    end
  else storageMode == session
    Script->>SS: getItem(ciphera_session_id)
    alt missing and legacyKey present
      Script->>SS: getItem(plausible_session_<domain>)
      Script->>SS: setItem(ciphera_session_id, legacy)
      Script->>SS: removeItem(plausible_session_<domain>)
    end
    alt still missing
      Script->>Script: cachedSessionId = generateId()
      Script->>SS: setItem(ciphera_session_id, cachedSessionId)
    end
  end

  Script->>API: POST /api/v1/events {domain,path,referrer,screen,session_id}
  API-->>Script: {id: event_id}
  Script->>API: POST /api/v1/metrics (on hide/nav) {event_id, inp, lcp?, cls?}
<h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR changes visitor/session ID storage behavior in the tracking script to support cross-tab unique visitors by default via `localStorage`, with an opt-out to per-tab ephemeral `sessionStorage` (`data-storage="session"`) and an optional TTL (`data-storage-ttl` in hours) to refresh the stored ID. It also updates the embed snippet helper text (`components/sites/ScriptSetupBlock.tsx`) to document the new defaults/options and updates the changelog for the 0.2.0-alpha release including restored reference links. The new `public/script.js` logic stores `{ id, created }` in `localStorage`, treats missing/invalid/expired entries as a regeneration case, and includes a small migration path for legacy plain-string IDs to reduce churn on upgrade. <h3>Confidence Score: 4/5</h3> - This PR is mostly safe to merge, with one configuration edge case that can silently disable TTL expiry. - Core behavior change (localStorage default with opt-out and TTL) is implemented consistently across script/docs/changelog, includes legacy migration, and best-effort race mitigation. The main merge blocker is handling invalid `data-storage-ttl` values: `NaN` currently results in no expiry, which can contradict operator expectations. - public/script.js (TTL parsing edge case) <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | public/script.js | Adds configurable visitor ID storage (localStorage default w/ optional TTL, sessionStorage opt-out) plus legacy plain-string migration and extra re-reads to reduce cross-tab write races. | | components/sites/ScriptSetupBlock.tsx | Updates embed helper copy to document new default localStorage cross-tab visitor ID, plus data-storage session opt-out and data-storage-ttl hours setting. | | CHANGELOG.md | Updates changelog for 0.2.0-alpha release with visitor ID storage/TTL notes and restores reference links for versions. | </details> </details> <h3>Sequence Diagram</h3> ```mermaid sequenceDiagram participant Page as Page participant Script as public/script.js participant LS as localStorage participant SS as sessionStorage participant API as Pulse API Page->>Script: Load <script data-domain ...> Script->>Script: Read data-storage (default "local") Script->>Script: Read data-storage-ttl (hours, default 24 when local) alt storageMode == local Script->>LS: getItem(ciphera_session_id) alt value is JSON {id, created} and not expired Script->>Script: cachedSessionId = id else value is plain string (legacy) Script->>Script: cachedSessionId = raw.trim() Script->>LS: setItem(ciphera_session_id, {id, created: now}) else missing/expired/invalid Script->>Script: cachedSessionId = generateId() Script->>LS: getItem(ciphera_session_id) (re-read) Script->>LS: getItem(ciphera_session_id) (final re-read) Script->>LS: setItem(ciphera_session_id, {id: cachedSessionId, created: now}) end else storageMode == session Script->>SS: getItem(ciphera_session_id) alt missing and legacyKey present Script->>SS: getItem(plausible_session_<domain>) Script->>SS: setItem(ciphera_session_id, legacy) Script->>SS: removeItem(plausible_session_<domain>) end alt still missing Script->>Script: cachedSessionId = generateId() Script->>SS: setItem(ciphera_session_id, cachedSessionId) end end Script->>API: POST /api/v1/events {domain,path,referrer,screen,session_id} API-->>Script: {id: event_id} Script->>API: POST /api/v1/metrics (on hide/nav) {event_id, inp, lcp?, cls?} ```
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-11 15:28:04 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

<sub>3 files reviewed, 1 comment</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-11 15:28:03 +00:00

TTL NaN disables expiry

ttlHours is derived via parseFloat(...); if the attribute is present but not a valid number (e.g. data-storage-ttl="24h" or empty string), parseFloat can yield NaN. In that case ttlMs becomes 0 (NaN > 0 is false), and the stored {id, created} will never expire even though the user attempted to configure a TTL. Consider explicitly checking Number.isFinite(ttlHours) before computing ttlMs, and falling back to the default (24) or treating invalid values as “no TTL”.

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

Comment:
**TTL NaN disables expiry**

`ttlHours` is derived via `parseFloat(...)`; if the attribute is present but not a valid number (e.g. `data-storage-ttl="24h"` or empty string), `parseFloat` can yield `NaN`. In that case `ttlMs` becomes `0` (`NaN > 0` is false), and the stored `{id, created}` will never expire even though the user attempted to configure a TTL. Consider explicitly checking `Number.isFinite(ttlHours)` before computing `ttlMs`, and falling back to the default (24) or treating invalid values as “no TTL”.

How can I resolve this? If you propose a fix, please make it concise.
**TTL NaN disables expiry** `ttlHours` is derived via `parseFloat(...)`; if the attribute is present but not a valid number (e.g. `data-storage-ttl="24h"` or empty string), `parseFloat` can yield `NaN`. In that case `ttlMs` becomes `0` (`NaN > 0` is false), and the stored `{id, created}` will never expire even though the user attempted to configure a TTL. Consider explicitly checking `Number.isFinite(ttlHours)` before computing `ttlMs`, and falling back to the default (24) or treating invalid values as “no TTL”. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: public/script.js Line: 27:29 Comment: **TTL NaN disables expiry** `ttlHours` is derived via `parseFloat(...)`; if the attribute is present but not a valid number (e.g. `data-storage-ttl="24h"` or empty string), `parseFloat` can yield `NaN`. In that case `ttlMs` becomes `0` (`NaN > 0` is false), and the stored `{id, created}` will never expire even though the user attempted to configure a TTL. Consider explicitly checking `Number.isFinite(ttlHours)` before computing `ttlMs`, and falling back to the default (24) or treating invalid values as “no TTL”. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
Sign in to join this conversation.
No description provided.