-
Notifications
You must be signed in to change notification settings - Fork 2
feat(spike): optimization surface for experiences integration [NT-3613] #362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Johannes Maximilian Toball (jmtoball)
wants to merge
7
commits into
main
Choose a base branch
from
docs/NT-3613-optimization-experiences-spike-design
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
42cfd47
feat(api-schemas): add SourceMap + NodeViewEvent schemas [NT-3613]
jmtoball 6255421
feat(core-sdk): add trackNodeView and node-view event builder [NT-3613]
jmtoball d380c42
feat(core-sdk): add personalization request/response methods [NT-3613]
jmtoball 93ca951
feat(web-sdk): add resolveNodeViewPayload node resolver [NT-3613]
jmtoball b3364d4
feat(web-sdk): add NodeInteractionRuntime for data-ctfl-node-id track…
jmtoball 95d5d0b
feat(react-web-sdk): add experiences-adapter subpath export [NT-3613]
jmtoball a42e326
test(core-sdk): buildNodeView + trackNodeView unit coverage [NT-3613]
jmtoball File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
packages/universal/api-schemas/src/experience/sourceMap/SourceMap.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import * as z from 'zod/mini' | ||
|
|
||
| /** | ||
| * Header/opt-in constant for requesting `extensions.sourceMap` from XDA. | ||
| * | ||
| * @remarks | ||
| * Setting either this header or `extensions.sourceMap: {}` on the request | ||
| * body signals opt-in. | ||
| * | ||
| * @public | ||
| */ | ||
| export const EXTENSIONS_SOURCE_MAP_HEADER = 'x-contentful-extensions-sourcemap' as const | ||
|
|
||
| /** | ||
| * Zod schema for a variant row in the source-map, currently only the | ||
| * `personalization` type is emitted. | ||
| * | ||
| * @remarks | ||
| * The wire authority is `@contentful/view-delivery-contract`; this schema is | ||
| * a subset of it, covering only the fields read by | ||
| * {@link resolveNodeViewPayload}. | ||
| * | ||
| * @public | ||
| */ | ||
| export const SourceMapVariant = z.object({ | ||
| type: z.literal('personalization'), | ||
| id: z.string(), | ||
| experienceId: z.optional(z.string()), | ||
| optimizationId: z.optional(z.string()), | ||
| variantId: z.optional(z.string()), | ||
| variantIndex: z.optional(z.number()), | ||
| }) | ||
|
|
||
| /** | ||
| * TypeScript type inferred from {@link SourceMapVariant}. | ||
| * | ||
| * @public | ||
| */ | ||
| export type SourceMapVariant = z.infer<typeof SourceMapVariant> | ||
|
|
||
| /** | ||
| * Zod schema for a layer row in the source-map. Discriminated on `kind`. | ||
| * | ||
| * @remarks | ||
| * Only `Experience` and `Fragment` layers carry a `variants` reference and | ||
| * are considered attributable by | ||
| * {@link resolveNodeViewPayload}. The remaining kinds (`ComponentType`, | ||
| * `Template`, `Slot`, `InlineFragment`) are accepted so `.parse` does not | ||
| * fail on future-added fields. | ||
| * | ||
| * @public | ||
| */ | ||
| export const SourceMapLayer = z.discriminatedUnion('kind', [ | ||
| z.object({ kind: z.literal('ComponentType'), id: z.string() }), | ||
| z.object({ kind: z.literal('Template'), id: z.string() }), | ||
| z.object({ kind: z.literal('Slot'), id: z.string() }), | ||
| z.object({ | ||
| kind: z.literal('Experience'), | ||
| id: z.string(), | ||
| variants: z.array(z.number()), | ||
| dataAssembly: z.optional(z.number()), | ||
| }), | ||
| z.object({ | ||
| kind: z.literal('Fragment'), | ||
| id: z.string(), | ||
| variants: z.array(z.number()), | ||
| dataAssembly: z.optional(z.number()), | ||
| }), | ||
| z.object({ | ||
| kind: z.literal('InlineFragment'), | ||
| id: z.string(), | ||
| dataAssembly: z.optional(z.number()), | ||
| }), | ||
| ]) | ||
|
|
||
| /** | ||
| * TypeScript type inferred from {@link SourceMapLayer}. | ||
| * | ||
| * @public | ||
| */ | ||
| export type SourceMapLayer = z.infer<typeof SourceMapLayer> | ||
|
|
||
| /** | ||
| * Zod schema for a per-hydrated-node mapping row. | ||
| * | ||
| * @remarks | ||
| * `layers` runs leaf-to-root; `scope` indexes the nearest data-context | ||
| * boundary layer used for entity-id attribution. | ||
| * | ||
| * @public | ||
| */ | ||
| export const SourceMapNode = z.object({ | ||
| layers: z.array(z.number()), | ||
| scope: z.number(), | ||
| contentProperties: z.array(z.unknown()), | ||
| }) | ||
|
|
||
| /** | ||
| * TypeScript type inferred from {@link SourceMapNode}. | ||
| * | ||
| * @public | ||
| */ | ||
| export type SourceMapNode = z.infer<typeof SourceMapNode> | ||
|
|
||
| /** | ||
| * Zod schema for the top-level source-map payload. Field names mirror | ||
| * `DeliveryViewSourceMapSchema` from `@contentful/view-delivery-contract` so | ||
| * `.parse` will not fail on future-added fields; only `variants`, `layers`, | ||
| * and `nodes` are read by consumers in this package. | ||
| * | ||
| * @public | ||
| */ | ||
| export const SourceMap = z.object({ | ||
| version: z.literal(1), | ||
| variants: z.array(SourceMapVariant), | ||
| spaces: z.array(z.string()), | ||
| environments: z.array(z.string()), | ||
| locales: z.array(z.string()), | ||
| entries: z.array(z.unknown()), | ||
| assets: z.array(z.unknown()), | ||
| layers: z.array(SourceMapLayer), | ||
| dataAssemblies: z.array(z.unknown()), | ||
| nodes: z.record(z.string(), SourceMapNode), | ||
| }) | ||
|
|
||
| /** | ||
| * TypeScript type inferred from {@link SourceMap}. | ||
| * | ||
| * @public | ||
| */ | ||
| export type SourceMap = z.infer<typeof SourceMap> |
1 change: 1 addition & 0 deletions
1
packages/universal/api-schemas/src/experience/sourceMap/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './SourceMap' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
packages/universal/api-schemas/src/insights/event/NodeViewEvent.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import * as z from 'zod/mini' | ||
| import { UniversalEventProperties } from '../../experience/event/UniversalEventProperties' | ||
|
|
||
| /** | ||
| * Structural kinds an `exo_node_view` event can be attributed to. | ||
| * | ||
| * @remarks | ||
| * Narrowed to the two kinds that carry a `variants` reference in the XDA | ||
| * source-map contract. `InlineFragment` and structural rows are never | ||
| * attributable and are therefore not surfaced here. | ||
| * | ||
| * @public | ||
| */ | ||
| const NodeViewEntityKind = z.union([z.literal('Experience'), z.literal('Fragment')]) | ||
|
|
||
| /** | ||
| * Zod schema describing an `exo_node_view` event used for XDA graph node | ||
| * viewport tracking. | ||
| * | ||
| * @remarks | ||
| * These events track the exposure of rendered XDA nodes in the browser | ||
| * viewport. They are self-contained: all metadata required by the ingestor | ||
| * is embedded in the payload and no server-side lookup is needed. | ||
| * | ||
| * Unlike {@link ViewEvent}, which is entry-centric, `NodeViewEvent` is | ||
| * graph-node-centric and carries structural metadata resolved from the XDA | ||
| * `extensions.sourceMap`. | ||
| * | ||
| * @public | ||
| */ | ||
| export const NodeViewEvent = z.extend(UniversalEventProperties, { | ||
| /** | ||
| * Stable anonymous user identifier for this event. | ||
| */ | ||
| anonymousId: z.string(), | ||
|
|
||
| /** | ||
| * Discriminator identifying this as an XDA node view event. | ||
| */ | ||
| type: z.literal('exo_node_view'), | ||
|
|
||
| /** | ||
| * `sys.id` of the Experience or Fragment that owns this node. | ||
| */ | ||
| entityId: z.string(), | ||
|
|
||
| /** | ||
| * Structural kind of the owning entity. | ||
| */ | ||
| entityKind: NodeViewEntityKind, | ||
|
|
||
| /** | ||
| * Variant identifier selected for this node. | ||
| * | ||
| * @remarks | ||
| * Resolved from `extensions.sourceMap.variants[].variantId`, falling back to | ||
| * `extensions.sourceMap.variants[].id`. | ||
| */ | ||
| variantId: z.string(), | ||
|
|
||
| /** | ||
| * Variant index selected for this node. | ||
| * | ||
| * @remarks | ||
| * Resolved from `extensions.sourceMap.variants[].variantIndex`, falling back | ||
| * to the selected `extensions.sourceMap.layers[].variants[]` reference. The | ||
| * default variant is index `0`. | ||
| */ | ||
| variantIndex: z.number(), | ||
|
|
||
| /** | ||
| * Ninetailed experience (optimization) ID associated with this node. | ||
| */ | ||
| optimizationId: z.string(), | ||
|
|
||
| /** | ||
| * UUID identifying a single active view session for this node. | ||
| * | ||
| * @remarks | ||
| * Multiple events emitted for the same active view share this identifier. | ||
| */ | ||
| viewId: z.string(), | ||
|
|
||
| /** | ||
| * Monotonically increasing visible duration for the active view, in | ||
| * milliseconds. | ||
| * | ||
| * @remarks | ||
| * Updated and re-emitted while the same view remains active. | ||
| */ | ||
| viewDurationMs: z.number(), | ||
|
|
||
| /** | ||
| * `sys.id` of the parent Experience when this node is nested inside one. | ||
| */ | ||
| parentExperienceId: z.optional(z.string()), | ||
| }) | ||
|
|
||
| /** | ||
| * TypeScript type inferred from {@link NodeViewEvent}. | ||
| * | ||
| * @public | ||
| */ | ||
| export type NodeViewEvent = z.infer<typeof NodeViewEvent> | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't a NodeViewEvent what was a ComponentViewEvent conceptually? I understand, that there's additional metadata but it feels a bit to Contentful specific for me instead of an open platform concept.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's an extension of the ViewEvent concept to cover ExO-backed nodes (its' not a clean extension as some fields are renamed here to avoid clashes, e.g. (9t-)
experienceId->optimizationId.It could theoretically be made more Contentful-agnostic (even though it's of course still Contentful Optimization-specific via
experienceIdandvariantIndex), but I'm not sure what that would buy us. It could make expressing the relationships more awkward if we're not talking about the ExO-entities directly and there is some abstraction over e.g. the concrete entities already to keep it extensible that could in principle be remapped outside Contentful. But maybe I'm not following where you're coming from with this 🤷♂️Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far we have tried to create not too many events and rather stay agnostic and use the more generic fields of the events.
I just want to make sure that we're not having a long list of different event types in the future which are all pretty much the same.
The Event types have to get implemented on the backend then aswell.