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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ coverage/

# Config files
tsconfig.json
tsconfig.cjs.json
.gitignore
.prettierrc
.eslintrc*
Expand Down
24 changes: 24 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# CLAUDE.md

## Quick Reference

- **Build**: `npm run build` (tsup, dual ESM/CJS)
- **Test**: `npm test` (vitest + jsdom)
- **Lint**: `npm run lint` (oxlint)
- **Format**: `npm run format` (prettier)
- **Type check**: `npm run type-check`

## Gotchas

- The git remote is named `main`, not `origin`. Use `git push main <branch>`.
- `validator.ts` has module-level mutable state (`defaultProtocol`). Tests that call `setDefaultProtocol()` must restore the original value.
- The React hook freezes config at mount via `useRef` — config prop changes after mount are ignored.
- All URL-facing operations convert keys to `snake_case` internally, regardless of the consumer's `keyFormat` setting.

## Releases

Releases are done via `npm run release:patch|minor|major` which creates a `release/<version>` branch from main. Pushing the tag triggers the publish workflow.

## Test Setup

`__tests__/setup.ts` mocks `sessionStorage` and `window.location` globally before each test. Tests that need specific URLs must override `location.href` and `location.search`.
31 changes: 31 additions & 0 deletions __tests__/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Noridoc: __tests__

Path: @/__tests__

### Overview

- Test suite for the library, mirroring the `@/src` directory structure with subdirectories for `config/`, `core/`, and `react/`.
- Uses vitest with jsdom environment, `@testing-library/react` for React component/hook tests, and a global setup file for browser API mocks.
- Coverage thresholds are enforced at 80% for statements, branches, functions, and lines (configured in `@/vitest.config.ts`).

### How it fits into the larger codebase

- Tests exercise all public API surfaces from `@/src/core`, `@/src/config`, and `@/src/react`.
- Coverage excludes barrel `index.ts` files and the `@/src/types` directory, since these contain only re-exports and type definitions.
- The test setup (`setup.ts`) provides the mock environment that all tests rely on: a sessionStorage mock backed by a plain object with `vi.fn()` wrappers, and a `window.location` mock defaulting to `https://example.com`.
- CI runs tests across Node 18, 20, and 22.

### Core Implementation

- **`setup.ts`**: Creates a fresh sessionStorage mock and location mock in `beforeEach`, ensuring tests are isolated. The storage mock implements `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), storage (write/read/clear, format conversion, validation of stored data, silent failure), 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), `validateConfig` error messages, and `loadConfigFromJson` fallback behavior.
- **`react/` tests**: Use `@testing-library/react` `renderHook` and `render` to test `useUtmTracking` (auto-capture, manual capture, clear, appendToUrl with share context and exclusions) and `UtmProvider`/`useUtmContext` (context propagation, error on missing provider).

### Things to Know

- The sessionStorage mock uses `vi.fn()` wrappers, which means tests can assert on call counts and arguments (`sessionStorage.setItem` calls, etc.).
- `window.location` is stubbed globally rather than using JSDOM's location, so tests that need specific URLs must override `location.href` and `location.search` in their setup.
- The `beforeEach` in `setup.ts` resets both mocks, so each test starts with empty storage and a clean `https://example.com` location.
Comment on lines +5 to +29
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 heading hierarchy to avoid MD001 warnings.

Suggested fix
-### Overview
+## Overview
@@
-### How it fits into the larger codebase
+## How it fits into the larger codebase
@@
-### Core Implementation
+## Core Implementation
@@
-### Things to Know
+## Things to Know
🧰 Tools
🪛 LanguageTool

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

(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: ...omJson fallback behavior. - **react/tests**: Use@testing-library/react render...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.20.0)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


[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 5 - 29, The Markdown uses multiple H3
headings (e.g., "Overview", "How it fits into the larger codebase", "Core
Implementation", "Things to Know") which triggers MD001; change the heading
hierarchy so the document starts with a single H1 title and demote those H3s to
H2 (or H3 as appropriate under a new H1), ensuring each subsequent section level
increments correctly (H1 -> H2 -> H3) so headings like "Overview" become H2 and
nested subsections remain H3.


Created and maintained by Nori.
32 changes: 32 additions & 0 deletions src/config/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Noridoc: config

Path: @/src/config

### Overview

- Defines default configuration values and provides utilities for creating, merging, and validating configuration objects.
- Produces `ResolvedUtmConfig` objects (all fields required) from partial `UtmConfig` input, establishing the configuration contract for the rest of the library.
- Re-exported through `@/src/index.ts` and consumed directly by `@/src/react/useUtmTracking.ts` and `@/src/debug`.

### How it fits into the larger codebase

- `createConfig()` is the primary entry point, called by `useUtmTracking` in `@/src/react` to resolve user-provided partial config into a complete `ResolvedUtmConfig`.
- `@/src/debug` imports `getDefaultConfig()` from here as a fallback when no config is provided to diagnostic functions.
- `DEFAULT_CONFIG` and `STANDARD_UTM_PARAMETERS` are the canonical definitions of default behavior (enabled, snake_case, sessionStorage key `utm_parameters`, auto-capture on mount, append to shares, the 6 standard UTM params).
- The config system does not perform side effects -- it is pure data transformation.

### Core Implementation

- `createConfig()` merges a partial user config with defaults using nullish coalescing (`??`) for scalar fields. Array fields (`allowedParameters`, `excludeFromShares`) are replaced wholesale when provided by the user, not merged. Object fields (`defaultParams`, `shareContextParams`) are shallow-merged.
- `mergeConfig()` follows the same semantics but takes a `ResolvedUtmConfig` as the base instead of implicitly using defaults -- useful for layering configurations.
- `loadConfigFromJson()` accepts `unknown` input, validates it is a non-null non-array object, then delegates to `createConfig()`. Invalid input falls back to defaults with a `console.warn`.
- `validateConfig()` performs runtime type checking on each config field and returns an array of error message strings (empty array means valid).
- `getDefaultConfig()` returns a shallow copy of `DEFAULT_CONFIG` with cloned arrays and objects to prevent mutation of the shared constant.

### Things to Know

- Array replacement (not merge) for `allowedParameters` is intentional: if a consumer provides `allowedParameters: ['utm_source']`, they get only that parameter, not the union with defaults. This is a deliberate design choice.
- `STANDARD_UTM_PARAMETERS` is declared `as const` and used both as the default `allowedParameters` value and as the source of truth in tests. It defines the 6 standard UTM params: source, medium, campaign, term, content, id.
- `validateConfig()` and `createConfig()` are independent -- `createConfig()` does not call `validateConfig()`. Validation is opt-in for consumers who want to check config before using it.

Comment on lines +5 to +31
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 heading hierarchy to avoid MD001 warnings.

Suggested fix
-### Overview
+## Overview
@@
-### How it fits into the larger codebase
+## How it fits into the larger codebase
@@
-### Core Implementation
+## Core Implementation
@@
-### Things to Know
+## Things to Know
📝 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
### Overview
- Defines default configuration values and provides utilities for creating, merging, and validating configuration objects.
- Produces `ResolvedUtmConfig` objects (all fields required) from partial `UtmConfig` input, establishing the configuration contract for the rest of the library.
- Re-exported through `@/src/index.ts` and consumed directly by `@/src/react/useUtmTracking.ts` and `@/src/debug`.
### How it fits into the larger codebase
- `createConfig()` is the primary entry point, called by `useUtmTracking` in `@/src/react` to resolve user-provided partial config into a complete `ResolvedUtmConfig`.
- `@/src/debug` imports `getDefaultConfig()` from here as a fallback when no config is provided to diagnostic functions.
- `DEFAULT_CONFIG` and `STANDARD_UTM_PARAMETERS` are the canonical definitions of default behavior (enabled, snake_case, sessionStorage key `utm_parameters`, auto-capture on mount, append to shares, the 6 standard UTM params).
- The config system does not perform side effects -- it is pure data transformation.
### Core Implementation
- `createConfig()` merges a partial user config with defaults using nullish coalescing (`??`) for scalar fields. Array fields (`allowedParameters`, `excludeFromShares`) are replaced wholesale when provided by the user, not merged. Object fields (`defaultParams`, `shareContextParams`) are shallow-merged.
- `mergeConfig()` follows the same semantics but takes a `ResolvedUtmConfig` as the base instead of implicitly using defaults -- useful for layering configurations.
- `loadConfigFromJson()` accepts `unknown` input, validates it is a non-null non-array object, then delegates to `createConfig()`. Invalid input falls back to defaults with a `console.warn`.
- `validateConfig()` performs runtime type checking on each config field and returns an array of error message strings (empty array means valid).
- `getDefaultConfig()` returns a shallow copy of `DEFAULT_CONFIG` with cloned arrays and objects to prevent mutation of the shared constant.
### Things to Know
- Array replacement (not merge) for `allowedParameters` is intentional: if a consumer provides `allowedParameters: ['utm_source']`, they get only that parameter, not the union with defaults. This is a deliberate design choice.
- `STANDARD_UTM_PARAMETERS` is declared `as const` and used both as the default `allowedParameters` value and as the source of truth in tests. It defines the 6 standard UTM params: source, medium, campaign, term, content, id.
- `validateConfig()` and `createConfig()` are independent -- `createConfig()` does not call `validateConfig()`. Validation is opt-in for consumers who want to check config before using it.
## Overview
- Defines default configuration values and provides utilities for creating, merging, and validating configuration objects.
- Produces `ResolvedUtmConfig` objects (all fields required) from partial `UtmConfig` input, establishing the configuration contract for the rest of the library.
- Re-exported through `@/src/index.ts` and consumed directly by `@/src/react/useUtmTracking.ts` and `@/src/debug`.
## How it fits into the larger codebase
- `createConfig()` is the primary entry point, called by `useUtmTracking` in `@/src/react` to resolve user-provided partial config into a complete `ResolvedUtmConfig`.
- `@/src/debug` imports `getDefaultConfig()` from here as a fallback when no config is provided to diagnostic functions.
- `DEFAULT_CONFIG` and `STANDARD_UTM_PARAMETERS` are the canonical definitions of default behavior (enabled, snake_case, sessionStorage key `utm_parameters`, auto-capture on mount, append to shares, the 6 standard UTM params).
- The config system does not perform side effects -- it is pure data transformation.
## Core Implementation
- `createConfig()` merges a partial user config with defaults using nullish coalescing (`??`) for scalar fields. Array fields (`allowedParameters`, `excludeFromShares`) are replaced wholesale when provided by the user, not merged. Object fields (`defaultParams`, `shareContextParams`) are shallow-merged.
- `mergeConfig()` follows the same semantics but takes a `ResolvedUtmConfig` as the base instead of implicitly using defaults -- useful for layering configurations.
- `loadConfigFromJson()` accepts `unknown` input, validates it is a non-null non-array object, then delegates to `createConfig()`. Invalid input falls back to defaults with a `console.warn`.
- `validateConfig()` performs runtime type checking on each config field and returns an array of error message strings (empty array means valid).
- `getDefaultConfig()` returns a shallow copy of `DEFAULT_CONFIG` with cloned arrays and objects to prevent mutation of the shared constant.
## Things to Know
- Array replacement (not merge) for `allowedParameters` is intentional: if a consumer provides `allowedParameters: ['utm_source']`, they get only that parameter, not the union with defaults. This is a deliberate design choice.
- `STANDARD_UTM_PARAMETERS` is declared `as const` and used both as the default `allowedParameters` value and as the source of truth in tests. It defines the 6 standard UTM params: source, medium, campaign, term, content, id.
- `validateConfig()` and `createConfig()` are independent -- `createConfig()` does not call `validateConfig()`. Validation is opt-in for consumers who want to check config before using it.
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
In `@src/config/docs.md` around lines 5 - 31, The doc uses multiple same-level H3
headings which triggers MD001; adjust the heading hierarchy so the top-level
title is H1 and the major sections use H2. Specifically, change "### Overview"
to "# Overview" (or add a top-level title above it) and change the other "###"
headings ("How it fits into the larger codebase", "Core Implementation", "Things
to Know") to "##" so headings increment correctly and avoid MD001 warnings.

Created and maintained by Nori.
59 changes: 59 additions & 0 deletions src/core/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Noridoc: core

Path: @/src/core

### Overview

- Framework-agnostic core logic for capturing UTM parameters from URLs, persisting them in sessionStorage, appending them to outbound URLs, converting between key formats, and validating URLs.
- This is the heart of the library. Every other module (`@/src/react`, `@/src/debug`) builds on top of these utilities.
- All functions are SSR-safe, returning empty/null/unchanged values when browser APIs are unavailable.

### How it fits into the larger codebase

- `@/src/react/useUtmTracking.ts` orchestrates the core modules: it calls `captureUtmParameters` on mount, `storeUtmParameters`/`getStoredUtmParameters` for persistence, `appendUtmParameters` for URL generation, and `convertParams`/`isSnakeCaseUtmKey` for format handling.
- `@/src/debug` imports from `capture` and `storage` to assemble diagnostic snapshots.
- `@/src/index.ts` re-exports everything from this module for direct consumer use without React.
- All modules import types from `@/src/types`.

### Core Implementation

The data flow through the core modules follows this path:

```text
URL string
|
v
[capture.ts] -- parses URL, filters to utm_* keys, applies allowedParameters, converts key format
|
v
UtmParameters object
|
v
[storage.ts] -- serializes to JSON, writes/reads sessionStorage, validates on read
|
v
[appender.ts] -- converts params to snake_case, merges into target URL query/fragment
|
v
URL string with UTM params
```
Comment on lines 5 to 39
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

Fix heading levels and add a code‑fence language.

MD001 and MD040 are both triggered here.

Suggested fix
-### Overview
+## Overview
@@
-### How it fits into the larger codebase
+## How it fits into the larger codebase
@@
-### Core Implementation
+## Core Implementation
@@
-```
+```text
@@
-### Things to Know
+## Things to Know
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


[warning] 22-22: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@src/core/docs.md` around lines 5 - 39, Update the Markdown to fix MD001/MD040
by adjusting heading levels (change any incorrect "### Things to Know" style
headings to top-level section headers like "## Things to Know" to match document
structure) and add a language identifier to the existing code fence (replace the
opening ``` with a language-tagged fence such as ```text) in the code block
under "Core Implementation" so the linter recognizes the fence language.


- **keys.ts**: Bidirectional key conversion between `snake_case` and `camelCase`. Uses lookup tables (`SNAKE_TO_CAMEL`, `CAMEL_TO_SNAKE`) for the 6 standard keys and regex-based conversion for custom keys. `isSnakeCaseUtmKey` checks for `utm_` prefix; `isCamelCaseUtmKey` checks for `utm` followed by an uppercase letter. `detectKeyFormat` scans keys and returns the first format found, defaulting to `snake_case` for empty objects.

- **capture.ts**: `captureUtmParameters()` takes a URL string (defaulting to `window.location.href`), parses it via `new URL()`, iterates `searchParams`, and filters to keys passing `isSnakeCaseUtmKey`. Optionally filters by an `allowedParameters` set and converts output via `convertParams`.

- **storage.ts**: Uses sessionStorage with a configurable key (default: `utm_parameters`). Write operations skip empty param objects and fail silently with `console.warn`. Read operations validate parsed JSON with `isValidStoredData()`, which checks that all keys pass `isUtmKey` and all values are strings or undefined.

- **appender.ts**: `appendUtmParameters()` always converts input params to snake_case before appending to URLs (URL parameters are conventionally snake_case). Supports query string or fragment placement via `AppendOptions.toFragment`. Uses a custom `buildQueryString()` that omits `=` for empty-string values. When adding to query, it also cleans conflicting UTM params from the fragment (and vice versa). `removeUtmParameters()` strips UTM params from both query and fragment. `extractUtmParameters()` pulls UTM params from both locations, with fragment params taking precedence.

- **validator.ts**: `validateUrl()` checks protocol (http/https only), domain (must contain a `.` for TLD), and parsability. `normalizeUrl()` prepends a configurable default protocol (module-level `let defaultProtocol`). `setDefaultProtocol()` mutates this module-level state.

### Things to Know

- **Key invariant**: All URL-facing operations use snake_case keys. The `appender` always converts to snake_case before manipulating URLs, regardless of what format the consumer passes in. This means URLs always contain `utm_source`, never `utmSource`.
- **SSR safety pattern**: Each module that accesses browser APIs (`window`, `sessionStorage`, `URL`, `document`) checks for their existence before use and returns a safe fallback (empty object, null, or unchanged URL). This is consistent across all core modules.
- **Silent failure**: Storage and capture operations never throw. Errors produce `console.warn` messages and return fallback values. The appender returns the original URL unchanged on failure.
- **validator.ts mutable state**: `defaultProtocol` is module-level mutable state modified via `setDefaultProtocol()`. This is global -- all callers share the same default protocol. Tests that call `setDefaultProtocol()` should restore the original value.
- **Fragment parameter handling in appender**: When appending to query, conflicting UTM params are removed from the fragment. When appending to fragment, conflicting UTM params are removed from the query. Only fragments that contain `=` are treated as parameter-bearing; plain anchors like `#section` are left alone.

Created and maintained by Nori.
31 changes: 31 additions & 0 deletions src/debug/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Noridoc: debug

Path: @/src/debug

### Overview

- Development-time diagnostic utilities for inspecting UTM tracking state (config, URL params, stored params, storage availability).
- Provides console-formatted output, status check messages, and an opt-in `window.utmDebug` helper object for browser console access.
- Not part of the production data flow; intended for troubleshooting.

### How it fits into the larger codebase

- Imports `captureUtmParameters` from `@/src/core/capture`, `getStoredUtmParameters`/`isSessionStorageAvailable`/`getRawStoredValue` from `@/src/core/storage`, and `getDefaultConfig` from `@/src/config/defaults`.
- Re-exported through `@/src/index.ts` so consumers can call these functions directly.
- Does not depend on or interact with `@/src/react` -- it operates on the core layer only.
- All functions accept an optional `ResolvedUtmConfig`; when omitted, they fall back to `getDefaultConfig()`.

### Core Implementation

- `getDiagnostics()` assembles a `DiagnosticInfo` snapshot: resolves config, captures URL params via `captureUtmParameters`, reads stored params via `getStoredUtmParameters`, and checks `isSessionStorageAvailable()`. SSR-safe (returns empty URL and empty params when `window` is unavailable).
- `debugUtmState()` calls `getDiagnostics()` and formats output using `console.group`/`console.table` for structured browser console display.
- `checkUtmTracking()` calls `getDiagnostics()` and returns an array of status strings with emoji prefixes indicating state (e.g., whether params are in the URL, in storage, or if there is a mismatch suggesting the hook has not initialized yet).
- `installDebugHelpers()` checks for `?debug_utm=true` in the URL query string. If present, it attaches a `window.utmDebug` object with `state()`, `check()`, `diagnostics()`, and `raw()` methods. Only activates in browser environments.

### Things to Know

- `installDebugHelpers()` is gated solely by the `debug_utm=true` URL parameter. It does not check `process.env` or `import.meta.env.DEV`.
- The `window.utmDebug` object is attached via a cast to `Record<string, unknown>` to avoid TypeScript errors on the global augmentation.
- `checkUtmTracking()` detects a potential timing issue: when URL params exist but storage is empty and `captureOnMount` is enabled, it warns that the hook may not have initialized yet.

Comment on lines +5 to +30
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 heading hierarchy to avoid MD001 warnings.

The file jumps from H1 to H3. Please use H2 for the section headings.

Suggested fix
-### Overview
+## Overview
@@
-### How it fits into the larger codebase
+## How it fits into the larger codebase
@@
-### Core Implementation
+## Core Implementation
@@
-### Things to Know
+## Things to Know
📝 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
### Overview
- Development-time diagnostic utilities for inspecting UTM tracking state (config, URL params, stored params, storage availability).
- Provides console-formatted output, status check messages, and an opt-in `window.utmDebug` helper object for browser console access.
- Not part of the production data flow; intended for troubleshooting.
### How it fits into the larger codebase
- Imports `captureUtmParameters` from `@/src/core/capture`, `getStoredUtmParameters`/`isSessionStorageAvailable`/`getRawStoredValue` from `@/src/core/storage`, and `getDefaultConfig` from `@/src/config/defaults`.
- Re-exported through `@/src/index.ts` so consumers can call these functions directly.
- Does not depend on or interact with `@/src/react` -- it operates on the core layer only.
- All functions accept an optional `ResolvedUtmConfig`; when omitted, they fall back to `getDefaultConfig()`.
### Core Implementation
- `getDiagnostics()` assembles a `DiagnosticInfo` snapshot: resolves config, captures URL params via `captureUtmParameters`, reads stored params via `getStoredUtmParameters`, and checks `isSessionStorageAvailable()`. SSR-safe (returns empty URL and empty params when `window` is unavailable).
- `debugUtmState()` calls `getDiagnostics()` and formats output using `console.group`/`console.table` for structured browser console display.
- `checkUtmTracking()` calls `getDiagnostics()` and returns an array of status strings with emoji prefixes indicating state (e.g., whether params are in the URL, in storage, or if there is a mismatch suggesting the hook has not initialized yet).
- `installDebugHelpers()` checks for `?debug_utm=true` in the URL query string. If present, it attaches a `window.utmDebug` object with `state()`, `check()`, `diagnostics()`, and `raw()` methods. Only activates in browser environments.
### Things to Know
- `installDebugHelpers()` is gated solely by the `debug_utm=true` URL parameter. It does not check `process.env` or `import.meta.env.DEV`.
- The `window.utmDebug` object is attached via a cast to `Record<string, unknown>` to avoid TypeScript errors on the global augmentation.
- `checkUtmTracking()` detects a potential timing issue: when URL params exist but storage is empty and `captureOnMount` is enabled, it warns that the hook may not have initialized yet.
## Overview
- Development-time diagnostic utilities for inspecting UTM tracking state (config, URL params, stored params, storage availability).
- Provides console-formatted output, status check messages, and an opt-in `window.utmDebug` helper object for browser console access.
- Not part of the production data flow; intended for troubleshooting.
## How it fits into the larger codebase
- Imports `captureUtmParameters` from `@/src/core/capture`, `getStoredUtmParameters`/`isSessionStorageAvailable`/`getRawStoredValue` from `@/src/core/storage`, and `getDefaultConfig` from `@/src/config/defaults`.
- Re-exported through `@/src/index.ts` so consumers can call these functions directly.
- Does not depend on or interact with `@/src/react` -- it operates on the core layer only.
- All functions accept an optional `ResolvedUtmConfig`; when omitted, they fall back to `getDefaultConfig()`.
## Core Implementation
- `getDiagnostics()` assembles a `DiagnosticInfo` snapshot: resolves config, captures URL params via `captureUtmParameters`, reads stored params via `getStoredUtmParameters`, and checks `isSessionStorageAvailable()`. SSR-safe (returns empty URL and empty params when `window` is unavailable).
- `debugUtmState()` calls `getDiagnostics()` and formats output using `console.group`/`console.table` for structured browser console display.
- `checkUtmTracking()` calls `getDiagnostics()` and returns an array of status strings with emoji prefixes indicating state (e.g., whether params are in the URL, in storage, or if there is a mismatch suggesting the hook has not initialized yet).
- `installDebugHelpers()` checks for `?debug_utm=true` in the URL query string. If present, it attaches a `window.utmDebug` object with `state()`, `check()`, `diagnostics()`, and `raw()` methods. Only activates in browser environments.
## Things to Know
- `installDebugHelpers()` is gated solely by the `debug_utm=true` URL parameter. It does not check `process.env` or `import.meta.env.DEV`.
- The `window.utmDebug` object is attached via a cast to `Record<string, unknown>` to avoid TypeScript errors on the global augmentation.
- `checkUtmTracking()` detects a potential timing issue: when URL params exist but storage is empty and `captureOnMount` is enabled, it warns that the hook may not have initialized yet.
🧰 Tools
🪛 LanguageTool

[uncategorized] ~29-~29: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...tial timing issue: when URL params exist but storage is empty and captureOnMount i...

(COMMA_COMPOUND_SENTENCE_2)

🪛 markdownlint-cli2 (0.20.0)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
In `@src/debug/docs.md` around lines 5 - 30, Update the markdown heading levels so
H2 is used instead of H3 to avoid MD001 warnings: change the headings
"Overview", "How it fits into the larger codebase", "Core Implementation", and
"Things to Know" from ### to ## in src/debug/docs.md (leave the H1 intact and
ensure subsections beneath those remain correctly nested); no code changes
required—just adjust the markdown heading markers.

Created and maintained by Nori.
4 changes: 1 addition & 3 deletions src/debug/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ export function checkUtmTracking(config?: ResolvedUtmConfig): string[] {
/**
* Install debug helpers on the window object for browser console access
*
* Only installs if running in a browser and either:
* - In development mode (import.meta.env.DEV)
* - URL contains ?debug_utm=true
* Only installs if running in a browser and URL contains ?debug_utm=true
*
* @example
* ```typescript
Expand Down
48 changes: 48 additions & 0 deletions src/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Noridoc: src

Path: @/src

### Overview

- Root source directory for `@jackmisner/utm-toolkit`, a TypeScript library for capturing, storing, and appending UTM tracking parameters.
- Contains framework-agnostic core utilities (`@/src/core`, `@/src/config`, `@/src/debug`, `@/src/types`) and an optional React integration (`@/src/react`).
- Exposes two package entry points: `@/src/index.ts` (main, imported as `@jackmisner/utm-toolkit`) and `@/src/react/index.ts` (imported as `@jackmisner/utm-toolkit/react`).

### How it fits into the larger codebase

- `@/src/index.ts` is the main barrel export that re-exports everything from `core`, `config`, `debug`, and `types`. This is what consumers get when they `import from '@jackmisner/utm-toolkit'`.
- `@/src/react/index.ts` is the second entry point for React-specific exports, built as a separate bundle with React externalized.
- `@/tsup.config.ts` defines these two entry points and produces dual ESM/CJS output with TypeScript declarations.
- `@/__tests__` mirrors this directory structure for testing.
- The library has zero runtime dependencies. React is an optional peer dependency used only by `@/src/react`.

### Core Implementation

The library follows a layered architecture:

```text
Consumer API
|
+--> src/index.ts (barrel) -----> core/ config/ debug/ types/
|
+--> src/react/index.ts --------> react/ (useUtmTracking, UtmProvider)
|
+--> core/ config/ types/
```

- **types/** (`@/src/types`): Shared type definitions consumed by all other modules. Defines the dual key format system (snake_case/camelCase) and configuration interfaces.
- **config/** (`@/src/config`): Pure configuration creation and validation. Merges partial user config with defaults to produce `ResolvedUtmConfig`.
- **core/** (`@/src/core`): Framework-agnostic UTM operations -- capture from URLs, persist in sessionStorage, append to outbound URLs, convert key formats, validate URLs. All SSR-safe.
- **debug/** (`@/src/debug`): Development-time diagnostics. Assembles state snapshots and provides formatted console output and optional `window.utmDebug` helpers.
- **react/** (`@/src/react`): React hook and context provider that orchestrate the core modules into stateful React APIs with auto-capture-on-mount behavior.

**Key data flow**: URL with UTM params --> `capture` --> `store` in sessionStorage --> `appendToUrl` for outbound link generation.

### Things to Know

- **Dual key format invariant**: The library supports both `snake_case` (URL convention) and `camelCase` (TypeScript convention) throughout, but all URL-facing operations always convert to snake_case internally. This is enforced in `@/src/core/appender.ts`.
- **SSR safety**: Every module that touches browser APIs (`window`, `sessionStorage`, `URL`, `document`) guards against their absence. The library can be imported and initialized on the server without errors.
- **Two entry points**: The package.json `exports` map defines separate conditional exports for `.` and `./react`, each with ESM/CJS/types variants. React is externalized in the build so it is not bundled into the output.
- **No runtime dependencies**: The library is self-contained. All functionality is implemented from scratch using standard Web APIs (`URL`, `URLSearchParams`, `sessionStorage`).
Comment on lines 5 to 46
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

Fix heading levels and add a code‑fence language.

Suggested fix
-### Overview
+## Overview
@@
-### How it fits into the larger codebase
+## How it fits into the larger codebase
@@
-### Core Implementation
+## Core Implementation
@@
-```
+```text
@@
-### Things to Know
+## Things to Know
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 5-5: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)


[warning] 23-23: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@src/docs.md` around lines 5 - 46, Change the markdown heading level "###
Things to Know" to "## Things to Know" and add a language identifier to the code
fence shown in the suggested fix (replace ```text with a proper language marker
like ```md or ```text as chosen) so heading hierarchy is corrected and the
fenced block has an explicit language; target the literal heading line "###
Things to Know" and the opening code fence token in the diff for this change.


Created and maintained by Nori.
Loading