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
2 changes: 1 addition & 1 deletion platforms/javascript/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
return
}

const personalizedEntry = optimization.personalization.personalizeEntry(entry)
const { entry: personalizedEntry } = optimization.personalization.personalizeEntry(entry)

let title = personalizedEntry.fields.internalTitle
let mergeTagValues = undefined
Expand Down
2 changes: 1 addition & 1 deletion platforms/javascript/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"build:umd": "vite build -c vite.umd.config.ts",
"build": "pnpm clean && tsc -b ./tsconfig.build.json && pnpm build:esm && pnpm build:umd",
"clean": "rimraf ./dist ./coverage tsconfig.tsbuildinfo",
"dev": "vite --host",
"dev": "vite -c vite.esm.config.ts --host",
"test:unit": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
Expand Down
3 changes: 3 additions & 0 deletions platforms/javascript/web/src/Optimization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,7 @@ class Optimization extends CoreStateful {
}
}

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- protect against non-Web
if (window) window.Optimization ??= Optimization

export default Optimization
4 changes: 2 additions & 2 deletions universal/api-schemas/src/contentful/AudienceEntry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as z from 'zod/mini'
import { Entry, EntryFields } from './Entry'
import { CtflEntry, EntryFields } from './CtflEntry'

export const AudienceEntryFields = z.extend(EntryFields, {
/**
Expand All @@ -19,7 +19,7 @@ export const AudienceEntryFields = z.extend(EntryFields, {
})
export type AudienceEntryFields = z.infer<typeof AudienceEntryFields>

export const AudienceEntry = z.extend(Entry, {
export const AudienceEntry = z.extend(CtflEntry, {
fields: AudienceEntryFields,
})
export type AudienceEntry = z.infer<typeof AudienceEntry>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Entry as ContentfulEntry } from 'contentful'
import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful'
import * as z from 'zod/mini'

export const EntryFields = z.catchall(z.object({}), z.json())
Expand Down Expand Up @@ -63,8 +63,8 @@ export const EntrySys = z.object({
})
export type EntrySys = z.infer<typeof EntrySys>

export const Entry = z.object({
fields: EntryFields,
export const CtflEntry = z.object({
fields: z.unknown(),
metadata: z.catchall(
z.object({
tags: z.array(TagLink),
Expand All @@ -73,8 +73,12 @@ export const Entry = z.object({
),
sys: EntrySys,
})
export type Entry = z.infer<typeof Entry>
export type CtflEntry = z.infer<typeof CtflEntry>

export function isEntry(entry: ContentfulEntry | undefined): entry is Entry {
return Entry.safeParse(entry).success
export function isEntry<
S extends EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
>(entry: Entry | undefined): entry is Entry<S, M, L> {
return CtflEntry.safeParse(entry).success
}
4 changes: 2 additions & 2 deletions universal/api-schemas/src/contentful/MergeTagEntry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as z from 'zod/mini'
import { Entry, EntrySys } from './Entry'
import { CtflEntry, EntrySys } from './CtflEntry'

export const MergeTagEntry = z.extend(Entry, {
export const MergeTagEntry = z.extend(CtflEntry, {
fields: z.object({
nt_name: z.string(),
nt_fallback: z.optional(z.string()),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Entry } from 'contentful'
import * as z from 'zod/mini'
import { AudienceEntry } from './AudienceEntry'
import { Entry, EntryFields, Link } from './Entry'
import { CtflEntry, EntryFields, Link } from './CtflEntry'
import { PersonalizationConfig } from './PersonalizationConfig'

export const PersonalizationType = z.union([
Expand Down Expand Up @@ -49,16 +50,16 @@ export const PersonalizationFields = z.extend(EntryFields, {
/**
* All used variants of the experience (Contentful references to other Content Types)
*/
nt_variants: z.optional(z.prefault(z.array(Entry), [])),
nt_variants: z.optional(z.prefault(z.array(z.custom<Entry>()), [])),
})
export type PersonalizationFields = z.infer<typeof PersonalizationFields>

export const PersonalizationEntry = z.extend(Entry, {
export const PersonalizationEntry = z.extend(CtflEntry, {
fields: PersonalizationFields,
})
export type PersonalizationEntry = z.infer<typeof PersonalizationEntry>

export function isPersonalizationEntry(entry: Entry | Link): entry is PersonalizationEntry {
export function isPersonalizationEntry(entry: CtflEntry | Link): entry is PersonalizationEntry {
return PersonalizationEntry.safeParse(entry).success
}

Expand Down
10 changes: 4 additions & 6 deletions universal/api-schemas/src/contentful/PersonalizedEntry.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import type { Entry as ContentfulEntry } from 'contentful'
import type { Entry } from 'contentful'
import * as z from 'zod/mini'
import { Entry, EntryFields } from './Entry'
import { CtflEntry, EntryFields } from './CtflEntry'
import { PersonalizationEntryArray } from './PersonalizationEntry'

export const PersonalizedEntry = z.extend(Entry, {
export const PersonalizedEntry = z.extend(CtflEntry, {
fields: z.extend(EntryFields, {
nt_experiences: PersonalizationEntryArray,
}),
})
export type PersonalizedEntry = z.infer<typeof PersonalizedEntry>

export function isPersonalizedEntry(
entry: ContentfulEntry | undefined,
): entry is PersonalizedEntry {
export function isPersonalizedEntry(entry: Entry | undefined): entry is PersonalizedEntry {
return PersonalizedEntry.safeParse(entry).success
}
2 changes: 1 addition & 1 deletion universal/api-schemas/src/contentful/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from './AudienceEntry'
export * from './Entry'
export * from './CtflEntry'
export * from './MergeTagEntry'
export * from './PersonalizationConfig'
export * from './PersonalizationEntry'
Expand Down
17 changes: 10 additions & 7 deletions universal/core/src/personalization/PersonalizationStateful.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
type TrackBuilderArgs,
TrackEvent,
} from '@contentful/optimization-api-client'
import type { Entry } from 'contentful'
import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful'
import { isEqual } from 'es-toolkit'
import { logger } from 'logger'
import type { ConsentGuard } from '../Consent'
Expand All @@ -39,8 +39,7 @@ import {
toObservable,
} from '../signals'
import PersonalizationBase from './PersonalizationBase'
import { PersonalizedEntryResolver } from './resolvers'
import MergeTagValueResolver from './resolvers/MergeTagValueResolver'
import { MergeTagValueResolver, PersonalizedEntryResolver, type ResolvedData } from './resolvers'

export interface PersonalizationProductConfigDefaults {
consent?: boolean
Expand Down Expand Up @@ -121,11 +120,15 @@ class Personalization extends PersonalizationBase implements ConsentGuard {
})
}

personalizeEntry(
entry: Entry,
personalizeEntry<
S extends EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
>(
entry: Entry<S, M, L>,
personalizations: SelectedPersonalizationArray | undefined = personalizationsSignal.value,
): Entry {
return PersonalizedEntryResolver.resolve(entry, personalizations)
): ResolvedData<S, M, L> {
return PersonalizedEntryResolver.resolve<S, M, L>(entry, personalizations)
}

getMergeTagValue(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type EntryReplacementComponent,
type EntryReplacementVariant,
isEntry,
isEntryReplacementComponent,
isEntryReplacementVariant,
isPersonalizationEntry,
Expand All @@ -10,9 +11,18 @@ import {
type SelectedPersonalization,
type SelectedPersonalizationArray,
} from '@contentful/optimization-api-client'
import type { Entry } from 'contentful'
import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful'
import { logger } from 'logger'

export interface ResolvedData<
S extends EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
> {
entry: Entry<S, M, L>
personalization?: SelectedPersonalization
}

const RESOLUTION_WARNING_BASE = '[Personalization] Could not resolve personalized entry variant:'

const PersonalizedEntryResolver = {
Expand Down Expand Up @@ -97,7 +107,11 @@ const PersonalizedEntryResolver = {
return relevantVariants.at(selectedVariantIndex - 1)
},

getSelectedVariantEntry(
getSelectedVariantEntry<
S extends EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
>(
{
personalizationEntry,
selectedVariant,
Expand All @@ -106,7 +120,7 @@ const PersonalizedEntryResolver = {
selectedVariant: EntryReplacementVariant
},
skipValidation = false,
): Entry | undefined {
): Entry<S, M, L> | undefined {
if (
!skipValidation &&
(!isPersonalizationEntry(personalizationEntry) || !isEntryReplacementVariant(selectedVariant))
Expand All @@ -117,30 +131,30 @@ const PersonalizedEntryResolver = {
(variant) => variant.sys.id === selectedVariant.id,
)

return selectedVariantEntry
},

decorateSelectedVariantFields(
selectedVariantEntry: Entry,
selectedPersonalization: SelectedPersonalization | undefined,
) {
selectedVariantEntry.fields.nt_personalization = selectedPersonalization ?? {}
return isEntry<S, M, L>(selectedVariantEntry) ? selectedVariantEntry : undefined
},

resolve(entry: Entry, selectedPersonalizations?: SelectedPersonalizationArray): Entry {
resolve<
S extends EntrySkeletonType,
M extends ChainModifiers = ChainModifiers,
L extends LocaleCode = LocaleCode,
>(
entry: Entry<S, M, L>,
selectedPersonalizations?: SelectedPersonalizationArray,
): ResolvedData<S, M, L> {
logger.info('[Personalization] Resolving personalized entry for baseline entry', entry.sys.id)

if (!selectedPersonalizations?.length) {
logger.warn(
RESOLUTION_WARNING_BASE,
'no selectedPersonalizations exist for the current profile',
)
return entry
return { entry }
}

if (!isPersonalizedEntry(entry)) {
logger.warn(RESOLUTION_WARNING_BASE, `entry ${entry.sys.id} is not personalized`)
return entry
return { entry }
}

const personalizationEntry = PersonalizedEntryResolver.getPersonalizationEntry(
Expand All @@ -156,7 +170,7 @@ const PersonalizedEntryResolver = {
RESOLUTION_WARNING_BASE,
`could not find a personalization entry for ${entry.sys.id}`,
)
return entry
return { entry }
}

const selectedPersonalization = PersonalizedEntryResolver.getSelectedPersonalization(
Expand All @@ -174,7 +188,7 @@ const PersonalizedEntryResolver = {
`[Personalization] Resolved personalization entry for entry ${entry.sys.id} is baseline`,
)

return entry
return { entry }
}

const selectedVariant = PersonalizedEntryResolver.getSelectedVariant(
Expand All @@ -191,10 +205,10 @@ const PersonalizedEntryResolver = {
RESOLUTION_WARNING_BASE,
`could not find a valid replacement variant entry for ${entry.sys.id}`,
)
return entry
return { entry }
}

const selectedVariantEntry = PersonalizedEntryResolver.getSelectedVariantEntry(
const selectedVariantEntry = PersonalizedEntryResolver.getSelectedVariantEntry<S, M, L>(
{
personalizationEntry,
selectedVariant,
Expand All @@ -207,19 +221,14 @@ const PersonalizedEntryResolver = {
RESOLUTION_WARNING_BASE,
`could not find a valid replacement variant entry for ${entry.sys.id}`,
)
return entry
return { entry }
} else {
logger.info(
`[Personalization] Entry ${entry.sys.id} has been resolved to variant entry ${selectedVariantEntry.sys.id}`,
)
}

PersonalizedEntryResolver.decorateSelectedVariantFields(
selectedVariantEntry,
selectedPersonalization,
)

return selectedVariantEntry
return { entry: selectedVariantEntry, personalization: selectedPersonalization }
},
}

Expand Down
5 changes: 5 additions & 0 deletions universal/core/src/personalization/resolvers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export * from './FlagsResolver'
export { default as FlagsResolver } from './FlagsResolver'

export * from './MergeTagValueResolver'
export { default as MergeTagValueResolver } from './MergeTagValueResolver'

export * from './PersonalizedEntryResolver'
export { default as PersonalizedEntryResolver } from './PersonalizedEntryResolver'