Skip to content

Comments

feat: add persistent storage with localStorage and optional TTL#12

Merged
jackmisner merged 3 commits intomainfrom
feature/persistent-storage-ttl
Feb 13, 2026
Merged

feat: add persistent storage with localStorage and optional TTL#12
jackmisner merged 3 commits intomainfrom
feature/persistent-storage-ttl

Conversation

@jackmisner
Copy link
Owner

@jackmisner jackmisner commented Feb 13, 2026

Summary

🤖 Generated with Nori

  • Add storageType config option to choose between sessionStorage (default) and localStorage for UTM parameter persistence
  • Add optional ttl (time-to-live in ms) for localStorage entries — expired data is auto-cleared on read
  • Store all data in an envelope format { params, iat, eat } for consistency, with backward compatibility for pre-envelope flat format data
  • Add isStorageAvailable(type) generic check, isLocalStorageAvailable(), and deprecate isSessionStorageAvailable() in favor of the generic

Test Plan

  • 363 tests pass (56 storage tests including localStorage backend, envelope format, TTL expiration with fake timers, backward compatibility)
  • Config validation covers storageType and ttl fields
  • React hook correctly forwards storageType and ttl to all storage operations
  • TypeScript compiles cleanly, build succeeds, formatting passes

Share Nori with your team: https://www.npmjs.com/package/nori-ai

Summary by CodeRabbit

  • New Features

    • Storage now supports sessionStorage or localStorage with an optional TTL; stored UTM data uses an envelope format and expired entries are auto-cleared.
    • New StorageType option, exported availability checks for storage backends, and storage-aware read/clear operations.
  • Documentation

    • README and docs updated with storageType/ttl examples, persistence behaviour, availability notes, and React usage guidance.
  • Bug Fixes

    • Backward compatibility preserved for legacy stored data formats.

Support localStorage as an alternative to sessionStorage with configurable
time-to-live for automatic expiration. Data is stored in an envelope format
{ params, iat, eat } for consistency across both backends.
🤖 Generated with [Nori](https://nori.ai)

Co-Authored-By: Nori <contact@tilework.tech>
@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Walkthrough

The pull request expands storage to support both sessionStorage and localStorage with an optional TTL, stores UTM data in an envelope { params, iat, eat } while remaining backward-compatible with the flat format, and exposes/configures storageType and ttl across config, core, react integration, debug tooling and public types/exports.

Changes

Cohort / File(s) Summary
Type definitions
src/types/index.ts
Added `StorageType = 'session'
Configuration defaults & loader
src/config/defaults.ts, src/config/loader.ts, src/config/docs.md
DEFAULT_CONFIG now includes storageType: 'session'; createConfig/mergeConfig propagate storageType and ttl; validateConfig enforces storageType ∈ {'session','local'} and positive finite ttl.
Core storage implementation
src/core/storage.ts, src/core/index.ts, src/core/docs.md
Introduced envelope format { params, iat, eat }, TTL semantics (meaningful for localStorage; sessionStorage uses eat = null), multi-backend support via getStorageBackend, isStorageAvailable, isLocalStorageAvailable, isSessionStorageAvailable; storage APIs updated to accept storageType/ttl and added clearStoredUtmParameters, hasStoredUtmParameters, getRawStoredValue; legacy flat-format reads preserved.
Public exports
src/index.ts, src/core/index.ts
Re-exported isStorageAvailable, isLocalStorageAvailable and StorageType type on main export surface.
React integration & hook
src/react/useUtmTracking.ts, src/react/docs.md
Threaded storageType and ttl from resolved config into all storage calls (initial read, store, clear) and updated docs to reflect forwarding; hook signature unchanged.
Debug / diagnostics
src/debug/index.ts, src/debug/docs.md
Diagnostics now use isStorageAvailable(storageType) and call storage APIs with explicit storageType; raw diagnostics include storage type and storage-specific availability messages.
Documentation & README
README.md, src/docs.md, src/types/docs.md, src/core/docs.md
Docs updated to describe envelope format, session/local storage options, TTL behaviour (lazy expiration), backward compatibility and new config fields.
Tests — setup & mocks
__tests__/setup.ts, __tests__/docs.md
Added global localStorage mock alongside sessionStorage; location mock adjusted; beforeEach resets both storage mocks and location.
Tests — config
__tests__/config/loader.test.ts
New tests for storageType and ttl defaults, overrides, merge behaviour and validation rules.
Tests — core storage
__tests__/core/storage.test.ts
Expanded tests: envelope format read/write, localStorage backend, TTL expiry and lazy clearing, flat-format compatibility, availability checks; added tests assume new public helpers (isLocalStorageAvailable, isStorageAvailable).
Tests — react hook
__tests__/react/useUtmTracking.test.tsx
Added scenarios verifying storageType forwarding (local vs session), TTL envelope behaviour, clearing per backend and parity across storage backends.

Sequence Diagram

sequenceDiagram
    participant App as React App
    participant Hook as useUtmTracking Hook
    participant Config as Config Loader
    participant Storage as Storage Backend (session/local)
    participant Expiry as TTL Check

    App->>Hook: Initialise with config
    Hook->>Config: Resolve storageType & ttl
    Config-->>Hook: Return resolved config

    Hook->>Hook: Initial load -> getStoredUtmParameters({storageType})
    Hook->>Storage: getRaw entry via getStorageBackend(storageType)
    Storage->>Storage: Parse stored value

    alt Envelope format
        Storage->>Expiry: Compare eat to now
        Expiry-->>Storage: Expired?
        alt Expired
            Storage->>Storage: Remove entry
            Storage-->>Hook: Return null
        else Valid
            Storage-->>Hook: Return envelope.params
        end
    else Flat (legacy) format
        Storage-->>Hook: Return legacy params (no expiry)
    end

    App->>Hook: Capture UTM params
    Hook->>Storage: storeUtmParameters(params, {storageType, ttl})
    Storage->>Storage: Build envelope {params, iat, eat}
    Storage->>Storage: Persist to selected backend

    App->>Hook: Clear stored params
    Hook->>Storage: clearStoredUtmParameters(storageKey, storageType)
    Storage->>Storage: Remove entry from chosen backend
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarises the main change: adding persistent storage with localStorage and optional TTL. It is concise, specific, and clearly reflects the primary feature introduced in the changeset.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/core/storage.ts (2)

193-200: Consider warning on non-positive TTL values.

The condition ttl > 0 correctly handles zero and negative values by treating them as "no TTL" (i.e., eat = null). However, a negative TTL (e.g., ttl: -1000) might indicate a caller error. Consider adding a dev-time warning for non-positive TTL values to help catch misconfigurations.

💡 Optional: Add validation warning
     // Build envelope — TTL only applies to localStorage
     const now = Date.now()
+    if (typeof ttl === 'number' && ttl <= 0 && typeof console !== 'undefined' && console.warn) {
+      console.warn(`Invalid TTL value (${ttl}ms): TTL must be positive. Storing without expiry.`)
+    }
     const eat = storageType === 'local' && typeof ttl === 'number' && ttl > 0 ? now + ttl : null

317-323: Note: hasStoredUtmParameters may clear expired data.

Calling this function will auto-clear expired data as a side effect (via getStoredUtmParameters). This is reasonable lazy-cleanup behaviour but worth documenting in the JSDoc if callers expect a pure check.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
__tests__/react/useUtmTracking.test.tsx (1)

6-12: ⚠️ Potential issue | 🟡 Minor

Clear localStorage in beforeEach to avoid inter-test leakage.

The new localStorage scenarios can leave state behind, which risks order-dependent failures.

Proposed fix
beforeEach(() => {
  sessionStorage.clear()
+ localStorage.clear()
  vi.stubGlobal('location', {
    href: 'https://example.com',
    search: '',
  })
})
🤖 Fix all issues with AI agents
In `@__tests__/docs.md`:
- Around line 20-23: The markdown strong-emphasis uses asterisks (e.g.,
**`setup.ts`**, **`core/` tests**, **`config/` tests**, **`react/` tests**)
which violates MD050; update each bold instance to use underscores (e.g.,
__`setup.ts`__) in __tests__/docs.md so all bolded tokens use __...__ style
consistently while keeping the same text and backticks.

In `@src/core/storage.ts`:
- Around line 191-194: The TTL truthy check incorrectly ignores ttl = 0; update
the calculation of eat so it treats numeric zero as a valid TTL by checking the
type (e.g., typeof ttl === 'number') rather than truthiness and optionally
validate non-negative values; specifically modify the expression that sets eat
in the storage envelope (refer to storageType, ttl, eat, and StoredUtmEnvelope)
so local storage uses now + ttl when ttl is a number (including 0) and otherwise
sets eat to null.

Comment on lines +20 to +23
- **`setup.ts`**: Creates fresh sessionStorage and localStorage mocks and a location mock in `beforeEach`, ensuring tests are isolated. Both storage mocks implement `getItem`, `setItem`, `removeItem`, `clear`, `length`, and `key`. Location is stubbed with `href`, `search`, `hash`, `pathname`, `protocol`, `host`, and `hostname`.
- **`core/` tests**: Cover capture (URL parsing, allowed parameters, key format conversion, SSR fallback, sanitization integration, PII filtering integration), sanitizer (HTML stripping, control character removal, custom patterns, truncation, combined rules), pii-filter (pattern detection, reject/redact modes, allowlist, callback, disabled patterns, edge cases), storage (write/read/clear, format conversion, validation of stored data, silent failure, localStorage backend, envelope format, TTL expiration with fake timers, backward compatibility with flat format data, `isStorageAvailable`/`isLocalStorageAvailable` availability checks), appender (query/fragment placement, preserveExisting, remove, extract), keys (bidirectional conversion, standard and custom keys, detection, validation), and validator (protocol, domain, normalization, mutable default protocol).
- **`config/` tests**: Cover `createConfig` merging semantics (nullish coalescing, array replacement, object merge, `storageType` and `ttl` merging), `validateConfig` error messages (including `storageType` and `ttl` validation), `loadConfigFromJson` fallback behavior, sanitize config handling (default inclusion, partial merge, custom pattern preservation, validation of each sanitize field), and piiFiltering config handling (default inclusion, partial merge, custom patterns replacement, mode validation).
- **`react/` tests**: Use `@testing-library/react` `renderHook` and `render` to test `useUtmTracking` (auto-capture, manual capture, clear, appendToUrl with share context and exclusions, sanitization, PII filtering, `storageType` forwarding to storage calls) and `UtmProvider`/`useUtmContext` (context propagation, error on missing provider).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align strong-emphasis style with markdownlint (MD050).
The linter expects underscores for bold in this repo; these bullets use asterisks.

Suggested fix
-- **`setup.ts`**: Creates fresh sessionStorage and localStorage mocks and a location mock in `beforeEach`, ensuring tests are isolated. Both storage mocks implement `getItem`, `setItem`, `removeItem`, `clear`, `length`, and `key`. Location is stubbed with `href`, `search`, `hash`, `pathname`, `protocol`, `host`, and `hostname`.
-- **`core/` tests**: Cover capture (URL parsing, allowed parameters, key format conversion, SSR fallback, sanitization integration, PII filtering integration), sanitizer (HTML stripping, control character removal, custom patterns, truncation, combined rules), pii-filter (pattern detection, reject/redact modes, allowlist, callback, disabled patterns, edge cases), storage (write/read/clear, format conversion, validation of stored data, silent failure, localStorage backend, envelope format, TTL expiration with fake timers, backward compatibility with flat format data, `isStorageAvailable`/`isLocalStorageAvailable` availability checks), appender (query/fragment placement, preserveExisting, remove, extract), keys (bidirectional conversion, standard and custom keys, detection, validation), and validator (protocol, domain, normalization, mutable default protocol).
-- **`config/` tests**: Cover `createConfig` merging semantics (nullish coalescing, array replacement, object merge, `storageType` and `ttl` merging), `validateConfig` error messages (including `storageType` and `ttl` validation), `loadConfigFromJson` fallback behavior, sanitize config handling (default inclusion, partial merge, custom pattern preservation, validation of each sanitize field), and piiFiltering config handling (default inclusion, partial merge, custom patterns replacement, mode validation).
-- **`react/` tests**: Use `@testing-library/react` `renderHook` and `render` to test `useUtmTracking` (auto-capture, manual capture, clear, appendToUrl with share context and exclusions, sanitization, PII filtering, `storageType` forwarding to storage calls) and `UtmProvider`/`useUtmContext` (context propagation, error on missing provider).
+- __`setup.ts`__: Creates fresh sessionStorage and localStorage mocks and a location mock in `beforeEach`, ensuring tests are isolated. Both storage mocks implement `getItem`, `setItem`, `removeItem`, `clear`, `length`, and `key`. Location is stubbed with `href`, `search`, `hash`, `pathname`, `protocol`, `host`, and `hostname`.
+- __`core/` tests__: Cover capture (URL parsing, allowed parameters, key format conversion, SSR fallback, sanitization integration, PII filtering integration), sanitizer (HTML stripping, control character removal, custom patterns, truncation, combined rules), pii-filter (pattern detection, reject/redact modes, allowlist, callback, disabled patterns, edge cases), storage (write/read/clear, format conversion, validation of stored data, silent failure, localStorage backend, envelope format, TTL expiration with fake timers, backward compatibility with flat format data, `isStorageAvailable`/`isLocalStorageAvailable` availability checks), appender (query/fragment placement, preserveExisting, remove, extract), keys (bidirectional conversion, standard and custom keys, detection, validation), and validator (protocol, domain, normalization, mutable default protocol).
+- __`config/` tests__: Cover `createConfig` merging semantics (nullish coalescing, array replacement, object merge, `storageType` and `ttl` merging), `validateConfig` error messages (including `storageType` and `ttl` validation), `loadConfigFromJson` fallback behavior, sanitize config handling (default inclusion, partial merge, custom pattern preservation, validation of each sanitize field), and piiFiltering config handling (default inclusion, partial merge, custom patterns replacement, mode validation).
+- __`react/` tests__: Use `@testing-library/react` `renderHook` and `render` to test `useUtmTracking` (auto-capture, manual capture, clear, appendToUrl with share context and exclusions, sanitization, PII filtering, `storageType` forwarding to storage calls) and `UtmProvider`/`useUtmContext` (context propagation, error on missing provider).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **`setup.ts`**: Creates fresh sessionStorage and localStorage mocks and a location mock in `beforeEach`, ensuring tests are isolated. Both storage mocks implement `getItem`, `setItem`, `removeItem`, `clear`, `length`, and `key`. Location is stubbed with `href`, `search`, `hash`, `pathname`, `protocol`, `host`, and `hostname`.
- **`core/` tests**: Cover capture (URL parsing, allowed parameters, key format conversion, SSR fallback, sanitization integration, PII filtering integration), sanitizer (HTML stripping, control character removal, custom patterns, truncation, combined rules), pii-filter (pattern detection, reject/redact modes, allowlist, callback, disabled patterns, edge cases), storage (write/read/clear, format conversion, validation of stored data, silent failure, localStorage backend, envelope format, TTL expiration with fake timers, backward compatibility with flat format data, `isStorageAvailable`/`isLocalStorageAvailable` availability checks), appender (query/fragment placement, preserveExisting, remove, extract), keys (bidirectional conversion, standard and custom keys, detection, validation), and validator (protocol, domain, normalization, mutable default protocol).
- **`config/` tests**: Cover `createConfig` merging semantics (nullish coalescing, array replacement, object merge, `storageType` and `ttl` merging), `validateConfig` error messages (including `storageType` and `ttl` validation), `loadConfigFromJson` fallback behavior, sanitize config handling (default inclusion, partial merge, custom pattern preservation, validation of each sanitize field), and piiFiltering config handling (default inclusion, partial merge, custom patterns replacement, mode validation).
- **`react/` tests**: Use `@testing-library/react` `renderHook` and `render` to test `useUtmTracking` (auto-capture, manual capture, clear, appendToUrl with share context and exclusions, sanitization, PII filtering, `storageType` forwarding to storage calls) and `UtmProvider`/`useUtmContext` (context propagation, error on missing provider).
- __`setup.ts`__: Creates fresh sessionStorage and localStorage mocks and a location mock in `beforeEach`, ensuring tests are isolated. Both storage mocks implement `getItem`, `setItem`, `removeItem`, `clear`, `length`, and `key`. Location is stubbed with `href`, `search`, `hash`, `pathname`, `protocol`, `host`, and `hostname`.
- __`core/` tests__: Cover capture (URL parsing, allowed parameters, key format conversion, SSR fallback, sanitization integration, PII filtering integration), sanitizer (HTML stripping, control character removal, custom patterns, truncation, combined rules), pii-filter (pattern detection, reject/redact modes, allowlist, callback, disabled patterns, edge cases), storage (write/read/clear, format conversion, validation of stored data, silent failure, localStorage backend, envelope format, TTL expiration with fake timers, backward compatibility with flat format data, `isStorageAvailable`/`isLocalStorageAvailable` availability checks), appender (query/fragment placement, preserveExisting, remove, extract), keys (bidirectional conversion, standard and custom keys, detection, validation), and validator (protocol, domain, normalization, mutable default protocol).
- __`config/` tests__: Cover `createConfig` merging semantics (nullish coalescing, array replacement, object merge, `storageType` and `ttl` merging), `validateConfig` error messages (including `storageType` and `ttl` validation), `loadConfigFromJson` fallback behavior, sanitize config handling (default inclusion, partial merge, custom pattern preservation, validation of each sanitize field), and piiFiltering config handling (default inclusion, partial merge, custom patterns replacement, mode validation).
- __`react/` tests__: Use `@testing-library/react` `renderHook` and `render` to test `useUtmTracking` (auto-capture, manual capture, clear, appendToUrl with share context and exclusions, sanitization, PII filtering, `storageType` forwarding to storage calls) and `UtmProvider`/`useUtmContext` (context propagation, error on missing provider).
🧰 Tools
🪛 LanguageTool

[uncategorized] ~20-~20: Loose punctuation mark.
Context: ...## Core Implementation - setup.ts: Creates fresh sessionStorage and localS...

(UNLIKELY_OPENING_PUNCTUATION)


[style] ~23-~23: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...acement, mode validation). - react/ tests: Use @testing-library/react `render...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.20.0)

[warning] 20-20: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)


[warning] 20-20: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)


[warning] 21-21: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)


[warning] 21-21: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)


[warning] 22-22: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)


[warning] 22-22: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)


[warning] 23-23: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)


[warning] 23-23: Strong style
Expected: underscore; Actual: asterisk

(MD050, strong-style)

🤖 Prompt for AI Agents
In `@__tests__/docs.md` around lines 20 - 23, The markdown strong-emphasis uses
asterisks (e.g., **`setup.ts`**, **`core/` tests**, **`config/` tests**,
**`react/` tests**) which violates MD050; update each bold instance to use
underscores (e.g., __`setup.ts`__) in __tests__/docs.md so all bolded tokens use
__...__ style consistently while keeping the same text and backticks.

Comment on lines 191 to 194
// Build envelope — TTL only applies to localStorage
const now = Date.now()
const eat = storageType === 'local' && ttl ? now + ttl : null
const envelope: StoredUtmEnvelope = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle ttl = 0 explicitly to avoid unintended non‑expiry.
The truthy check treats 0 as undefined; if zero is valid for immediate expiry, it will be ignored. Consider an explicit number check or validation.

Suggested fix
-    const eat = storageType === 'local' && ttl ? now + ttl : null
+    const eat = storageType === 'local' && typeof ttl === 'number' ? now + ttl : null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Build envelope — TTL only applies to localStorage
const now = Date.now()
const eat = storageType === 'local' && ttl ? now + ttl : null
const envelope: StoredUtmEnvelope = {
// Build envelope — TTL only applies to localStorage
const now = Date.now()
const eat = storageType === 'local' && typeof ttl === 'number' ? now + ttl : null
const envelope: StoredUtmEnvelope = {
🤖 Prompt for AI Agents
In `@src/core/storage.ts` around lines 191 - 194, The TTL truthy check incorrectly
ignores ttl = 0; update the calculation of eat so it treats numeric zero as a
valid TTL by checking the type (e.g., typeof ttl === 'number') rather than
truthiness and optionally validate non-negative values; specifically modify the
expression that sets eat in the storage envelope (refer to storageType, ttl,
eat, and StoredUtmEnvelope) so local storage uses now + ttl when ttl is a number
(including 0) and otherwise sets eat to null.

🤖 Generated with [Nori](https://nori.ai)

Co-Authored-By: Nori <contact@tilework.tech>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/core/storage.ts`:
- Around line 96-108: The isEnvelopeFormat type guard currently only checks for
params and iat, which lets objects missing or having invalid eat bypass expiry;
update the function (isEnvelopeFormat) to also verify that the 'eat' property
exists on the object and is either a number or null (i.e. typeof obj.eat ===
'number' || obj.eat === null) while keeping the existing checks for params and
iat so StoredUtmEnvelope is correctly validated.

@jackmisner jackmisner merged commit 0048a16 into main Feb 13, 2026
5 checks passed
@jackmisner jackmisner deleted the feature/persistent-storage-ttl branch February 13, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant