feat: capture JSON-LD as replay custom events - #4640
Conversation
Replay incident risk checkThis diff touches code involved in past incidents. This is a heads-up, not a verdict: read the matched sections of INCIDENTS.md and answer their review questions before merging. For a judgment on whether this diff has the same failure mode, run the |
posthog-js Compliance ReportDate: 2026-08-25 22:25:03 UTC ✅ All Tests Passed!26/26 tests passed Capture Tests✅ 26/26 tests passed View Details
|
|
Size Change: +59.6 kB (+0.29%) Total Size: 20.3 MB 📦 View Changed
ℹ️ View Unchanged
|
…n-ld-events # Conflicts: # packages/browser/terser-mangled-names.json
Prompt To Fix All With AI### Issue 1
packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts:907
**Promise-chained JSON-LD scheduling**
`_scheduleJsonLdScan` uses `Promise.resolve().then(...)` for the new asynchronous lifecycle path instead of the repository-preferred async/await style, making this state-sensitive scheduling less consistent and harder to follow.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "perf(replay): simplify JSON-LD capture" | Re-trigger Greptile |
| this._instance.config.session_recording?.captureJsonLd === true && | ||
| !this._urlTriggerMatching.urlBlocked && | ||
| this._isIdle !== true | ||
| ) |
There was a problem hiding this comment.
Promise-chained JSON-LD scheduling
_scheduleJsonLdScan uses Promise.resolve().then(...) for the new asynchronous lifecycle path instead of the repository-preferred async/await style, making this state-sensitive scheduling less consistent and harder to follow.
Rule Used: Prefer using 'async/await' for handling asynchrono... (source)
Learned From
PostHog/posthog#32091
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts
Line: 907
Comment:
**Promise-chained JSON-LD scheduling**
`_scheduleJsonLdScan` uses `Promise.resolve().then(...)` for the new asynchronous lifecycle path instead of the repository-preferred async/await style, making this state-sensitive scheduling less consistent and harder to follow.
**Rule Used:** Prefer using 'async/await' for handling asynchrono... ([source](https://app.greptile.com/posthog-org-19734/-/custom-context?memory=2c94c575-260f-4a48-a803-33ca36abe20f))
**Learned From**
[PostHog/posthog#32091](https://github.com/PostHog/posthog/pull/32091)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
marandaneto
left a comment
There was a problem hiding this comment.
Automated advisory code review.
|
|
||
| export const JSON_LD_EVENT_TAG = '$json_ld' | ||
|
|
||
| function isObject(value: unknown): value is Record<string, unknown> { |
There was a problem hiding this comment.
nit: Reuse the shared object type guard — this duplicates isObject already exported by @posthog/core, while subtly accepting arrays as records. Importing the shared implementation removes this one-off abstraction and uses the repository's established semantics.
| * The recorder keeps `@id` values without changes. | ||
| * The event tag is `$json_ld`. The payload is a JSON-LD object or array. | ||
| * The recorder removes all script nodes from snapshots when this option is enabled. | ||
| * Supported types are Action, AggregateOffer, AggregateRating, Brand, CreativeWork, Offer, Organization, Person, Place, and Product. |
There was a problem hiding this comment.
suggestion: Document the complete capture surface — this lists only ten supported types, but the sanitizer also captures Event, ItemList, OfferCatalog, Service, and over a hundred named or inherited types. Because this is a privacy-sensitive capture contract, please describe all categories or link to an authoritative allowlist.
Summary
session_recording.captureJsonLd.$json_ldtag.@graph. DOM position is not part of the event.This is an alternative to #4625. The option is off by default.
Whitelist
The whitelist accepts the 124 entity types that Google names in its current Search structured-data feature guides and examples. It also accepts 43 common Schema.org types outside that list. These additions cover common pages, actions, services, catalogs, organizations, local businesses, events, media, and product subtypes. Examples include
WebSite,FAQPage,SearchAction,Service,OfferCatalog,School,CafeOrCoffeeShop,MusicEvent,PodcastEpisode, andProductModel.The sanitizer accepts an
@typearray or a fullhttps://schema.org/Typevalue. It removes unknown type values and drops an entity if no allowed type remains. A root@graphcan contain supported and unsupported entities. The sanitizer keeps only rebuilt supported entities and drops the complete graph if none remain.Every captured entity can contain
@id. Types use these property rules:Actionand named action subtypes:actionStatusAggregateOffer: price range, currency, count, availability, and nested offersAggregateRating,EmployerAggregateRating, andRating: rating values and countsBrand:nameBreadcrumbListandItemList: list metadata and rebuiltListItemvaluesCreativeWorkand named subtypes: language, format, date, accessibility, education, rating, publisher, and potential action fieldsEventand named subtypes: date, status, attendance, capacity, accessibility, rating, and offer fieldsListItem: position and one rebuilt supported entityOffer: price, currency, validity, availability, condition, and seller fieldsOfferCatalog: name and rebuilt supported entitiesOrganizationand named subtypes: name, date, nonprofit, rating, and brand fieldsPlaceand named subtypes: access, capacity, accessibility, and rating fieldsProductand named subtypes: identifiers, model, category, physical attributes, dates, brand, manufacturer, offers, and rating fieldsService: name, type, category, provider, service area, offers, and rating fieldsPersonand PII-heavy support types such asContactPointandPostalAddress: no properties other than@id@typeand@idLeaf values must be a string, number, boolean, null, or an array that contains only those values. Nested values must match an allowed entity type.
Privacy and lifecycle
@idis kept without changes by design. We need a separate follow-up PR to define and add@idscrubbing before we enable this option for customers.Incident review
Bundle size
array.js:+0.00%minified,-0.01%gzip,+0.04%Brotli againstorigin/main.posthog-recorder.js:+8,718minified bytes and+3,470gzip bytes.+2,263minified bytes and+1,027gzip bytes of the recorder total.@graphsupport add another+1,017minified bytes and+453gzip bytes.Tests
posthog-js@1.418.16core and this recorder bundle.Firefox and WebKit were not available in the local Playwright installation. CI will run the configured browser coverage.