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
6 changes: 3 additions & 3 deletions implementations/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"scripts": {
"clean": "rimraf ./coverage ./playwright-report ./test-results",
"serve": "pnpm serve:mocks && pnpm serve:app",
"serve:app": "pm2 start \"tsx --env-file=.env ./src/app.ts\"",
"serve:mocks": "pm2 start \"pnpm --filter mocks mocks:serve\"",
"serve:stop": "pm2 stop all && pm2 delete all",
"serve:app": "pm2 start --name node-app \"tsx --env-file=.env ./src/app.ts\"",
"serve:mocks": "pm2 start --name node-mocks \"pnpm --filter mocks mocks:serve\"",
"serve:stop": "pm2 stop node-app node-mocks && pm2 delete node-app node-mocks",
"test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT",
"test:e2e:codegen": "playwright codegen",
"test:e2e:report": "playwright show-report",
Expand Down
4 changes: 2 additions & 2 deletions implementations/web-vanilla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"clean": "rimraf ./public/dist ./coverage ./playwright-report ./test-results tsconfig.tsbuildinfo",
"serve": "pnpm serve:mocks && pnpm serve:app",
"serve:app": "pnpm build && docker compose up -d",
"serve:mocks": "pm2 start \"pnpm --filter mocks mocks:serve\"",
"serve:stop": "docker compose down && pm2 stop all && pm2 delete all",
"serve:mocks": "pm2 start --name web-mocks \"pnpm --filter mocks mocks:serve\"",
"serve:stop": "docker compose down && pm2 stop web-mocks && pm2 delete web-mocks",
"test:e2e": "pnpm serve && playwright test; E2E_RESULT=$?; pnpm serve:stop; exit $E2E_RESULT",
"test:e2e:codegen": "playwright codegen",
"test:e2e:report": "playwright show-report",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "Contentful Optimization SDK Suite",
"license": "MIT",
"scripts": {
"pm2:list": "pm2 list",
"prepare": "husky",
"build": "pnpm --filter @contentful/* build",
"build:implementations": "pnpm --filter @implementation/* build",
Expand All @@ -13,7 +14,7 @@
"format:fix": "prettier . --check --write",
"lint:check": "eslint .",
"lint:fix": "eslint . --fix",
"test:e2e": "pnpm --filter @implementation/* test:e2e",
"test:e2e": "pnpm --parallel --filter @implementation/* test:e2e",
"test:unit": "pnpm --filter @contentful/* test:unit",
"test:unit:implementations": "pnpm --filter @implementation/* test:unit",
"typecheck": "pnpm -r typecheck",
Expand All @@ -34,6 +35,7 @@
"husky": "^9.1.7",
"jiti": "^2.5.1",
"lint-staged": "^16.1.2",
"pm2": "catalog:",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"typedoc": "^0.28.8",
Expand Down
63 changes: 39 additions & 24 deletions platforms/javascript/core/src/CoreBase.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import ApiClient, {
EventBuilder,
type InsightsEvent as AnalyticsEvent,
type ApiClientConfig,
type ChangeArray,
type EventBuilderConfig,
type GlobalApiConfigProperties,
type Profile,
type SelectedPersonalizationArray,
type ExperienceEvent as PersonalizationEvent,
} from '@contentful/optimization-api-client'
import type { LogLevels } from 'logger'
import { ConsoleLogSink, logger } from 'logger'
import type AnalyticsBase from './analytics/AnalyticsBase'
import { Personalization } from './personalization'
import { batch, changes, consent, personalizations, profile } from './signals'
import type { AnalyticsConfigDefaults, AnalyticsStates } from './analytics/AnalyticsBase'
import {
Personalization,
type PersonalizationConfigDefaults,
type PersonalizationStates,
} from './personalization'
import { consent, event, toObservable, type Observable } from './signals'

export interface CoreConfigDefaults {
changes?: ChangeArray
consent?: boolean
profile?: Profile
personalizations?: SelectedPersonalizationArray
personalization?: PersonalizationConfigDefaults
analytics?: AnalyticsConfigDefaults
}

export interface CoreStates extends AnalyticsStates, PersonalizationStates {
consent: Observable<boolean | undefined>
eventStream: Observable<AnalyticsEvent | PersonalizationEvent | undefined>
}

/** Options that may be passed to the Core constructor */
Expand Down Expand Up @@ -50,20 +58,9 @@ abstract class CoreBase implements ConsentController {

logger.addSink(new ConsoleLogSink(logLevel))

if (defaults) {
const {
changes: defaultChanges,
consent: defaultConsent,
personalizations: defaultPersonalizations,
profile: defaultProfile,
} = defaults

batch(() => {
changes.value = defaultChanges
consent.value = defaultConsent
personalizations.value = defaultPersonalizations
profile.value = defaultProfile
})
if (defaults?.consent !== undefined) {
const { consent: defaultConsent } = defaults
consent.value = defaultConsent
}

const apiConfig = {
Expand All @@ -82,12 +79,30 @@ abstract class CoreBase implements ConsentController {
},
)

this.personalization = new Personalization(this.api, this.eventBuilder)
this.personalization = new Personalization(
this.api,
this.eventBuilder,
defaults?.personalization,
)
}

get states(): CoreStates {
return {
...this.analytics.states,
...this.personalization.states,
consent: toObservable(consent),
eventStream: toObservable(event),
}
}

public consent(accept: boolean): void {
consent(accept: boolean): void {
consent.value = accept
}

/** Do not reset consent, resetting personalization _currently_ also resets analytics' dependencies */
reset(): void {
this.personalization.reset()
}
}

export default CoreBase
48 changes: 3 additions & 45 deletions platforms/javascript/core/src/CoreStateful.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,15 @@
import type {
InsightsEvent as AnalyticsEvent,
Flags,
ExperienceEvent as PersonalizationEvent,
Profile,
SelectedPersonalizationArray,
} from '@contentful/optimization-api-client'
import { AnalyticsStateful } from './analytics'
import CoreBase, { type CoreConfig } from './CoreBase'
import { consent, effect, event, flags, personalizations, profile } from './signals'

export interface Subscription {
unsubscribe: () => void
}

export interface Observable<T> {
subscribe: (next: (v: T) => void) => Subscription
}

export interface States {
consent: Observable<boolean | undefined>
eventStream: Observable<AnalyticsEvent | PersonalizationEvent | undefined>
flags: Observable<Flags | undefined>
profile: Observable<Profile | undefined>
personalizations: Observable<SelectedPersonalizationArray | undefined>
}

function toObservable<T>(s: { value: T }): Observable<T> {
return {
subscribe(next) {
const dispose = effect(() => {
next(s.value)
})

return { unsubscribe: dispose }
},
}
}

class CoreStateful extends CoreBase {
readonly analytics: AnalyticsStateful

readonly states: States = {
consent: toObservable(consent),
eventStream: toObservable(event),
flags: toObservable(flags),
profile: toObservable(profile),
personalizations: toObservable(personalizations),
}

constructor(config: CoreConfig) {
super(config)

this.analytics = new AnalyticsStateful(this.api, this.eventBuilder)
const { defaults } = config

this.analytics = new AnalyticsStateful(this.api, this.eventBuilder, defaults?.analytics)
}
}

Expand Down
4 changes: 3 additions & 1 deletion platforms/javascript/core/src/CoreStateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ class CoreStateless extends CoreBase {
constructor(config: CoreConfig) {
super(config)

this.analytics = new AnalyticsStateless(this.api, this.eventBuilder)
const { defaults } = config

this.analytics = new AnalyticsStateless(this.api, this.eventBuilder, defaults?.analytics)
}
}

Expand Down
23 changes: 20 additions & 3 deletions platforms/javascript/core/src/analytics/AnalyticsBase.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import type ApiClient from '@contentful/optimization-api-client'
import type { EventBuilder, InsightsEvent } from '@contentful/optimization-api-client'
import type { EventBuilder, InsightsEvent, Profile } from '@contentful/optimization-api-client'
import { logger } from 'logger'
import ProductBase, { type ConsentGuard } from '../ProductBase'
import { consent, effect, profile } from '../signals'
import { consent, effect, type Observable, profile, toObservable } from '../signals'

export interface AnalyticsConfigDefaults {
profile?: Profile
}

export interface AnalyticsStates {
profile: Observable<Profile | undefined>
}

abstract class AnalyticsBase extends ProductBase<InsightsEvent> implements ConsentGuard {
constructor(api: ApiClient, builder: EventBuilder) {
readonly states: AnalyticsStates = {
profile: toObservable(profile),
}

constructor(api: ApiClient, builder: EventBuilder, defaults?: AnalyticsConfigDefaults) {
super(api, builder)

if (defaults?.profile !== undefined) {
const { profile: defaultProfile } = defaults
profile.value = defaultProfile
}

effect(() => {
const id = profile.value?.id

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type ApiClient from '@contentful/optimization-api-client'
import {
type ChangeArray,
type ComponentViewBuilderArgs,
ComponentViewEvent,
type EventBuilder,
type ExperienceEvent,
type Flags,
type IdentifyBuilderArgs,
IdentifyEvent,
type OptimizationData,
type PageViewBuilderArgs,
PageViewEvent,
type ExperienceEvent as PersonalizationEvent,
type Profile,
type SelectedPersonalizationArray,
type TrackBuilderArgs,
TrackEvent,
} from '@contentful/optimization-api-client'
Expand All @@ -24,18 +27,55 @@ import {
consent,
effect,
event as eventSignal,
flags,
flags as flagsSignal,
type Observable,
personalizations,
personalizations as personalizationsSignal,
profile,
profile as profileSignal,
toObservable,
} from '../signals'
import { PersonalizedEntryResolver } from './resolvers'

class Personalization extends ProductBase<ExperienceEvent> implements ConsentGuard {
export interface PersonalizationConfigDefaults {
changes?: ChangeArray
profile?: Profile
personalizations?: SelectedPersonalizationArray
}

export interface PersonalizationStates {
flags: Observable<Flags | undefined>
profile: Observable<Profile | undefined>
personalizations: Observable<SelectedPersonalizationArray | undefined>
}

class Personalization extends ProductBase<PersonalizationEvent> implements ConsentGuard {
readonly personalizedEntryResolver = PersonalizedEntryResolver

constructor(api: ApiClient, builder: EventBuilder) {
readonly states: PersonalizationStates = {
flags: toObservable(flags),
profile: toObservable(profile),
personalizations: toObservable(personalizations),
}

constructor(api: ApiClient, builder: EventBuilder, defaults?: PersonalizationConfigDefaults) {
super(api, builder)

if (defaults) {
const {
changes: defaultChanges,
personalizations: defaultPersonalizations,
profile: defaultProfile,
} = defaults

batch(() => {
changesSignal.value = defaultChanges
personalizationsSignal.value = defaultPersonalizations
profileSignal.value = defaultProfile
})
}

effect(() => {
logger.info(
`[Personalization] Profile ${profileSignal.value && `with ID ${profileSignal.value.id}`} has been ${profileSignal.value ? 'set' : 'cleared'}`,
Expand Down Expand Up @@ -114,7 +154,7 @@ class Personalization extends ProductBase<ExperienceEvent> implements ConsentGua
return await this.#upsertProfile(event)
}

async #upsertProfile(event: ExperienceEvent): Promise<OptimizationData> {
async #upsertProfile(event: PersonalizationEvent): Promise<OptimizationData> {
const intercepted = await this.interceptor.event.run(event)

eventSignal.value = intercepted
Expand Down
20 changes: 20 additions & 0 deletions platforms/javascript/core/src/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ export interface Signals {
personalizations: typeof personalizations
}

export interface Subscription {
unsubscribe: () => void
}

export interface Observable<T> {
subscribe: (next: (v: T) => void) => Subscription
}

export function toObservable<T>(s: { value: T }): Observable<T> {
return {
subscribe(next) {
const dispose = effect(() => {
next(s.value)
})

return { unsubscribe: dispose }
},
}
}

export const signals: Signals = {
changes,
consent,
Expand Down
23 changes: 19 additions & 4 deletions platforms/javascript/react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,28 @@ async function mergeConfig({ defaults, logLevel, ...config }: CoreConfig): Promi
// Initialize AsyncStorage before reading from it
await AsyncStorageStore.initialize()

const {
consent = AsyncStorageStore.consent,
analytics: { profile: analyticsProfile = AsyncStorageStore.profile } = {},
personalization: {
changes = AsyncStorageStore.changes,
profile: personalizationProfile = AsyncStorageStore.profile,
personalizations = AsyncStorageStore.personalizations,
} = {},
} = defaults ?? {}

return merge(
{
defaults: {
changes: AsyncStorageStore.changes ?? defaults?.changes,
consent: AsyncStorageStore.consent ?? defaults?.consent,
profile: AsyncStorageStore.profile ?? defaults?.profile,
personalizations: AsyncStorageStore.personalizations ?? defaults?.personalizations,
consent,
analytics: {
profile: analyticsProfile,
},
personalization: {
changes,
profile: personalizationProfile,
personalizations,
},
},
eventBuilder: {
channel: 'react-native',
Expand Down
Loading