Skip to content

feat: capture page views and surface as page_events in selectPlacements - #109

Merged
alexs-mparticle merged 28 commits into
developmentfrom
feat/capture-page-views
Aug 4, 2026
Merged

feat: capture page views and surface as page_events in selectPlacements#109
alexs-mparticle merged 28 commits into
developmentfrom
feat/capture-page-views

Conversation

@alexs-mparticle

@alexs-mparticle alexs-mparticle commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

Captures page-view events as they are logged, persists them in the kit's own localStorage (separate from mParticle persistence and cookie sync), and surfaces them as a stringified page_events array on the next selectPlacements call as targeting context.

What's included

  • Capture (processcapturePageView): page views (EventDataType === 3) are appended to a kit-owned mpPageViews list in localStorage. Each record stores pageUrl (from window.location.href, query string stripped), sourceMessageId, timestamp, and activeTimeOnSite. Capture runs independently of setLocalSessionAttribute availability, since the kit controls its own storage.
  • Fixed count cap: the list is capped at 25 records, evicting the oldest first. A simple, predictable limit — no storage-backend sniffing.
  • timeOnPage (buildPageEvents): each entry carries a derived timeOnPage — the active time a page was viewed before the next page view was logged (diff of consecutive activeTimeOnSite). Emitted only when it's a genuine non-negative number; omitted for the still-open last entry, negative diffs, and non-numeric values. Derived at read time — nothing extra persisted.
  • Feeding selectPlacements: the stored page views are read back, flattened via buildPageEvents, and sent as a stringified page_events array alongside the other placement attributes.

Storage & error handling

The kit owns its localStorage directly rather than writing through mParticle's local-session-attribute store, so page-view capture no longer touches mParticle persistence or cookie sync.

  • Read (readPageViewsStorage): guarded — returns an empty array when nothing is stored, the value can't be parsed, or localStorage is unavailable (Safari private mode, storage disabled).
  • Write (writePageViewsStorage): a failure (quota exceeded, storage disabled) is caught in capturePageView and surfaced as a PAGE_VIEW_CAPTURE_FAILED WARNING, bounded by the reporting service's per-severity rate limiter, without ever throwing out of the forwarder.

Testing

Navigate through pages on an MPA, then inspect the kit-owned store in the dev console:

JSON.parse(window.localStorage.getItem('mpPageViews'))

Expected result:

[
    {
        "pageUrl": "https://www.domain.com/",
        "sourceMessageId": "80588459-ea2c-491a-9f41-2acc15b8cfb8",
        "timestamp": 1785526522349,
        "activeTimeOnSite": 328892
    },
    {
        "pageUrl": "https://www.domain.com/page_1",
        "sourceMessageId": "88a1ef32-83fd-41a8-03d8-04c4c67c879d",
        "timestamp": 1785526728989,
        "activeTimeOnSite": 364827
    }
]

Verify the expected payload when making a selectPlacements call:

{
  "active_time_on_site_ms": "427361",
  "page_events": "[ /* stringified array of events, each with a derived timeOnPage */ ]",
  // other placement attributes
}

alexs-mparticle and others added 11 commits July 31, 2026 10:44
…ents

Derive a flat page_events array from stored page views at selectPlacements
time. Each view's eventAttributes are exploded into attr_-namespaced keys;
the title attribute is surfaced as page_name and EventName as event_name.
The raw nested mpPageViews store is stripped so only the flattened copy is
sent. Relax returnLocalSessionAttributes to no longer require a placement
mapping. Restore the isKitReady guard in process() and remove debug logging.
Compute a timeOnPage field for each page_events entry at read time in
buildPageEvents as the diff of consecutive activeTimeOnSite values — the
active time a page was viewed before the next page view was logged. Emitted
only when it is a genuine, non-negative number; omitted for the still-open
last entry, negative diffs (clock skew, reset, out-of-order), and
non-numeric activeTimeOnSite. Nothing new is persisted; StoredPageView is
unchanged.
…iews

# Conflicts:
#	dist/Rokt-Kit.common.js
#	dist/Rokt-Kit.common.js.map
#	dist/Rokt-Kit.esm.js.map
#	dist/Rokt-Kit.iife.js
#	dist/Rokt-Kit.iife.js.map
Add an explicit PageEvent return type for buildPageEvents (with timeOnPage
optional) instead of Record<string, unknown>[]. Revert the legacy
#processEvent attribute-mapping tests back to MessageType.PageView and strip
the captured mpPageViews list from their exact-shape assertions via a helper,
since processing a PageView now legitimately captures a page-view record.
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
…st test asserts

- Split mpPageViews off the session attributes via destructuring instead of
  delete, so the store returned by getLocalSessionAttributes is not mutated.
- Rename buildPageEvents map params pv/i to pageView/index.
- Drop the mappedSessionAttributes masking helper; the attribute-mapping tests
  now assert only their own mapped keys directly and leave mpPageViews to the
  page-view capture tests.
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
…ey, report capture failures

- Store page views as a JSON string via setLocalSessionAttribute to respect
  the primitive-only AttributeValue contract; parse on read (rmi22186)
- Rename PAGE_VIEWS_KEY -> LS_PAGE_VIEWS_KEY to distinguish the persistence
  key from the PAGE_EVENTS_KEY wire shape (rmi22186)
- Report page-view capture failures via errorReportingService instead of
  console.error for observability (rmi22186)
- Fold page_name assignment into the eventAttributes guard to drop the
  optional chain (rmi22186)

@jamesnrokt jamesnrokt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Requesting changes as we definitely need the cleansing of the URL in before go live

Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts
Comment thread src/Rokt-Kit.ts Outdated
Comment thread src/Rokt-Kit.ts
- sanitizeUrl now strips the query string (commonly carries PII) before
  a page-view URL is persisted and sent to Rokt
- add rationale comment for MAX_PAGE_VIEWS cap
- rename buildPageEvents var flat -> pageEvent
- drop redundant typeof guards on activeTimeOnSite (already typed number;
  diff >= 0 check handles NaN)
- add test covering query-param stripping
This flow targets auto page views, which carry no useful EventName or
EventAttributes, so remove event_name, page_name, and the attr_-namespaced
event-attribute explosion from both the stored and wire shapes. Simplifies
StoredPageView/PageEvent to url, sourceMessageId, timestamp, activeTimeOnSite
(+ derived timeOnPage).
page_events is an array of objects, but the Rokt attribute contract only
permits primitives and arrays of primitives. Passing raw objects to the
launcher is undefined behaviour, so JSON-stringify it at the call site and
JSON.parse in the tests that assert on it.
- Resolve pageUrl as the first step inside capturePageView's try and reuse
  it in the failure log, so an undefined URL signals failure at/before URL
  construction and the log is never lost.
- Collapse StoredPageView and PageEvent into a single PageEvent interface;
  timeOnPage is optional and derived at transmission.
Replace the arbitrary MAX_PAGE_VIEWS=25 count cap with a byte-budget cap
resolved from mParticle's storage backend: 128 KB in localStorage mode,
1/3 of maxCookieSize in cookie mode, with a safe cookie-default fallback
when SDK internals are unavailable. Evicts oldest page views first until
the serialized blob fits the budget, always retaining the current view.
Persist captured page views in the kit's own localStorage instead of
mParticle's local session attributes, so page-view capture no longer
touches mParticle persistence or cookie sync. Replace the storage-backend
byte-budget calculator with a fixed 25-record cap (oldest evicted), and
drop the SDK-internal _Store/SDKConfig typing it depended on.

Capture now runs independently of setLocalSessionAttribute availability,
and localStorage read/write is guarded so a storage failure surfaces a
single WARNING without throwing out of the forwarder.

@jamesnrokt jamesnrokt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Final remaining comment I know you're already addressing is clearing out storage on session

Comment thread src/Rokt-Kit.ts
Comment thread src/Rokt-Kit.ts Outdated
A localStorage JSON round-trip can yield undefined/NaN for a mangled or
partially-written record, so activeTimeOnSite is no longer guaranteed to
be a number. Type it as optional and guard the timeOnPage subtraction in
buildPageEvents against non-number values.

Addresses review feedback on PR #109.
Restore the isKitReady guard in process() that was removed in 329efb3,
but place it after kit-owned page-view capture so capture still runs
before the launcher attaches while the core SDK regains the not-ready
signal for the forwarding path.

Also gate page-view capture and session-end cleanup on the noTargeting
launcher option: page views are behavioral targeting signals and must
not be collected when the partner has opted out of targeting.

Addresses review feedback on PR #109.
A finite number round-trips losslessly through JSON, and SDKEvent types
ActiveTimeOnSite as a non-optional number, so buildPageEvents never sees
a non-number in the normal flow. Drop the per-field typeof guard and
realign PageEvent.activeTimeOnSite with the source type.
A NaN/Infinity source serializes to "null" via JSON.stringify and reads
back as a non-number, which is what made the stored type look inconsistent
in review. Normalizing at capture guarantees only finite numbers enter
storage, so PageEvent.activeTimeOnSite stays an honest non-optional number
with no per-field guard needed on read.
Comment thread src/Rokt-Kit.ts
…ting disabled

Revert PageEvent.activeTimeOnSite to optional and omit it at capture when
non-finite rather than coercing to 0, which would be diffed against the next
record in buildPageEvents and fabricate a dwell time. Guard the timeOnPage diff
on both records carrying a finite value so "unknown" stays distinguishable from
a genuine zero.

Clear the kit-owned page-view store once on init when targeting is disabled, so
a later re-enable starts fresh without reading storage on every event or
dispatch. A clear failure surfaces as a PAGE_VIEW_CAPTURE_FAILED warning.

@rmi22186 rmi22186 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I noticed that Event.EventAttributes were removed between commits at some point. If that is the intent, then this LGTM

@jamesnrokt jamesnrokt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for your efforts on this - apologies for the many questions

@alexs-mparticle
alexs-mparticle merged commit 098aabe into development Aug 4, 2026
4 of 5 checks passed
github-actions Bot pushed a commit that referenced this pull request Aug 4, 2026
# [1.30.0](v1.29.0...v1.30.0) (2026-08-04)

### Features

* capture page views and surface as page_events in selectPlacements ([#109](#109)) ([098aabe](098aabe))
@mparticle-automation

Copy link
Copy Markdown
Collaborator

🎉 This PR is included in version 1.30.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants