feat: add persistent storage with localStorage and optional TTL#12
feat: add persistent storage with localStorage and optional TTL#12jackmisner merged 3 commits intomainfrom
Conversation
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>
WalkthroughThe 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
Sequence DiagramsequenceDiagram
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
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ 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
Comment |
There was a problem hiding this comment.
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 | 🟡 MinorClear 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.
| - **`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). |
There was a problem hiding this comment.
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.
| - **`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.
| // Build envelope — TTL only applies to localStorage | ||
| const now = Date.now() | ||
| const eat = storageType === 'local' && ttl ? now + ttl : null | ||
| const envelope: StoredUtmEnvelope = { |
There was a problem hiding this comment.
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.
| // 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>
There was a problem hiding this comment.
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.
Summary
🤖 Generated with Nori
storageTypeconfig option to choose betweensessionStorage(default) andlocalStoragefor UTM parameter persistencettl(time-to-live in ms) for localStorage entries — expired data is auto-cleared on read{ params, iat, eat }for consistency, with backward compatibility for pre-envelope flat format dataisStorageAvailable(type)generic check,isLocalStorageAvailable(), and deprecateisSessionStorageAvailable()in favor of the genericTest Plan
Share Nori with your team: https://www.npmjs.com/package/nori-ai
Summary by CodeRabbit
New Features
Documentation
Bug Fixes