diff --git a/.husky/pre-push b/.husky/pre-push index d979fe1a..a2792afd 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -19,6 +19,9 @@ fi # List workspaces (relative paths) workspaces=$(pnpm ls -r --depth -1 --json | jq -r ".[].path" | sed "s|^$REPO_ROOT/||") +# List implementations (relative names under implementations/) +implementations=$(find implementations -mindepth 1 -maxdepth 1 -type d -exec basename {} \;) + # Get changed files in this push changed_files=$(git diff --name-only $range) @@ -30,24 +33,54 @@ for workspace in $workspaces; do fi done -if [ ${#changed_workspaces[@]} -eq 0 ]; then - echo "No changed workspaces detected for type checking." +# Find changed implementations with tsconfig.json +changed_implementations=() +for implementation in $implementations; do + implementation_path="implementations/$implementation" + if echo "$changed_files" | grep -q "^$implementation_path/"; then + [ -f "$implementation_path/tsconfig.json" ] && changed_implementations+=("$implementation") + fi +done + +if [ ${#changed_workspaces[@]} -eq 0 ] && [ ${#changed_implementations[@]} -eq 0 ]; then + echo "No changed workspaces or implementations detected for checks." exit 0 fi -echo "Type-checking and unit testing changed workspaces:" -for workspace in "${changed_workspaces[@]}"; do - echo " - $workspace" -done +if [ ${#changed_workspaces[@]} -gt 0 ]; then + echo "Type-checking and unit testing changed workspaces:" + for workspace in "${changed_workspaces[@]}"; do + echo " - $workspace" + done -for workspace in "${changed_workspaces[@]}"; do - pnpm --filter "./$workspace" exec tsc --noEmit -done + for workspace in "${changed_workspaces[@]}"; do + pnpm --filter "./$workspace" exec tsc --noEmit + done -echo "✔ Type checking complete." + echo "✔ Workspace type checking complete." -for workspace in "${changed_workspaces[@]}"; do - pnpm --filter "./$workspace" test:unit -done + for workspace in "${changed_workspaces[@]}"; do + pnpm --filter "./$workspace" test:unit + done -echo "✔ Unit testing complete." + echo "✔ Workspace unit testing complete." +fi + +if [ ${#changed_implementations[@]} -gt 0 ]; then + echo "Type-checking and unit testing changed implementations:" + for implementation in "${changed_implementations[@]}"; do + echo " - implementations/$implementation" + done + + for implementation in "${changed_implementations[@]}"; do + pnpm run implementation:run -- "$implementation" typecheck + done + + echo "✔ Implementation type checking complete." + + for implementation in "${changed_implementations[@]}"; do + pnpm run implementation:run -- "$implementation" test:unit + done + + echo "✔ Implementation unit testing complete." +fi diff --git a/README.md b/README.md index dd811451..e58a4dbc 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,10 @@ the SDK hierarchy. ## Shared Internal Libraries -Libraries that are shared internally among Optimization SDKs, and are not currently published, are -located within the `/lib` folder in the project root. +Libraries that are shared internally among Optimization SDKs, and are not currently published. -- [Logger](./lib/logger/README.md) is a simple logging abstraction utility +- Logger functionality is provided through `@contentful/optimization-api-client/logger` and passed + through by each SDK at its own `./logger` entry point - [Mocks](./lib/mocks/README.md) supplies testing fixtures and data, as well as mock server implementations used in both unit and end to end tests throughout the SDK suite diff --git a/implementations/node-ssr-only/src/app.ts b/implementations/node-ssr-only/src/app.ts index 41e88dfd..61e3199d 100644 --- a/implementations/node-ssr-only/src/app.ts +++ b/implementations/node-ssr-only/src/app.ts @@ -1,9 +1,10 @@ -import Optimization, { +import Optimization, { type OptimizationNodeConfig } from '@contentful/optimization-node' +import type { UniversalEventBuilderArgs } from '@contentful/optimization-node/api-client' +import { + isMergeTagEntry, type OptimizationData, - type OptimizationNodeConfig, type SelectedPersonalization, - type UniversalEventBuilderArgs, -} from '@contentful/optimization-node' +} from '@contentful/optimization-node/api-schemas' import { documentToHtmlString } from '@contentful/rich-text-html-renderer' import { INLINES, type Document } from '@contentful/rich-text-types' import type { Entry } from 'contentful' @@ -146,22 +147,22 @@ app.get('/', limiter, async (req, res) => { let optimizationResponse: OptimizationData | undefined = undefined if (isNonEmptyString(userId)) { - const pageResponse = await sdk.personalization.page({ + const pageResponse = await sdk.page({ ...universalEventBuilderArgs, }) - optimizationResponse = await sdk.personalization.identify({ + optimizationResponse = await sdk.identify({ ...universalEventBuilderArgs, userId, traits: { identified: true }, - profile: pageResponse.profile, + profile: pageResponse?.profile, }) } else { - optimizationResponse = await sdk.personalization.page({ + optimizationResponse = await sdk.page({ ...universalEventBuilderArgs, }) } - const { profile, personalizations, changes } = optimizationResponse + const { profile, personalizations, changes } = optimizationResponse ?? {} const personalizedEntries = new Map< string, @@ -180,10 +181,8 @@ app.get('/', limiter, async (req, res) => { entry.fields.text = documentToHtmlString(entry.fields.text, { renderNode: { [INLINES.EMBEDDED_ENTRY]: (node) => { - if (sdk.personalization.mergeTagValueResolver.isMergeTagEntry(node.data.target)) { - return ( - sdk.personalization.mergeTagValueResolver.resolve(node.data.target, profile) ?? '' - ) + if (isMergeTagEntry(node.data.target)) { + return sdk.getMergeTagValue(node.data.target, profile) ?? '' } else { return '' } @@ -192,15 +191,12 @@ app.get('/', limiter, async (req, res) => { }) } - const personalizedEntry = sdk.personalization.personalizedEntryResolver.resolve( - entry, - personalizations, - ) + const personalizedEntry = sdk.personalizeEntry(entry, personalizations) personalizedEntries.set(entryId, personalizedEntry) }) - const flags = sdk.personalization.flagsResolver.resolve(changes) + const flags = sdk.getCustomFlags(changes) const pageData = { profile, diff --git a/implementations/node-ssr-web-vanilla/src/app.ts b/implementations/node-ssr-web-vanilla/src/app.ts index a8f2eb09..68b94c5b 100644 --- a/implementations/node-ssr-web-vanilla/src/app.ts +++ b/implementations/node-ssr-web-vanilla/src/app.ts @@ -1,7 +1,6 @@ -import Optimization, { - type OptimizationData, - type UniversalEventBuilderArgs, -} from '@contentful/optimization-node' +import Optimization from '@contentful/optimization-node' +import type { UniversalEventBuilderArgs } from '@contentful/optimization-node/api-client' +import type { OptimizationData } from '@contentful/optimization-node/api-schemas' import { ANONYMOUS_ID_COOKIE } from '@contentful/optimization-node/constants' import cookieParser from 'cookie-parser' import express, { type Express, type Request, type Response } from 'express' @@ -103,32 +102,35 @@ async function getProfile( req: Request, userId?: string, anonymousId?: string, -): Promise { +): Promise { const sdk = new Optimization(config.optimization) const args = getUniversalEventBuilderArgs(req) const cookieProfile = anonymousId ? { id: anonymousId } : undefined if (!userId) { - return await sdk.personalization.page({ ...args, profile: cookieProfile }) + return await sdk.page({ ...args, profile: cookieProfile }) } - const { profile } = await sdk.personalization.identify({ - ...args, - userId, - profile: cookieProfile, - traits: { identified: true }, - }) + const { profile } = + (await sdk.identify({ + ...args, + userId, + profile: cookieProfile, + traits: { identified: true }, + })) ?? {} + + if (!profile) return - return await sdk.personalization.page({ + return await sdk.page({ ...args, profile: cookieProfile ?? { id: profile.id }, }) } app.get('/', limiter, async (req, res) => { - const { profile } = await getProfile(req) + const { profile } = (await getProfile(req)) ?? {} - respond(res, profile.id) + respond(res, profile?.id ?? '') }) app.get('/smoke-test', limiter, (_, res) => { res.render('index', { config }) @@ -136,9 +138,9 @@ app.get('/smoke-test', limiter, (_, res) => { app.get('/user/:id', limiter, async (req, res) => { const anonymousId = getAnonymousIdFromCookies(req.cookies) const userId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id - const { profile } = await getProfile(req, userId, anonymousId) + const { profile } = (await getProfile(req, userId, anonymousId)) ?? {} - respond(res, profile.id, userId) + respond(res, profile?.id ?? '', userId) }) app.use('/dist', express.static('./public/dist')) diff --git a/implementations/node-ssr-web-vanilla/src/index.ejs b/implementations/node-ssr-web-vanilla/src/index.ejs index bbf00453..7f129138 100644 --- a/implementations/node-ssr-web-vanilla/src/index.ejs +++ b/implementations/node-ssr-web-vanilla/src/index.ejs @@ -219,7 +219,7 @@ }) // Emit page event - optimization.personalization.page() + optimization.page() /* --- Utility Functionality (for E2E) --- */ @@ -328,7 +328,7 @@ const span = document.createElement('span') // This is how we can inject MergeTag data into Rich Text - span.innerText = optimization.personalization.getMergeTagValue(field.data.target) + span.innerText = optimization.getMergeTagValue(field.data.target) parent.appendChild(span) } else { @@ -353,7 +353,7 @@ // Render personalized entries async function renderPersonalizedEntry(rawEntry, element, autoObserve = true) { - const { entry, personalization } = optimization.personalization.personalizeEntry(rawEntry) + const { entry, personalization } = optimization.personalizeEntry(rawEntry) const renderInline = element.dataset.e2eEntryInline === 'true' || element.tagName === 'BUTTON' diff --git a/implementations/react-native/App.tsx b/implementations/react-native/App.tsx index dc98e437..0d0da093 100644 --- a/implementations/react-native/App.tsx +++ b/implementations/react-native/App.tsx @@ -45,7 +45,7 @@ function App(): React.JSX.Element { return } - void sdk.personalization.page({ properties: { url: 'app' } }) + void sdk.page({ properties: { url: 'app' } }) const subscription = sdk.states.profile.subscribe((profile) => { if (!profile) { @@ -63,7 +63,7 @@ function App(): React.JSX.Element { const handleIdentify = (): void => { if (!sdk) return - void sdk.personalization.identify({ userId: 'charles', traits: { identified: true } }) + void sdk.identify({ userId: 'charles', traits: { identified: true } }) setIsIdentified(true) } @@ -71,7 +71,7 @@ function App(): React.JSX.Element { if (!sdk) return sdk.reset() - void sdk.personalization.page({ properties: { url: 'app' } }) + void sdk.page({ properties: { url: 'app' } }) setIsIdentified(false) } diff --git a/implementations/react-native/components/RichTextRenderer.tsx b/implementations/react-native/components/RichTextRenderer.tsx index 6b352d5d..73bea5d0 100644 --- a/implementations/react-native/components/RichTextRenderer.tsx +++ b/implementations/react-native/components/RichTextRenderer.tsx @@ -2,8 +2,8 @@ import React from 'react' import { Text } from 'react-native' import type Optimization from '@contentful/optimization-react-native' -import type { MergeTagEntry } from '@contentful/optimization-react-native' -import { createScopedLogger } from '@contentful/optimization-react-native' +import type { MergeTagEntry } from '@contentful/optimization-react-native/api-schemas' +import { createScopedLogger } from '@contentful/optimization-react-native/logger' import type { Entry } from 'contentful' const logger = createScopedLogger('Demo:RichText') @@ -76,9 +76,9 @@ function convertToString(value: unknown): string { } function resolveMergeTagValue(includedEntry: MergeTagEntry, sdk: Optimization): string { - const resolvedValue = sdk.personalization.getMergeTagValue(includedEntry) + const resolvedValue = sdk.getMergeTagValue(includedEntry) - if (resolvedValue === undefined || resolvedValue === null) { + if (resolvedValue === undefined) { logger.error( `Failed to resolve merge tag: getMergeTagValue returned ${String(resolvedValue)} for merge tag "${includedEntry.fields.nt_name}" (nt_mergetag_id: ${includedEntry.fields.nt_mergetag_id})`, ) diff --git a/implementations/react-native/screens/LiveUpdatesTestScreen.tsx b/implementations/react-native/screens/LiveUpdatesTestScreen.tsx index cdb2d4d7..ae74e087 100644 --- a/implementations/react-native/screens/LiveUpdatesTestScreen.tsx +++ b/implementations/react-native/screens/LiveUpdatesTestScreen.tsx @@ -127,13 +127,13 @@ export function LiveUpdatesTestScreen({ }, []) const handleIdentify = (): void => { - void sdk.personalization.identify({ userId: 'charles', traits: { identified: true } }) + void sdk.identify({ userId: 'charles', traits: { identified: true } }) setIsIdentified(true) } const handleReset = (): void => { sdk.reset() - void sdk.personalization.page({ properties: { url: 'live-updates-test' } }) + void sdk.page({ properties: { url: 'live-updates-test' } }) setIsIdentified(false) } diff --git a/implementations/react-native/sections/ContentEntry.tsx b/implementations/react-native/sections/ContentEntry.tsx index 75728975..b2792b42 100644 --- a/implementations/react-native/sections/ContentEntry.tsx +++ b/implementations/react-native/sections/ContentEntry.tsx @@ -2,11 +2,8 @@ import React from 'react' import { Text, View } from 'react-native' import type Optimization from '@contentful/optimization-react-native' -import { - Analytics, - isPersonalizedEntry, - Personalization, -} from '@contentful/optimization-react-native' +import { Analytics, Personalization } from '@contentful/optimization-react-native' +import { isPersonalizedEntry } from '@contentful/optimization-react-native/api-schemas' import type { Entry } from 'contentful' import { getRichTextContent, RichTextRenderer } from '../components/RichTextRenderer' diff --git a/implementations/react-native/tsconfig.json b/implementations/react-native/tsconfig.json index 86f70875..ec339968 100644 --- a/implementations/react-native/tsconfig.json +++ b/implementations/react-native/tsconfig.json @@ -5,7 +5,7 @@ "baseUrl": "./", "paths": {}, "skipLibCheck": true, - "moduleResolution": "node" + "moduleResolution": "bundler" }, "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules", "android", "ios"] diff --git a/implementations/react-native/utils/sdkHelpers.ts b/implementations/react-native/utils/sdkHelpers.ts index 1f967dc1..84fa3cac 100644 --- a/implementations/react-native/utils/sdkHelpers.ts +++ b/implementations/react-native/utils/sdkHelpers.ts @@ -1,4 +1,5 @@ -import Optimization, { createScopedLogger } from '@contentful/optimization-react-native' +import Optimization from '@contentful/optimization-react-native' +import { createScopedLogger } from '@contentful/optimization-react-native/logger' import AsyncStorage from '@react-native-async-storage/async-storage' import { createClient, type Entry } from 'contentful' import { ENV_CONFIG } from '../env.config' diff --git a/implementations/web-react/src/App.tsx b/implementations/web-react/src/App.tsx index d41e0d6c..e943c8d1 100644 --- a/implementations/web-react/src/App.tsx +++ b/implementations/web-react/src/App.tsx @@ -1,4 +1,4 @@ -import type { Profile } from '@contentful/optimization-web' +import type { Profile } from '@contentful/optimization-web/api-schemas' import { type JSX, useEffect, useMemo, useState } from 'react' import { Link, Navigate, Route, Routes, useLocation } from 'react-router-dom' import { AnalyticsEventDisplay } from './components/AnalyticsEventDisplay' @@ -54,7 +54,7 @@ export default function App({ return } - void sdk.personalization.page({ properties: { url: location.pathname } }) + void sdk.page({ properties: { url: location.pathname } }) }, [isReady, location.pathname, sdk]) useEffect(() => { @@ -99,7 +99,7 @@ export default function App({ return } - void sdk.personalization.identify({ userId: 'charles', traits: { identified: true } }) + void sdk.identify({ userId: 'charles', traits: { identified: true } }) } const handleReset = (): void => { diff --git a/implementations/web-react/src/optimization/hooks/useOptimizationState.ts b/implementations/web-react/src/optimization/hooks/useOptimizationState.ts index 5f98a055..8dd84ad7 100644 --- a/implementations/web-react/src/optimization/hooks/useOptimizationState.ts +++ b/implementations/web-react/src/optimization/hooks/useOptimizationState.ts @@ -1,4 +1,4 @@ -import type { CoreStates } from '@contentful/optimization-web' +import type { CoreStates } from '@contentful/optimization-web/core-sdk' import { useEffect, useState } from 'react' interface Subscription { diff --git a/implementations/web-react/src/optimization/hooks/usePersonalization.ts b/implementations/web-react/src/optimization/hooks/usePersonalization.ts index 0698c29d..a4f49cb6 100644 --- a/implementations/web-react/src/optimization/hooks/usePersonalization.ts +++ b/implementations/web-react/src/optimization/hooks/usePersonalization.ts @@ -1,25 +1,24 @@ -import type Optimization from '@contentful/optimization-web' +import type { + MergeTagEntry, + SelectedPersonalizationArray, +} from '@contentful/optimization-web/api-schemas' +import type { ResolvedData } from '@contentful/optimization-web/core-sdk' +import type { Entry, EntrySkeletonType } from 'contentful' import { useMemo } from 'react' import { useOptimization } from './useOptimization' -type PersonalizationApi = Optimization['personalization'] -type BaselineEntry = Parameters[0] -type PersonalizeResult = ReturnType -export type PersonalizationSelection = Parameters[1] -type MergeTagTarget = Parameters[0] - export interface UsePersonalizationResult { resolveEntry: ( - baselineEntry: BaselineEntry, - selectedPersonalizations?: PersonalizationSelection, - ) => PersonalizeResult - getMergeTagValue: (mergeTagEntry: MergeTagTarget) => string + baselineEntry: Entry, + selectedPersonalizations?: SelectedPersonalizationArray, + ) => ResolvedData + getMergeTagValue: (mergeTagEntry: MergeTagEntry) => string } function fallbackResolveEntry( - baselineEntry: BaselineEntry, - _selectedPersonalizations?: PersonalizationSelection, -): PersonalizeResult { + baselineEntry: Entry, + _selectedPersonalizations?: SelectedPersonalizationArray, +): ResolvedData { return { entry: baselineEntry } } @@ -50,18 +49,19 @@ export function usePersonalization(): UsePersonalizationResult { if (!isReady || sdk === undefined) { return { resolveEntry: fallbackResolveEntry, - getMergeTagValue: (_mergeTagEntry: MergeTagTarget): string => '', + getMergeTagValue: (_mergeTagEntry: MergeTagEntry): string => '', } } return { resolveEntry: ( - baselineEntry: BaselineEntry, - selectedPersonalizations?: PersonalizationSelection, - ): PersonalizeResult => sdk.personalizeEntry(baselineEntry, selectedPersonalizations), + baselineEntry: Entry, + selectedPersonalizations?: SelectedPersonalizationArray, + ): ResolvedData => + sdk.personalizeEntry(baselineEntry, selectedPersonalizations), - getMergeTagValue: (mergeTagEntry: MergeTagTarget): string => - toStringValue(sdk.personalization.getMergeTagValue(mergeTagEntry)), + getMergeTagValue: (mergeTagEntry: MergeTagEntry): string => + toStringValue(sdk.getMergeTagValue(mergeTagEntry)), } }, [isReady, sdk]) } diff --git a/implementations/web-react/src/sections/LiveUpdatesExampleEntry.tsx b/implementations/web-react/src/sections/LiveUpdatesExampleEntry.tsx index 9cd6208a..e1876f42 100644 --- a/implementations/web-react/src/sections/LiveUpdatesExampleEntry.tsx +++ b/implementations/web-react/src/sections/LiveUpdatesExampleEntry.tsx @@ -1,9 +1,7 @@ +import type { SelectedPersonalizationArray } from '@contentful/optimization-web/api-schemas' import { type JSX, useEffect, useMemo, useState } from 'react' import { useOptimization } from '../optimization/hooks/useOptimization' -import { - type PersonalizationSelection, - usePersonalization, -} from '../optimization/hooks/usePersonalization' +import { usePersonalization } from '../optimization/hooks/usePersonalization' import { useLiveUpdates } from '../optimization/liveUpdates/LiveUpdatesContext' import type { ContentfulEntry } from '../types/contentful' @@ -22,7 +20,7 @@ export function LiveUpdatesExampleEntry({ const { resolveEntry } = usePersonalization() const liveUpdatesContext = useLiveUpdates() const [lockedPersonalizations, setLockedPersonalizations] = useState< - PersonalizationSelection | undefined + SelectedPersonalizationArray | undefined >(undefined) const shouldLiveUpdate = diff --git a/implementations/web-vanilla/public/index.html b/implementations/web-vanilla/public/index.html index 3f1fa1d7..b1e658fd 100644 --- a/implementations/web-vanilla/public/index.html +++ b/implementations/web-vanilla/public/index.html @@ -231,7 +231,7 @@

Event Stream

}) // Emit page event - optimization.personalization.page() + optimization.page() /* --- Utility Functionality (for E2E) --- */ @@ -265,7 +265,7 @@

Event Stream

} identify.addEventListener('click', async () => { - await optimization.personalization.identify({ + await optimization.identify({ userId: 'charles', traits: { identified: true }, }) @@ -273,7 +273,7 @@

Event Stream

unidentify.addEventListener('click', async () => { optimization.reset() // Ensure we have a new profile after resetting the old one - await optimization.personalization.page() + await optimization.page() }) const eventStream = document.querySelector('#event-stream') @@ -366,7 +366,7 @@

Event Stream

const span = document.createElement('span') // This is how we can inject MergeTag data into Rich Text - span.innerText = optimization.personalization.getMergeTagValue(field.data.target) + span.innerText = optimization.getMergeTagValue(field.data.target) parent.appendChild(span) } else { @@ -391,7 +391,7 @@

Event Stream

// Render personalized entries async function renderPersonalizedEntry(rawEntry, element, autoObserve = true) { - const { entry, personalization } = optimization.personalization.personalizeEntry(rawEntry) + const { entry, personalization } = optimization.personalizeEntry(rawEntry) const renderInline = element.dataset.e2eEntryInline === 'true' || element.tagName === 'BUTTON' diff --git a/lib/logger/README.md b/lib/logger/README.md deleted file mode 100644 index 136754af..00000000 --- a/lib/logger/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Contentful Optimization SDK Logger Implementation - -> [!WARNING] -> -> This is an internal-only package that is not intended for publishing outside this monorepo - -The Optimization SDK Logger implementation is based on [Diary](https://github.com/maraisr/diary). It -supplies a default `ConsoleLogSink` for logging to the console, and also supports the addition of -other logging sinks. - -## Usage - -```ts -import { logger, LogSink, type LogEvent } from 'logger' - -class MySink extends LogSink { - constructor() { - super() - // Setup code - } - - ingest(event: LogEvent): void { - // Your logging logic - } -} - -const mySink = new MySink() - -logger.addSink(mySink) - -logger.info('Something informative') - -logger.removeSink(mySink.name) -``` diff --git a/lib/logger/package.json b/lib/logger/package.json deleted file mode 100644 index 501065d3..00000000 --- a/lib/logger/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "private": true, - "name": "logger", - "version": "0.0.0", - "license": "MIT", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "import": { - "types": "./dist/index.d.mts", - "default": "./dist/index.mjs" - }, - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } - }, - "./package.json": "./package.json" - }, - "files": [ - "dist/**/*" - ], - "scripts": { - "build": "pnpm clean; pnpm build:dist", - "build:ci": "pnpm build:dist", - "build:dist": "rslib build && build-tools emit-dual-dts ./dist", - "build:rsdoctor": "RSDOCTOR=true rslib build && build-tools emit-dual-dts ./dist", - "clean": "rimraf ./.rslib ./dist ./coverage .tsbuildinfo", - "test:unit": "rstest run --coverage", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "diary": "catalog:" - }, - "devDependencies": { - "build-tools": "workspace:*", - "@microsoft/api-extractor": "catalog:", - "@rsdoctor/cli": "catalog:", - "@rslib/core": "catalog:", - "@types/node": "catalog:", - "@rstest/core": "catalog:", - "@rstest/coverage-istanbul": "catalog:", - "rimraf": "catalog:", - "tslib": "catalog:", - "typescript": "catalog:" - } -} diff --git a/lib/logger/rslib.config.ts b/lib/logger/rslib.config.ts deleted file mode 100644 index 416bcf59..00000000 --- a/lib/logger/rslib.config.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { defineConfig } from '@rslib/core' -import { maybeEnableRsDoctor } from 'build-tools' - -const common = { - bundle: true, - autoExtension: false, - autoExternal: { - dependencies: true, - peerDependencies: true, - optionalDependencies: true, - devDependencies: false, - }, -} as const - -export default defineConfig({ - source: { - tsconfigPath: './tsconfig.build.json', - decorators: { version: '2022-03' }, // stage-3 decorators - }, - - lib: [ - { - ...common, - format: 'esm', - output: { - distPath: { root: 'dist' }, - filename: { js: '[name].mjs' }, - sourceMap: true, - cleanDistPath: true, - }, - - dts: { - bundle: true, - build: false, - }, - - redirect: { - dts: { path: false }, - }, - - tools: { - rspack: maybeEnableRsDoctor, - }, - }, - - { - ...common, - format: 'cjs', - dts: false, - output: { - distPath: { root: 'dist' }, - filename: { js: '[name].cjs' }, - sourceMap: true, - cleanDistPath: false, - }, - - tools: { - rspack: maybeEnableRsDoctor, - }, - }, - ], -}) diff --git a/lib/logger/rstest.config.ts b/lib/logger/rstest.config.ts deleted file mode 100644 index b0c1c5bf..00000000 --- a/lib/logger/rstest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from '@rstest/core' - -const coverageReporters = process.env.CI === 'true' ? ['text-summary', 'lcov'] : ['text', 'html'] - -export default defineConfig({ - include: ['**/*.test.?(c|m)[jt]s?(x)'], - globals: true, - testEnvironment: 'node', - coverage: { - exclude: ['**/test/*'], - include: ['src/**/*'], - reporters: coverageReporters, - }, -}) diff --git a/lib/logger/src/index.ts b/lib/logger/src/index.ts deleted file mode 100644 index f6a4b8cb..00000000 --- a/lib/logger/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Contentful Optimization SDK Logger - provides structured logging with configurable sinks. - * - * @packageDocumentation - */ -import type { LogLevels } from 'diary' - -import { Logger } from './Logger' -import LogSink from './LogSink' - -export * from './ConsoleLogSink' -export * from './Logger' -export * from './LogSink' - -export { LogSink, type LogLevels } - -export default Logger diff --git a/lib/logger/tsconfig.build.json b/lib/logger/tsconfig.build.json deleted file mode 100644 index c4d0ced8..00000000 --- a/lib/logger/tsconfig.build.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "composite": true, - "declarationMap": true, - "incremental": true, - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "noEmit": false, - "outDir": "./dist", - "paths": {}, - "rootDir": "./src", - "types": ["node"], - "tsBuildInfoFile": "./.tsbuildinfo" - }, - "include": ["./src/**/*", "./src/**/*.json"], - "exclude": ["./src/**/*.test.*", "./src/test/**/*"] -} diff --git a/lib/logger/tsconfig.json b/lib/logger/tsconfig.json deleted file mode 100644 index e8de2504..00000000 --- a/lib/logger/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": ["./*.ts", "./src/**/*", "./src/**/*.json"] -} diff --git a/lib/mocks/src/loggerMock.ts b/lib/mocks/src/loggerMock.ts index ab47eabe..eeea16f1 100644 --- a/lib/mocks/src/loggerMock.ts +++ b/lib/mocks/src/loggerMock.ts @@ -214,7 +214,7 @@ export interface LoggerMockModule { } /** - * Creates a mock module for the logger package that includes both the global logger + * Creates a mock module for an SDK logger entry point that includes both the global logger * and the {@link createScopedLogger} factory. The scoped logger routes calls to the * mock logger with the location prepended. * @@ -225,7 +225,7 @@ export interface LoggerMockModule { * ```typescript * import { mockLogger, createLoggerMock } from 'mocks' * - * vi.mock('logger', () => createLoggerMock(mockLogger)) + * vi.mock('@contentful/optimization-core/logger', () => createLoggerMock(mockLogger)) * * // In tests: * expect(mockLogger.info).toHaveBeenCalledWith('ModuleName', 'message') @@ -269,7 +269,7 @@ export function createLoggerMock(logger: MockLogger): LoggerMockModule { * // In setup.ts or test file: * import { loggerMock } from 'mocks' * - * vi.mock('logger', () => loggerMock) + * vi.mock('@contentful/optimization-core/logger', () => loggerMock) * ``` * * @public diff --git a/package.json b/package.json index 428c8a35..21272fbc 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "description": "Contentful Optimization SDK Suite", "license": "MIT", "scripts": { - "build": "pnpm --filter logger build && pnpm --filter @contentful/* --stream build", - "build:ci": "pnpm --filter logger build:ci && pnpm --filter @contentful/* --stream build:ci", + "build": "pnpm --filter @contentful/* --stream build", + "build:ci": "pnpm --filter @contentful/* --stream build:ci", "build:pkgs": "pnpm build && pnpm run pack:pkgs", - "build:rsdoctor": "pnpm --filter logger build:rsdoctor && pnpm --filter @contentful/* --stream build:rsdoctor", + "build:rsdoctor": "pnpm --filter @contentful/* --stream build:rsdoctor", "clean": "pnpm -r --parallel clean", "docs:generate": "typedoc", "docs:watch": "typedoc --watch", @@ -22,6 +22,7 @@ "implementation:web-vanilla": "pnpm run implementation:run -- web-vanilla", "implementation:lint": "eslint implementations --cache --cache-location .cache/eslint/implementations", "implementation:lint:fix": "eslint implementations --fix", + "implementation:typecheck": "pnpm run implementation:run -- --all -- typecheck", "lint": "eslint lib platforms universal --cache --cache-location .cache/eslint/workspace", "lint:fix": "eslint lib platforms universal --fix", "notices:generate": "pnpm --filter \"@contentful/*\" licenses list --prod --json | pnpm dlx @quantco/pnpm-licenses generate-disclaimer --json-input --output-file THIRD_PARTY_NOTICES.txt", diff --git a/platforms/javascript/node/package.json b/platforms/javascript/node/package.json index bc02aecc..ba83d4c7 100644 --- a/platforms/javascript/node/package.json +++ b/platforms/javascript/node/package.json @@ -17,6 +17,16 @@ "default": "./dist/index.cjs" } }, + "./logger": { + "import": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "require": { + "types": "./dist/logger.d.cts", + "default": "./dist/logger.cjs" + } + }, "./constants": { "import": { "types": "./dist/constants.d.mts", @@ -27,6 +37,36 @@ "default": "./dist/constants.cjs" } }, + "./core-sdk": { + "import": { + "types": "./dist/core-sdk.d.mts", + "default": "./dist/core-sdk.mjs" + }, + "require": { + "types": "./dist/core-sdk.d.cts", + "default": "./dist/core-sdk.cjs" + } + }, + "./api-client": { + "import": { + "types": "./dist/api-client.d.mts", + "default": "./dist/api-client.mjs" + }, + "require": { + "types": "./dist/api-client.d.cts", + "default": "./dist/api-client.cjs" + } + }, + "./api-schemas": { + "import": { + "types": "./dist/api-schemas.d.mts", + "default": "./dist/api-schemas.mjs" + }, + "require": { + "types": "./dist/api-schemas.d.cts", + "default": "./dist/api-schemas.cjs" + } + }, "./package.json": "./package.json" }, "files": [ diff --git a/platforms/javascript/node/rslib.config.ts b/platforms/javascript/node/rslib.config.ts index 3923299d..2a535fca 100644 --- a/platforms/javascript/node/rslib.config.ts +++ b/platforms/javascript/node/rslib.config.ts @@ -18,7 +18,11 @@ export default defineConfig({ source: { entry: { index: './src/index.ts', + logger: './src/logger.ts', constants: './src/constants.ts', + 'core-sdk': './src/core-sdk.ts', + 'api-client': './src/api-client.ts', + 'api-schemas': './src/api-schemas.ts', }, tsconfigPath: './tsconfig.build.json', decorators: { version: '2022-03' }, // stage-3 decorators diff --git a/platforms/javascript/node/rstest.config.ts b/platforms/javascript/node/rstest.config.ts index 149fefc0..3f3a75fa 100644 --- a/platforms/javascript/node/rstest.config.ts +++ b/platforms/javascript/node/rstest.config.ts @@ -15,7 +15,6 @@ export default defineConfig({ '../../../universal/api-schemas/src/', ), '@contentful/optimization-core': resolve(__dirname, '../../../universal/core/src/'), - logger: resolve(__dirname, '../../../lib/logger/src/'), }, }, include: ['**/*.test.?(c|m)[jt]s?(x)'], diff --git a/platforms/javascript/node/server.ts b/platforms/javascript/node/server.ts index ef854bda..1559354a 100644 --- a/platforms/javascript/node/server.ts +++ b/platforms/javascript/node/server.ts @@ -1,8 +1,3 @@ -import type { - OptimizationData, - PartialProfile, - SelectedPersonalization, -} from '@contentful/optimization-api-schemas' import { documentToHtmlString } from '@contentful/rich-text-html-renderer' import { type Document, INLINES } from '@contentful/rich-text-types' import type { Entry } from 'contentful' @@ -11,6 +6,8 @@ import express, { type Express } from 'express' import rateLimit from 'express-rate-limit' import path from 'node:path' import Optimization from './src' +import type { OptimizationData, PartialProfile, SelectedPersonalization } from './src/api-schemas' +import { isMergeTagEntry } from './src/api-schemas' const limiter = rateLimit({ windowMs: 30_000, @@ -130,16 +127,16 @@ app.get('/', limiter, async (req, res) => { let apiResponse: OptimizationData | undefined = undefined if (isNonEmptyString(userId)) { - await sdk.personalization.page({ profile: requestProfile }) - apiResponse = await sdk.personalization.identify({ + await sdk.page({ profile: requestProfile }) + apiResponse = await sdk.identify({ userId, profile: requestProfile, }) } else { - apiResponse = await sdk.personalization.page({ profile: requestProfile }) + apiResponse = await sdk.page({ profile: requestProfile }) } - const { profile, personalizations, changes } = apiResponse + const { profile, personalizations, changes } = apiResponse ?? {} const entryIds: string[] = [ '1MwiFl4z7gkwqGYdvCmr8c', // Rich Text field Entry with Merge Tag @@ -169,10 +166,8 @@ app.get('/', limiter, async (req, res) => { entry.fields.text = documentToHtmlString(entry.fields.text, { renderNode: { [INLINES.EMBEDDED_ENTRY]: (node) => { - if (sdk.personalization.mergeTagValueResolver.isMergeTagEntry(node.data.target)) { - return ( - sdk.personalization.mergeTagValueResolver.resolve(node.data.target, profile) ?? '' - ) + if (isMergeTagEntry(node.data.target)) { + return sdk.getMergeTagValue(node.data.target, profile) ?? '' } else { return '' } @@ -181,16 +176,13 @@ app.get('/', limiter, async (req, res) => { }) } - const personalizedEntry = sdk.personalization.personalizedEntryResolver.resolve( - entry, - personalizations, - ) + const personalizedEntry = sdk.personalizeEntry(entry, personalizations) entries.set(entryId, personalizedEntry) }), ) - const flags = sdk.personalization.flagsResolver.resolve(changes) + const flags = sdk.getCustomFlags(changes) const pageData = { consent, diff --git a/platforms/javascript/node/src/Optimization.ts b/platforms/javascript/node/src/Optimization.ts index 32c0b5b8..5961aafc 100644 --- a/platforms/javascript/node/src/Optimization.ts +++ b/platforms/javascript/node/src/Optimization.ts @@ -1,4 +1,5 @@ -import { type App, type CoreStatelessConfig, CoreStateless } from '@contentful/optimization-core' +import { type CoreStatelessConfig, CoreStateless } from '@contentful/optimization-core' +import type { App } from '@contentful/optimization-core/api-schemas' import { merge } from 'es-toolkit' import { OPTIMIZATION_NODE_SDK_NAME, OPTIMIZATION_NODE_SDK_VERSION } from './constants' diff --git a/platforms/javascript/node/src/api-client.ts b/platforms/javascript/node/src/api-client.ts new file mode 100644 index 00000000..ca5423a4 --- /dev/null +++ b/platforms/javascript/node/src/api-client.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core/api-client' diff --git a/platforms/javascript/node/src/api-schemas.ts b/platforms/javascript/node/src/api-schemas.ts new file mode 100644 index 00000000..9fe2852a --- /dev/null +++ b/platforms/javascript/node/src/api-schemas.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core/api-schemas' diff --git a/platforms/javascript/node/src/core-sdk.ts b/platforms/javascript/node/src/core-sdk.ts new file mode 100644 index 00000000..a69a81d2 --- /dev/null +++ b/platforms/javascript/node/src/core-sdk.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core' diff --git a/platforms/javascript/node/src/index.ts b/platforms/javascript/node/src/index.ts index 4b20242a..1107760f 100644 --- a/platforms/javascript/node/src/index.ts +++ b/platforms/javascript/node/src/index.ts @@ -2,16 +2,17 @@ * Contentful Optimization Node SDK. * * @remarks - * Re-exports the full public API of {@link @contentful/optimization-core} and - * adds Node-specific defaults via the {@link Optimization} class. + * Adds Node-specific defaults via the {@link Optimization} class. + * Core and transitive API exports are available from dedicated entrypoints: + * `@contentful/optimization-node/core-sdk`, + * `@contentful/optimization-node/api-client`, + * and `@contentful/optimization-node/api-schemas`. * * @packageDocumentation */ import Optimization from './Optimization' -export * from '@contentful/optimization-core' - export { OPTIMIZATION_NODE_SDK_NAME, OPTIMIZATION_NODE_SDK_VERSION } from './constants' export * from './Optimization' diff --git a/platforms/javascript/node/src/logger.ts b/platforms/javascript/node/src/logger.ts new file mode 100644 index 00000000..6f89dc38 --- /dev/null +++ b/platforms/javascript/node/src/logger.ts @@ -0,0 +1,2 @@ +export * from '@contentful/optimization-core/logger' +export { default } from '@contentful/optimization-core/logger' diff --git a/platforms/javascript/node/tsconfig.build.json b/platforms/javascript/node/tsconfig.build.json index 99101dfb..f9772f22 100644 --- a/platforms/javascript/node/tsconfig.build.json +++ b/platforms/javascript/node/tsconfig.build.json @@ -15,7 +15,6 @@ "include": ["./src/**/*", "./src/**/*.json"], "exclude": ["./src/**/*.test.*", "./src/test/**/*"], "references": [ - { "path": "../../../lib/logger/tsconfig.build.json" }, { "path": "../../../universal/api-schemas/tsconfig.build.json" }, { "path": "../../../universal/api-client/tsconfig.build.json" }, { "path": "../../../universal/core/tsconfig.build.json" } diff --git a/platforms/javascript/react-native/README.md b/platforms/javascript/react-native/README.md index 1ca94c78..ff373bf8 100644 --- a/platforms/javascript/react-native/README.md +++ b/platforms/javascript/react-native/README.md @@ -322,7 +322,7 @@ function MyComponent() { const optimization = useOptimization() const trackManually = async () => { - await optimization.analytics.trackComponentView({ + await optimization.trackComponentView({ componentId: 'my-component', experienceId: 'exp-456', variantIndex: 0, diff --git a/platforms/javascript/react-native/dev/App.tsx b/platforms/javascript/react-native/dev/App.tsx index 9fbb4d3a..dc90180b 100644 --- a/platforms/javascript/react-native/dev/App.tsx +++ b/platforms/javascript/react-native/dev/App.tsx @@ -18,8 +18,8 @@ import { import { SafeAreaView } from 'react-native-safe-area-context' import type Optimization from '@contentful/optimization-react-native' -import type { MergeTagEntry, Profile } from '@contentful/optimization-react-native' import { OptimizationRoot } from '@contentful/optimization-react-native' +import type { MergeTagEntry, Profile } from '@contentful/optimization-react-native/api-schemas' import type { Entry } from 'contentful' import { createClient } from 'contentful' import { LoadingScreen } from './components/LoadingScreen' @@ -205,10 +205,10 @@ function App(): React.JSX.Element { if (includedEntry && isMergeTagEntry(includedEntry)) { mergeTagEntriesList.push(includedEntry) - const resolvedValue = sdk.personalization.getMergeTagValue(includedEntry, profile) - const { nt_mergetag_id: mergeTagId } = includedEntry.fields as { - nt_mergetag_id: string - } + const resolvedValue = sdk.getMergeTagValue(includedEntry, profile) + const { + fields: { nt_mergetag_id: mergeTagId }, + } = includedEntry resolvedValuesList.push({ id: mergeTagId, value: resolvedValue, diff --git a/platforms/javascript/react-native/dev/TestTrackingScreen.tsx b/platforms/javascript/react-native/dev/TestTrackingScreen.tsx index c4fecb5c..3dd1fd1b 100644 --- a/platforms/javascript/react-native/dev/TestTrackingScreen.tsx +++ b/platforms/javascript/react-native/dev/TestTrackingScreen.tsx @@ -72,11 +72,11 @@ export function TestTrackingScreen({ // Listen to the event stream to capture tracking events const subscription = sdk.states.eventStream.subscribe((event) => { if (event?.type === 'component') { - const { componentId } = event as { componentId?: string } + const { componentId } = event const timestamp = new Date().toLocaleTimeString() setTrackedEvents((prev) => [...prev, `${timestamp}: Tracked component "${componentId}"`]) } else if (event?.type === 'screen') { - const { name } = event as { name?: string } + const { name } = event const timestamp = new Date().toLocaleTimeString() setTrackedEvents((prev) => [...prev, `${timestamp}: Tracked screen "${name}"`]) } diff --git a/platforms/javascript/react-native/dev/components/MergeTagDetailCard.tsx b/platforms/javascript/react-native/dev/components/MergeTagDetailCard.tsx index d02d8c4c..a1fafe86 100644 --- a/platforms/javascript/react-native/dev/components/MergeTagDetailCard.tsx +++ b/platforms/javascript/react-native/dev/components/MergeTagDetailCard.tsx @@ -5,7 +5,7 @@ import React from 'react' import { StyleSheet, Text, View } from 'react-native' -import type { MergeTagEntry } from '@contentful/optimization-react-native' +import type { MergeTagEntry } from '@contentful/optimization-react-native/api-schemas' import type { ThemeColors } from '../types' @@ -51,11 +51,7 @@ export function MergeTagDetailCard({ Merge Tag Details {mergeTagDetails.map((mergeTag, index) => { - const tagFields = mergeTag.fields as { - nt_name: string - nt_mergetag_id: string - nt_fallback?: string - } + const { fields: tagFields } = mergeTag return ( diff --git a/platforms/javascript/react-native/dev/metro.config.js b/platforms/javascript/react-native/dev/metro.config.js index 0244a1be..a34367f6 100644 --- a/platforms/javascript/react-native/dev/metro.config.js +++ b/platforms/javascript/react-native/dev/metro.config.js @@ -49,16 +49,6 @@ const config = { } } - // Use browser version of diary package (no Node.js dependencies) - if (moduleName === 'diary') { - return { - filePath: path.resolve( - workspaceRoot, - 'node_modules/.pnpm/diary@0.4.5/node_modules/diary/browser.js', - ), - type: 'sourceFile', - } - } // Let Metro handle everything else return context.resolveRequest(context, moduleName, platform) }, diff --git a/platforms/javascript/react-native/dev/tsconfig.json b/platforms/javascript/react-native/dev/tsconfig.json index 1a935fe2..956d6b1c 100644 --- a/platforms/javascript/react-native/dev/tsconfig.json +++ b/platforms/javascript/react-native/dev/tsconfig.json @@ -7,9 +7,13 @@ "moduleResolution": "node", "paths": { "@contentful/optimization-api-client": ["../../../../universal/api-client/src/index.ts"], + "@contentful/optimization-api-client/*": ["../../../../universal/api-client/src/*"], "@contentful/optimization-api-schemas": ["../../../../universal/api-schemas/src/index.ts"], + "@contentful/optimization-api-schemas/*": ["../../../../universal/api-schemas/src/*"], "@contentful/optimization-core": ["../../../../universal/core/src/index.ts"], - "@contentful/optimization-react-native": ["../src/index.ts"] + "@contentful/optimization-core/*": ["../../../../universal/core/src/*"], + "@contentful/optimization-react-native": ["../src/index.ts"], + "@contentful/optimization-react-native/*": ["../src/*"] } }, "include": ["**/*.ts", "**/*.tsx"], diff --git a/platforms/javascript/react-native/package.json b/platforms/javascript/react-native/package.json index 6c418163..7c9cad18 100644 --- a/platforms/javascript/react-native/package.json +++ b/platforms/javascript/react-native/package.json @@ -22,6 +22,20 @@ "default": "./dist/index.cjs" } }, + "./logger": { + "react-native": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "import": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "require": { + "types": "./dist/logger.d.cts", + "default": "./dist/logger.cjs" + } + }, "./constants": { "react-native": { "types": "./dist/constants.d.mts", @@ -36,6 +50,48 @@ "default": "./dist/constants.cjs" } }, + "./core-sdk": { + "react-native": { + "types": "./dist/core-sdk.d.mts", + "default": "./dist/core-sdk.mjs" + }, + "import": { + "types": "./dist/core-sdk.d.mts", + "default": "./dist/core-sdk.mjs" + }, + "require": { + "types": "./dist/core-sdk.d.cts", + "default": "./dist/core-sdk.cjs" + } + }, + "./api-client": { + "react-native": { + "types": "./dist/api-client.d.mts", + "default": "./dist/api-client.mjs" + }, + "import": { + "types": "./dist/api-client.d.mts", + "default": "./dist/api-client.mjs" + }, + "require": { + "types": "./dist/api-client.d.cts", + "default": "./dist/api-client.cjs" + } + }, + "./api-schemas": { + "react-native": { + "types": "./dist/api-schemas.d.mts", + "default": "./dist/api-schemas.mjs" + }, + "import": { + "types": "./dist/api-schemas.d.mts", + "default": "./dist/api-schemas.mjs" + }, + "require": { + "types": "./dist/api-schemas.d.cts", + "default": "./dist/api-schemas.cjs" + } + }, "./package.json": "./package.json" }, "files": [ diff --git a/platforms/javascript/react-native/rslib.config.ts b/platforms/javascript/react-native/rslib.config.ts index 1c16359a..fe89023f 100644 --- a/platforms/javascript/react-native/rslib.config.ts +++ b/platforms/javascript/react-native/rslib.config.ts @@ -4,10 +4,6 @@ import path from 'node:path' const packageName = getPackageName(__dirname, '@contentful/optimization-react-native') const workspaceRoot = path.resolve(__dirname, '../../..') -const browserDiaryEntry = path.resolve( - workspaceRoot, - 'node_modules/.pnpm/node_modules/diary/browser.js', -) const browserUtilEntry = path.resolve(workspaceRoot, 'node_modules/.pnpm/node_modules/util/util.js') const runtimeExternals = [ @@ -54,7 +50,11 @@ export default defineConfig({ source: { entry: { index: './src/index.ts', + logger: './src/logger.ts', constants: './src/constants.ts', + 'core-sdk': './src/core-sdk.ts', + 'api-client': './src/api-client.ts', + 'api-schemas': './src/api-schemas.ts', }, tsconfigPath: './tsconfig.build.json', decorators: { version: '2022-03' }, // stage-3 decorators @@ -68,7 +68,6 @@ export default defineConfig({ }, resolve: { alias: { - diary$: browserDiaryEntry, util$: browserUtilEntry, }, }, diff --git a/platforms/javascript/react-native/rstest.config.ts b/platforms/javascript/react-native/rstest.config.ts index c12955db..2cfa552c 100644 --- a/platforms/javascript/react-native/rstest.config.ts +++ b/platforms/javascript/react-native/rstest.config.ts @@ -20,7 +20,6 @@ export default defineConfig({ __dirname, './__mocks__/@react-native-community/netinfo.ts', ), - logger: resolve(__dirname, '../../../lib/logger/src/'), 'mocks/loggerMock': resolve(__dirname, '../../../lib/mocks/src/loggerMock.ts'), 'react-native': resolve(__dirname, './src/test/reactNativeShim.ts'), }, diff --git a/platforms/javascript/react-native/src/api-client.ts b/platforms/javascript/react-native/src/api-client.ts new file mode 100644 index 00000000..ca5423a4 --- /dev/null +++ b/platforms/javascript/react-native/src/api-client.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core/api-client' diff --git a/platforms/javascript/react-native/src/api-schemas.ts b/platforms/javascript/react-native/src/api-schemas.ts new file mode 100644 index 00000000..9fe2852a --- /dev/null +++ b/platforms/javascript/react-native/src/api-schemas.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core/api-schemas' diff --git a/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.test.tsx b/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.test.tsx index 2f111ba1..05995dec 100644 --- a/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.test.tsx +++ b/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.test.tsx @@ -19,8 +19,8 @@ rs.mock('@react-native-async-storage/async-storage', () => ({ }, })) -// Mock @contentful/optimization-core -rs.mock('@contentful/optimization-core', () => ({ +// Mock @contentful/optimization-core/logger +rs.mock('@contentful/optimization-core/logger', () => ({ logger: { info: rs.fn(), debug: rs.fn(), diff --git a/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.tsx b/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.tsx index 59985123..3db63172 100644 --- a/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.tsx +++ b/platforms/javascript/react-native/src/components/OptimizationNavigationContainer.tsx @@ -1,4 +1,4 @@ -import type { Properties } from '@contentful/optimization-core' +import type { Properties } from '@contentful/optimization-core/api-schemas' import type React from 'react' import { useCallback, useRef } from 'react' import * as z from 'zod/mini' diff --git a/platforms/javascript/react-native/src/components/OptimizationProvider.tsx b/platforms/javascript/react-native/src/components/OptimizationProvider.tsx index d477b780..b98f4c1d 100644 --- a/platforms/javascript/react-native/src/components/OptimizationProvider.tsx +++ b/platforms/javascript/react-native/src/components/OptimizationProvider.tsx @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import React, { type ReactNode } from 'react' import type Optimization from '../' import OptimizationContext from '../context/OptimizationContext' diff --git a/platforms/javascript/react-native/src/components/Personalization.tsx b/platforms/javascript/react-native/src/components/Personalization.tsx index e1b92c4a..09a87ca9 100644 --- a/platforms/javascript/react-native/src/components/Personalization.tsx +++ b/platforms/javascript/react-native/src/components/Personalization.tsx @@ -1,4 +1,5 @@ -import type { ResolvedData, SelectedPersonalizationArray } from '@contentful/optimization-core' +import type { ResolvedData } from '@contentful/optimization-core' +import type { SelectedPersonalizationArray } from '@contentful/optimization-core/api-schemas' import type { Entry, EntrySkeletonType } from 'contentful' import React, { useEffect, useMemo, useState, type ReactNode } from 'react' import { View, type StyleProp, type ViewStyle } from 'react-native' @@ -188,7 +189,7 @@ export function Personalization({ }, [optimization, shouldLiveUpdate, lockedPersonalizations]) const resolvedData: ResolvedData = useMemo( - () => optimization.personalization.personalizeEntry(baselineEntry, lockedPersonalizations), + () => optimization.personalizeEntry(baselineEntry, lockedPersonalizations), [baselineEntry, optimization, lockedPersonalizations], ) diff --git a/platforms/javascript/react-native/src/context/OptimizationContext.tsx b/platforms/javascript/react-native/src/context/OptimizationContext.tsx index fa6dafa9..5d3120c5 100644 --- a/platforms/javascript/react-native/src/context/OptimizationContext.tsx +++ b/platforms/javascript/react-native/src/context/OptimizationContext.tsx @@ -28,7 +28,7 @@ const OptimizationContext = createContext(null) * const optimization = useOptimization() * * const handlePress = async () => { - * await optimization.analytics.trackComponentView({ + * await optimization.trackComponentView({ * componentId: 'my-component', * variantIndex: 0, * }) diff --git a/platforms/javascript/react-native/src/context/OptimizationScrollContext.tsx b/platforms/javascript/react-native/src/context/OptimizationScrollContext.tsx index e72f2890..48ed10d9 100644 --- a/platforms/javascript/react-native/src/context/OptimizationScrollContext.tsx +++ b/platforms/javascript/react-native/src/context/OptimizationScrollContext.tsx @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import React, { createContext, useCallback, useContext, useState, type ReactNode } from 'react' import { ScrollView, diff --git a/platforms/javascript/react-native/src/core-sdk.ts b/platforms/javascript/react-native/src/core-sdk.ts new file mode 100644 index 00000000..a69a81d2 --- /dev/null +++ b/platforms/javascript/react-native/src/core-sdk.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core' diff --git a/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.test.ts b/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.test.ts index 61c23446..8a1fcdb6 100644 --- a/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.test.ts +++ b/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.test.ts @@ -40,7 +40,7 @@ describe('createAppStateChangeListener', () => { rs.resetModules() // Set up mocks before each test - rs.doMock('@contentful/optimization-core', () => createLoggerMock(mockLogger)) + rs.doMock('@contentful/optimization-core/logger', () => createLoggerMock(mockLogger)) rs.doMock('react-native', () => ({ AppState: { diff --git a/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.ts b/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.ts index 89b064a1..b1acb620 100644 --- a/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.ts +++ b/platforms/javascript/react-native/src/handlers/createAppStateChangeListener.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { AppState } from 'react-native' const logger = createScopedLogger('RN:AppState') diff --git a/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.test.ts b/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.test.ts index 3ea296ec..2d605a3d 100644 --- a/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.test.ts +++ b/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.test.ts @@ -21,7 +21,7 @@ describe('createOnlineChangeListener', () => { rs.clearAllMocks() rs.resetModules() - rs.doMock('@contentful/optimization-core', () => createLoggerMock(mockLogger)) + rs.doMock('@contentful/optimization-core/logger', () => createLoggerMock(mockLogger)) }) describe('when NetInfo is not installed', () => { diff --git a/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.ts b/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.ts index f0b11a80..eb07dc2f 100644 --- a/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.ts +++ b/platforms/javascript/react-native/src/handlers/createOnlineChangeListener.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' const logger = createScopedLogger('RN:Network') diff --git a/platforms/javascript/react-native/src/hooks/useScreenTracking.test.ts b/platforms/javascript/react-native/src/hooks/useScreenTracking.test.ts index 27e0f110..bcfcd995 100644 --- a/platforms/javascript/react-native/src/hooks/useScreenTracking.test.ts +++ b/platforms/javascript/react-native/src/hooks/useScreenTracking.test.ts @@ -19,8 +19,8 @@ rs.mock('@react-native-async-storage/async-storage', () => ({ }, })) -// Mock @contentful/optimization-core -rs.mock('@contentful/optimization-core', () => ({ +// Mock @contentful/optimization-core/logger +rs.mock('@contentful/optimization-core/logger', () => ({ logger: { info: rs.fn(), debug: rs.fn(), diff --git a/platforms/javascript/react-native/src/hooks/useScreenTracking.ts b/platforms/javascript/react-native/src/hooks/useScreenTracking.ts index af7015d8..eb6d6de7 100644 --- a/platforms/javascript/react-native/src/hooks/useScreenTracking.ts +++ b/platforms/javascript/react-native/src/hooks/useScreenTracking.ts @@ -1,8 +1,5 @@ -import { - createScopedLogger, - type OptimizationData, - type Properties, -} from '@contentful/optimization-core' +import type { OptimizationData, Properties } from '@contentful/optimization-core/api-schemas' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { useCallback, useEffect, useRef } from 'react' import { useOptimization } from '../context/OptimizationContext' diff --git a/platforms/javascript/react-native/src/hooks/useViewportTracking.ts b/platforms/javascript/react-native/src/hooks/useViewportTracking.ts index f5495575..b6dbcc35 100644 --- a/platforms/javascript/react-native/src/hooks/useViewportTracking.ts +++ b/platforms/javascript/react-native/src/hooks/useViewportTracking.ts @@ -1,4 +1,5 @@ -import { createScopedLogger, type SelectedPersonalization } from '@contentful/optimization-core' +import type { SelectedPersonalization } from '@contentful/optimization-core/api-schemas' +import { createScopedLogger } from '@contentful/optimization-core/logger' import type { Entry } from 'contentful' import { useCallback, useEffect, useRef, useState } from 'react' import { Dimensions, type LayoutChangeEvent } from 'react-native' @@ -213,7 +214,7 @@ export function useViewportTracking({ // Track the component view void (async () => { - await currentOptimization.analytics.trackComponentView({ + await currentOptimization.trackComponentView({ componentId, componentViewId, experienceId, diff --git a/platforms/javascript/react-native/src/index.ts b/platforms/javascript/react-native/src/index.ts index 857ece3d..f256174e 100644 --- a/platforms/javascript/react-native/src/index.ts +++ b/platforms/javascript/react-native/src/index.ts @@ -209,8 +209,6 @@ class Optimization extends CoreStateful { } } -export * from '@contentful/optimization-core' - export { OptimizationProvider } from './components/OptimizationProvider' export { OptimizationRoot } from './components/OptimizationRoot' export type { OptimizationRootProps, PreviewPanelConfig } from './components/OptimizationRoot' diff --git a/platforms/javascript/react-native/src/logger.ts b/platforms/javascript/react-native/src/logger.ts new file mode 100644 index 00000000..6f89dc38 --- /dev/null +++ b/platforms/javascript/react-native/src/logger.ts @@ -0,0 +1,2 @@ +export * from '@contentful/optimization-core/logger' +export { default } from '@contentful/optimization-core/logger' diff --git a/platforms/javascript/react-native/src/preview/components/PreviewPanel.tsx b/platforms/javascript/react-native/src/preview/components/PreviewPanel.tsx index 775d006b..9c7badb6 100644 --- a/platforms/javascript/react-native/src/preview/components/PreviewPanel.tsx +++ b/platforms/javascript/react-native/src/preview/components/PreviewPanel.tsx @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import React, { useCallback, useEffect, useState } from 'react' import { Alert, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' diff --git a/platforms/javascript/react-native/src/preview/hooks/useContentfulEntries.ts b/platforms/javascript/react-native/src/preview/hooks/useContentfulEntries.ts index 3818cb14..020d7d47 100644 --- a/platforms/javascript/react-native/src/preview/hooks/useContentfulEntries.ts +++ b/platforms/javascript/react-native/src/preview/hooks/useContentfulEntries.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { useEffect, useState } from 'react' import type { ContentfulClient, ContentfulEntry } from '../types' import { fetchAudienceAndExperienceEntries } from '../utils' diff --git a/platforms/javascript/react-native/src/preview/hooks/usePreviewState.ts b/platforms/javascript/react-native/src/preview/hooks/usePreviewState.ts index cc05af86..dbabe86b 100644 --- a/platforms/javascript/react-native/src/preview/hooks/usePreviewState.ts +++ b/platforms/javascript/react-native/src/preview/hooks/usePreviewState.ts @@ -1,8 +1,5 @@ -import { - createScopedLogger, - type Profile, - type SelectedPersonalization, -} from '@contentful/optimization-core' +import type { Profile, SelectedPersonalization } from '@contentful/optimization-core/api-schemas' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { useEffect, useState } from 'react' import { useOptimization } from '../../context/OptimizationContext' import type { PreviewState } from '../types' diff --git a/platforms/javascript/react-native/src/preview/hooks/useProfileOverrides.ts b/platforms/javascript/react-native/src/preview/hooks/useProfileOverrides.ts index ef6b9aed..f5ce34ff 100644 --- a/platforms/javascript/react-native/src/preview/hooks/useProfileOverrides.ts +++ b/platforms/javascript/react-native/src/preview/hooks/useProfileOverrides.ts @@ -1,10 +1,9 @@ -import { - createScopedLogger, - type OptimizationData, - type PreviewPanelSignalObject, - type SelectedPersonalizationArray, - type Signals, -} from '@contentful/optimization-core' +import type { PreviewPanelSignalObject, Signals } from '@contentful/optimization-core' +import type { + OptimizationData, + SelectedPersonalizationArray, +} from '@contentful/optimization-core/api-schemas' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { PREVIEW_PANEL_SIGNALS_SYMBOL } from '@contentful/optimization-core/symbols' import { useCallback, useEffect, useRef, useState } from 'react' import { useOptimization } from '../../context/OptimizationContext' diff --git a/platforms/javascript/react-native/src/preview/types.ts b/platforms/javascript/react-native/src/preview/types.ts index 5a7de8c4..4a3085b7 100644 --- a/platforms/javascript/react-native/src/preview/types.ts +++ b/platforms/javascript/react-native/src/preview/types.ts @@ -1,4 +1,4 @@ -import type { Profile, SelectedPersonalization } from '@contentful/optimization-core' +import type { Profile, SelectedPersonalization } from '@contentful/optimization-core/api-schemas' import type { StyleProp, ViewStyle } from 'react-native' // ============================================================================ diff --git a/platforms/javascript/react-native/src/preview/utils/contentfulUtils.ts b/platforms/javascript/react-native/src/preview/utils/contentfulUtils.ts index be287bd9..f394d99d 100644 --- a/platforms/javascript/react-native/src/preview/utils/contentfulUtils.ts +++ b/platforms/javascript/react-native/src/preview/utils/contentfulUtils.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import type { ContentfulClient, ContentfulEntry } from '../types' const logger = createScopedLogger('RN:Preview') diff --git a/platforms/javascript/react-native/src/storage/AsyncStorageStore.ts b/platforms/javascript/react-native/src/storage/AsyncStorageStore.ts index f2606e0b..b6272052 100644 --- a/platforms/javascript/react-native/src/storage/AsyncStorageStore.ts +++ b/platforms/javascript/react-native/src/storage/AsyncStorageStore.ts @@ -1,9 +1,8 @@ import { ChangeArray, - createScopedLogger, Profile, SelectedPersonalizationArray, -} from '@contentful/optimization-core' +} from '@contentful/optimization-core/api-schemas' import { ANONYMOUS_ID_KEY, CHANGES_CACHE_KEY, @@ -12,6 +11,7 @@ import { PERSONALIZATIONS_CACHE_KEY, PROFILE_CACHE_KEY, } from '@contentful/optimization-core/constants' +import { createScopedLogger } from '@contentful/optimization-core/logger' import AsyncStorage from '@react-native-async-storage/async-storage' import type { z } from 'zod/mini' diff --git a/platforms/javascript/react-native/tsconfig.build.json b/platforms/javascript/react-native/tsconfig.build.json index ee14331d..bad4aa6d 100644 --- a/platforms/javascript/react-native/tsconfig.build.json +++ b/platforms/javascript/react-native/tsconfig.build.json @@ -16,7 +16,6 @@ "include": ["./src/**/*", "./src/**/*.json"], "exclude": ["./src/**/*.test.*", "./src/test/**/*"], "references": [ - { "path": "../../../lib/logger/tsconfig.build.json" }, { "path": "../../../universal/api-schemas/tsconfig.build.json" }, { "path": "../../../universal/api-client/tsconfig.build.json" }, { "path": "../../../universal/core/tsconfig.build.json" } diff --git a/platforms/javascript/web-frameworks/react-web/package.json b/platforms/javascript/web-frameworks/react-web/package.json index b444b2e8..984b716f 100644 --- a/platforms/javascript/web-frameworks/react-web/package.json +++ b/platforms/javascript/web-frameworks/react-web/package.json @@ -17,6 +17,56 @@ "default": "./dist/index.cjs" } }, + "./logger": { + "import": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "require": { + "types": "./dist/logger.d.cts", + "default": "./dist/logger.cjs" + } + }, + "./web-sdk": { + "import": { + "types": "./dist/web-sdk.d.mts", + "default": "./dist/web-sdk.mjs" + }, + "require": { + "types": "./dist/web-sdk.d.cts", + "default": "./dist/web-sdk.cjs" + } + }, + "./core-sdk": { + "import": { + "types": "./dist/core-sdk.d.mts", + "default": "./dist/core-sdk.mjs" + }, + "require": { + "types": "./dist/core-sdk.d.cts", + "default": "./dist/core-sdk.cjs" + } + }, + "./api-client": { + "import": { + "types": "./dist/api-client.d.mts", + "default": "./dist/api-client.mjs" + }, + "require": { + "types": "./dist/api-client.d.cts", + "default": "./dist/api-client.cjs" + } + }, + "./api-schemas": { + "import": { + "types": "./dist/api-schemas.d.mts", + "default": "./dist/api-schemas.mjs" + }, + "require": { + "types": "./dist/api-schemas.d.cts", + "default": "./dist/api-schemas.cjs" + } + }, "./package.json": "./package.json" }, "files": [ diff --git a/platforms/javascript/web-frameworks/react-web/rslib.config.ts b/platforms/javascript/web-frameworks/react-web/rslib.config.ts index c5c264cf..4bcb81fd 100644 --- a/platforms/javascript/web-frameworks/react-web/rslib.config.ts +++ b/platforms/javascript/web-frameworks/react-web/rslib.config.ts @@ -22,6 +22,14 @@ const common = { export default defineConfig({ source: { + entry: { + index: './src/index.ts', + logger: './src/logger.ts', + 'web-sdk': './src/web-sdk.ts', + 'core-sdk': './src/core-sdk.ts', + 'api-client': './src/api-client.ts', + 'api-schemas': './src/api-schemas.ts', + }, tsconfigPath: './tsconfig.build.json', define: { __OPTIMIZATION_VERSION__: JSON.stringify(process.env.RELEASE_VERSION ?? '0.0.0'), diff --git a/platforms/javascript/web-frameworks/react-web/rstest.config.ts b/platforms/javascript/web-frameworks/react-web/rstest.config.ts index bfead847..fb6e52aa 100644 --- a/platforms/javascript/web-frameworks/react-web/rstest.config.ts +++ b/platforms/javascript/web-frameworks/react-web/rstest.config.ts @@ -18,7 +18,6 @@ export default defineConfig({ ), '@contentful/optimization-core': resolve(__dirname, '../../../../universal/core/src/'), '@contentful/optimization-web': resolve(__dirname, '../../web/src/'), - logger: resolve(__dirname, '../../../../lib/logger/src/'), }, }, include: ['**/*.test.?(c|m)[jt]s?(x)'], diff --git a/platforms/javascript/web-frameworks/react-web/src/api-client.ts b/platforms/javascript/web-frameworks/react-web/src/api-client.ts new file mode 100644 index 00000000..928eb241 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/api-client.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-web/api-client' diff --git a/platforms/javascript/web-frameworks/react-web/src/api-schemas.ts b/platforms/javascript/web-frameworks/react-web/src/api-schemas.ts new file mode 100644 index 00000000..d8be5c46 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/api-schemas.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-web/api-schemas' diff --git a/platforms/javascript/web-frameworks/react-web/src/core-sdk.ts b/platforms/javascript/web-frameworks/react-web/src/core-sdk.ts new file mode 100644 index 00000000..99c0aaf1 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/core-sdk.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-web/core-sdk' diff --git a/platforms/javascript/web-frameworks/react-web/src/logger.ts b/platforms/javascript/web-frameworks/react-web/src/logger.ts new file mode 100644 index 00000000..4c202e40 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/logger.ts @@ -0,0 +1,2 @@ +export * from '@contentful/optimization-web/logger' +export { default } from '@contentful/optimization-web/logger' diff --git a/platforms/javascript/web-frameworks/react-web/src/web-sdk.ts b/platforms/javascript/web-frameworks/react-web/src/web-sdk.ts new file mode 100644 index 00000000..d34efec1 --- /dev/null +++ b/platforms/javascript/web-frameworks/react-web/src/web-sdk.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-web' diff --git a/platforms/javascript/web-preview-panel/index.html b/platforms/javascript/web-preview-panel/index.html index 1b43a2d7..4c81e213 100644 --- a/platforms/javascript/web-preview-panel/index.html +++ b/platforms/javascript/web-preview-panel/index.html @@ -275,7 +275,7 @@

Entries

}) // Emit page event - optimization.personalization.page() + optimization.page() /* --- Utility Functionality --- */ @@ -309,7 +309,7 @@

Entries

} identify.addEventListener('click', async () => { - await optimization.personalization.identify({ + await optimization.identify({ userId: 'charles', traits: { identified: true }, }) @@ -317,7 +317,7 @@

Entries

unidentify.addEventListener('click', async () => { optimization.reset() // Ensure we have a new profile after resetting the old one - await optimization.personalization.page() + await optimization.page() }) const eventStream = document.querySelector('#event-stream') @@ -461,7 +461,7 @@

Entries

const span = document.createElement('span') // This is how we can inject MergeTag data into Rich Text - span.innerText = optimization.personalization.getMergeTagValue(field.data.target) + span.innerText = optimization.getMergeTagValue(field.data.target) parent.appendChild(span) } else { @@ -475,7 +475,7 @@

Entries

// Render personalized entries async function renderPersonalizedEntry(rawEntry, element, autoObserve = true) { - const { entry, personalization } = optimization.personalization.personalizeEntry(rawEntry) + const { entry, personalization } = optimization.personalizeEntry(rawEntry) if (isRichText(entry.fields?.text)) { const div = document.createElement('div') diff --git a/platforms/javascript/web-preview-panel/package.json b/platforms/javascript/web-preview-panel/package.json index 75fa1097..bf22778f 100644 --- a/platforms/javascript/web-preview-panel/package.json +++ b/platforms/javascript/web-preview-panel/package.json @@ -17,6 +17,16 @@ "default": "./dist/index.cjs" } }, + "./logger": { + "import": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "require": { + "types": "./dist/logger.d.cts", + "default": "./dist/logger.cjs" + } + }, "./constants": { "import": { "types": "./dist/constants.d.mts", diff --git a/platforms/javascript/web-preview-panel/rslib.config.ts b/platforms/javascript/web-preview-panel/rslib.config.ts index 3084b97a..df1a33d9 100644 --- a/platforms/javascript/web-preview-panel/rslib.config.ts +++ b/platforms/javascript/web-preview-panel/rslib.config.ts @@ -55,6 +55,7 @@ export default defineConfig({ source: { entry: { index: './src/index.ts', + logger: './src/logger.ts', constants: './src/constants.ts', }, }, @@ -86,6 +87,7 @@ export default defineConfig({ source: { entry: { index: './src/index.ts', + logger: './src/logger.ts', constants: './src/constants.ts', }, }, diff --git a/platforms/javascript/web-preview-panel/src/attachOptimizationPreviewPanel.ts b/platforms/javascript/web-preview-panel/src/attachOptimizationPreviewPanel.ts index 39d7703d..bc73fa78 100644 --- a/platforms/javascript/web-preview-panel/src/attachOptimizationPreviewPanel.ts +++ b/platforms/javascript/web-preview-panel/src/attachOptimizationPreviewPanel.ts @@ -4,9 +4,9 @@ import type { OptimizationData, PersonalizationEntry, PersonalizationEntrySkeleton, - PreviewPanelSignalObject, SelectedPersonalizationArray, -} from '@contentful/optimization-web' +} from '@contentful/optimization-web/api-schemas' +import type { PreviewPanelSignalObject } from '@contentful/optimization-web/core-sdk' import { PREVIEW_PANEL_SIGNAL_FNS_SYMBOL, PREVIEW_PANEL_SIGNALS_SYMBOL, diff --git a/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-audience.ts b/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-audience.ts index dae0a603..7094d902 100644 --- a/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-audience.ts +++ b/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-audience.ts @@ -3,7 +3,7 @@ import type { PersonalizationEntry, Profile, SelectedPersonalizationArray, -} from '@contentful/optimization-web' +} from '@contentful/optimization-web/api-schemas' import { consume } from '@lit/context' import { css, html, LitElement, nothing, type PropertyValues, type TemplateResult } from 'lit' import { property, state } from 'lit/decorators.js' diff --git a/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-panel.ts b/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-panel.ts index 307c76fd..096bc694 100644 --- a/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-panel.ts +++ b/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-panel.ts @@ -3,7 +3,7 @@ import type { PersonalizationEntry, Profile, SelectedPersonalizationArray, -} from '@contentful/optimization-web' +} from '@contentful/optimization-web/api-schemas' import { provide } from '@lit/context' import { groupBy } from 'es-toolkit' import { css, html, LitElement, type PropertyValues, type TemplateResult } from 'lit' diff --git a/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-personalization.ts b/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-personalization.ts index 52aaaa5e..a1f73927 100644 --- a/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-personalization.ts +++ b/platforms/javascript/web-preview-panel/src/components/ctfl-opt-preview-personalization.ts @@ -1,4 +1,4 @@ -import type { PersonalizationEntry } from '@contentful/optimization-web' +import type { PersonalizationEntry } from '@contentful/optimization-web/api-schemas' import { css, html, LitElement, nothing, type TemplateResult } from 'lit' import { property } from 'lit/decorators.js' diff --git a/platforms/javascript/web-preview-panel/src/constants.ts b/platforms/javascript/web-preview-panel/src/constants.ts index 38539dfa..9e595122 100644 --- a/platforms/javascript/web-preview-panel/src/constants.ts +++ b/platforms/javascript/web-preview-panel/src/constants.ts @@ -1,4 +1,4 @@ -import type { AudienceEntry } from '@contentful/optimization-web' +import type { AudienceEntry } from '@contentful/optimization-web/api-schemas' /** * Identifier for the synthetic "All Visitors" audience used as a fallback. diff --git a/platforms/javascript/web-preview-panel/src/lib/contexts.ts b/platforms/javascript/web-preview-panel/src/lib/contexts.ts index e58d056c..82d0bea9 100644 --- a/platforms/javascript/web-preview-panel/src/lib/contexts.ts +++ b/platforms/javascript/web-preview-panel/src/lib/contexts.ts @@ -1,4 +1,5 @@ -import type { Profile, SignalFns, Signals } from '@contentful/optimization-web' +import type { Profile } from '@contentful/optimization-web/api-schemas' +import type { SignalFns, Signals } from '@contentful/optimization-web/core-sdk' import { type Context, createContext } from '@lit/context' /** diff --git a/platforms/javascript/web-preview-panel/src/lib/entries.ts b/platforms/javascript/web-preview-panel/src/lib/entries.ts index 0b9e2be3..63656128 100644 --- a/platforms/javascript/web-preview-panel/src/lib/entries.ts +++ b/platforms/javascript/web-preview-panel/src/lib/entries.ts @@ -1,4 +1,4 @@ -import type { AudienceEntry, PersonalizationEntry } from '@contentful/optimization-web' +import type { AudienceEntry, PersonalizationEntry } from '@contentful/optimization-web/api-schemas' import type { ChainModifiers, ContentfulClientApi, diff --git a/platforms/javascript/web-preview-panel/src/lib/overrides.ts b/platforms/javascript/web-preview-panel/src/lib/overrides.ts index e6bab24a..dde5381b 100644 --- a/platforms/javascript/web-preview-panel/src/lib/overrides.ts +++ b/platforms/javascript/web-preview-panel/src/lib/overrides.ts @@ -1,4 +1,4 @@ -import type { SelectedPersonalizationArray } from '@contentful/optimization-web' +import type { SelectedPersonalizationArray } from '@contentful/optimization-web/api-schemas' /** * Merges user-selected variant overrides into the given selected personalizations. diff --git a/platforms/javascript/web-preview-panel/src/logger.ts b/platforms/javascript/web-preview-panel/src/logger.ts new file mode 100644 index 00000000..4c202e40 --- /dev/null +++ b/platforms/javascript/web-preview-panel/src/logger.ts @@ -0,0 +1,2 @@ +export * from '@contentful/optimization-web/logger' +export { default } from '@contentful/optimization-web/logger' diff --git a/platforms/javascript/web/index.html b/platforms/javascript/web/index.html index 0109572c..01e0bb5b 100644 --- a/platforms/javascript/web/index.html +++ b/platforms/javascript/web/index.html @@ -269,7 +269,7 @@

Entries

}) // Emit page event - optimization.personalization.page() + optimization.page() /* --- Utility Functionality --- */ @@ -303,7 +303,7 @@

Entries

} identify.addEventListener('click', async () => { - await optimization.personalization.identify({ + await optimization.identify({ userId: 'charles', traits: { identified: true }, }) @@ -311,7 +311,7 @@

Entries

unidentify.addEventListener('click', async () => { optimization.reset() // Ensure we have a new profile after resetting the old one - await optimization.personalization.page() + await optimization.page() }) const eventStream = document.querySelector('#event-stream') @@ -455,7 +455,7 @@

Entries

const span = document.createElement('span') // This is how we can inject MergeTag data into Rich Text - span.innerText = optimization.personalization.getMergeTagValue(field.data.target) + span.innerText = optimization.getMergeTagValue(field.data.target) parent.appendChild(span) } else { @@ -469,7 +469,7 @@

Entries

// Render personalized entries async function renderPersonalizedEntry(rawEntry, element, autoObserve = true) { - const { entry, personalization } = optimization.personalization.personalizeEntry(rawEntry) + const { entry, personalization } = optimization.personalizeEntry(rawEntry) if (isRichText(entry.fields?.text)) { const div = document.createElement('div') diff --git a/platforms/javascript/web/package.json b/platforms/javascript/web/package.json index eeee02dd..68b62cea 100644 --- a/platforms/javascript/web/package.json +++ b/platforms/javascript/web/package.json @@ -17,6 +17,16 @@ "default": "./dist/index.cjs" } }, + "./logger": { + "import": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "require": { + "types": "./dist/logger.d.cts", + "default": "./dist/logger.cjs" + } + }, "./constants": { "import": { "types": "./dist/constants.d.mts", @@ -36,6 +46,36 @@ "types": "./dist/symbols.d.cts", "default": "./dist/symbols.cjs" } + }, + "./core-sdk": { + "import": { + "types": "./dist/core-sdk.d.mts", + "default": "./dist/core-sdk.mjs" + }, + "require": { + "types": "./dist/core-sdk.d.cts", + "default": "./dist/core-sdk.cjs" + } + }, + "./api-client": { + "import": { + "types": "./dist/api-client.d.mts", + "default": "./dist/api-client.mjs" + }, + "require": { + "types": "./dist/api-client.d.cts", + "default": "./dist/api-client.cjs" + } + }, + "./api-schemas": { + "import": { + "types": "./dist/api-schemas.d.mts", + "default": "./dist/api-schemas.mjs" + }, + "require": { + "types": "./dist/api-schemas.d.cts", + "default": "./dist/api-schemas.cjs" + } } }, "files": [ diff --git a/platforms/javascript/web/rslib.config.ts b/platforms/javascript/web/rslib.config.ts index e1b36e32..2cf427ed 100644 --- a/platforms/javascript/web/rslib.config.ts +++ b/platforms/javascript/web/rslib.config.ts @@ -54,8 +54,12 @@ export default defineConfig({ source: { entry: { index: './src/index.ts', + logger: './src/logger.ts', constants: './src/constants.ts', symbols: './src/symbols.ts', + 'core-sdk': './src/core-sdk.ts', + 'api-client': './src/api-client.ts', + 'api-schemas': './src/api-schemas.ts', }, }, output: { @@ -86,8 +90,12 @@ export default defineConfig({ source: { entry: { index: './src/index.ts', + logger: './src/logger.ts', constants: './src/constants.ts', symbols: './src/symbols.ts', + 'core-sdk': './src/core-sdk.ts', + 'api-client': './src/api-client.ts', + 'api-schemas': './src/api-schemas.ts', }, }, output: { diff --git a/platforms/javascript/web/rstest.config.ts b/platforms/javascript/web/rstest.config.ts index 87860f35..a2d7021d 100644 --- a/platforms/javascript/web/rstest.config.ts +++ b/platforms/javascript/web/rstest.config.ts @@ -15,7 +15,6 @@ export default defineConfig({ '../../../universal/api-schemas/src/', ), '@contentful/optimization-core': resolve(__dirname, '../../../universal/core/src/'), - logger: resolve(__dirname, '../../../lib/logger/src/'), }, }, include: ['**/*.test.?(c|m)[jt]s?(x)'], diff --git a/platforms/javascript/web/src/Optimization.test.ts b/platforms/javascript/web/src/Optimization.test.ts index e0e9022a..c1e07a76 100644 --- a/platforms/javascript/web/src/Optimization.test.ts +++ b/platforms/javascript/web/src/Optimization.test.ts @@ -1,4 +1,6 @@ -import type { CoreConfig, TrackBuilderArgs } from '@contentful/optimization-core' +import type { CoreConfig } from '@contentful/optimization-core' +import type { TrackBuilderArgs } from '@contentful/optimization-core/api-client' +import type { OptimizationData, Profile } from '@contentful/optimization-core/api-schemas' import Optimization from './Optimization' import { OPTIMIZATION_WEB_SDK_NAME } from './constants' @@ -10,6 +12,36 @@ const config: CoreConfig = { environment: ENVIRONMENT, } +const DEFAULT_PROFILE: Profile = { + id: 'profile-id', + stableId: 'profile-id', + random: 1, + audiences: [], + traits: {}, + location: {}, + session: { + id: 'session-id', + isReturningVisitor: false, + landingPage: { + path: '/', + query: {}, + referrer: '', + search: '', + title: '', + url: 'https://example.test/', + }, + count: 1, + activeSessionLength: 0, + averageSessionLength: 0, + }, +} + +const EMPTY_OPTIMIZATION_DATA: OptimizationData = { + changes: [], + personalizations: [], + profile: DEFAULT_PROFILE, +} + interface AutoTrackState { clicks: boolean hovers: boolean @@ -51,16 +83,6 @@ const getAutoTrackEntryHovers = (optimization: Optimization): boolean | undefine return state?.hovers } -const getAllowedEventTypes = (optimization: Optimization): string[] | undefined => { - const value = Reflect.get(optimization.personalization, 'allowedEventTypes') - - if (!Array.isArray(value)) { - return - } - - return value.filter((eventType): eventType is string => typeof eventType === 'string') -} - describe('Optimization', () => { beforeEach(() => { delete window.optimization @@ -150,19 +172,46 @@ describe('Optimization', () => { expect(getAutoTrackEntryHovers(web)).toBe(false) }) - it('defaults allowedEventTypes to identify/page for web', () => { - const web = new Optimization(config) + it('defaults allowedEventTypes to identify/page for web', async () => { + const onEventBlocked = rs.fn() + const web = new Optimization({ + ...config, + onEventBlocked, + }) + const upsertProfile = rs + .spyOn(web.api.experience, 'upsertProfile') + .mockResolvedValue(EMPTY_OPTIMIZATION_DATA) + + await web.identify({ userId: 'user-123' }) + await web.page({}) + await web.track({ event: 'purchase' }) - expect(getAllowedEventTypes(web)).toEqual(['identify', 'page']) + expect(upsertProfile).toHaveBeenCalledTimes(2) + expect(onEventBlocked).toHaveBeenCalledTimes(1) + expect(onEventBlocked).toHaveBeenCalledWith( + expect.objectContaining({ + reason: 'consent', + product: 'personalization', + method: 'track', + }), + ) }) - it('uses user-provided allowedEventTypes when configured', () => { + it('uses user-provided allowedEventTypes when configured', async () => { + const onEventBlocked = rs.fn() const web = new Optimization({ ...config, - allowedEventTypes: ['identify', 'page', 'screen'], + allowedEventTypes: ['identify', 'page', 'track'], + onEventBlocked, }) + const upsertProfile = rs + .spyOn(web.api.experience, 'upsertProfile') + .mockResolvedValue(EMPTY_OPTIMIZATION_DATA) + + await web.track({ event: 'purchase' }) - expect(getAllowedEventTypes(web)).toEqual(['identify', 'page', 'screen']) + expect(upsertProfile).toHaveBeenCalledTimes(1) + expect(onEventBlocked).not.toHaveBeenCalled() }) it('forwards onEventBlocked callback to core stateful guards', async () => { diff --git a/platforms/javascript/web/src/Optimization.ts b/platforms/javascript/web/src/Optimization.ts index 3f8a3451..30dbfeb5 100644 --- a/platforms/javascript/web/src/Optimization.ts +++ b/platforms/javascript/web/src/Optimization.ts @@ -11,12 +11,12 @@ */ import { - type App, CoreStateful, type CoreStatefulConfig, effect, signals, } from '@contentful/optimization-core' +import type { App } from '@contentful/optimization-core/api-schemas' import { ANONYMOUS_ID_COOKIE_LEGACY } from '@contentful/optimization-core/constants' import { getLocale, getPageProperties, getUserAgent } from './builders' import { diff --git a/platforms/javascript/web/src/api-client.ts b/platforms/javascript/web/src/api-client.ts new file mode 100644 index 00000000..ca5423a4 --- /dev/null +++ b/platforms/javascript/web/src/api-client.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core/api-client' diff --git a/platforms/javascript/web/src/api-schemas.ts b/platforms/javascript/web/src/api-schemas.ts new file mode 100644 index 00000000..9fe2852a --- /dev/null +++ b/platforms/javascript/web/src/api-schemas.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core/api-schemas' diff --git a/platforms/javascript/web/src/builders/EventBuilder.ts b/platforms/javascript/web/src/builders/EventBuilder.ts index f30cf505..095ed10c 100644 --- a/platforms/javascript/web/src/builders/EventBuilder.ts +++ b/platforms/javascript/web/src/builders/EventBuilder.ts @@ -1,4 +1,5 @@ -import { createScopedLogger, type Dictionary, type Page } from '@contentful/optimization-core' +import type { Dictionary, Page } from '@contentful/optimization-core/api-schemas' +import { createScopedLogger } from '@contentful/optimization-core/logger' const logger = createScopedLogger('Web:EventBuilder') diff --git a/platforms/javascript/web/src/core-sdk.ts b/platforms/javascript/web/src/core-sdk.ts new file mode 100644 index 00000000..a69a81d2 --- /dev/null +++ b/platforms/javascript/web/src/core-sdk.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-core' diff --git a/platforms/javascript/web/src/entry-tracking/events/click/createEntryClickDetector.ts b/platforms/javascript/web/src/entry-tracking/events/click/createEntryClickDetector.ts index 84ade134..ae7c49e4 100644 --- a/platforms/javascript/web/src/entry-tracking/events/click/createEntryClickDetector.ts +++ b/platforms/javascript/web/src/entry-tracking/events/click/createEntryClickDetector.ts @@ -1,4 +1,5 @@ -import { createScopedLogger, type CoreStateful } from '@contentful/optimization-core' +import type { CoreStateful } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import type { EntryInteractionDetector } from '../../EntryInteractionDetector' import type { EntryClickInteractionElementOptions } from '../../resolveAutoTrackEntryInteractionOptions' import { resolveComponentTrackingPayload as resolveTrackedComponentPayload } from '../../resolveComponentTrackingPayload' diff --git a/platforms/javascript/web/src/entry-tracking/events/hover/ElementHoverObserver.ts b/platforms/javascript/web/src/entry-tracking/events/hover/ElementHoverObserver.ts index be6a3052..a8b2faa4 100644 --- a/platforms/javascript/web/src/entry-tracking/events/hover/ElementHoverObserver.ts +++ b/platforms/javascript/web/src/entry-tracking/events/hover/ElementHoverObserver.ts @@ -10,7 +10,7 @@ * - Sweeps orphan/disconnected element state to avoid leaks. */ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { CAN_ADD_LISTENERS } from '../../../constants' import { safeCallAsync } from '../../../lib' import { diff --git a/platforms/javascript/web/src/entry-tracking/events/view/ElementViewObserver.ts b/platforms/javascript/web/src/entry-tracking/events/view/ElementViewObserver.ts index 005e1365..b2fb2068 100644 --- a/platforms/javascript/web/src/entry-tracking/events/view/ElementViewObserver.ts +++ b/platforms/javascript/web/src/entry-tracking/events/view/ElementViewObserver.ts @@ -10,7 +10,7 @@ * - Sweeps orphan/disconnected element state to avoid leaks */ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { safeCallAsync } from '../../../lib' import { ensureSweeper, diff --git a/platforms/javascript/web/src/handlers/beaconHandler.ts b/platforms/javascript/web/src/handlers/beaconHandler.ts index 671d7b89..508f4cd0 100644 --- a/platforms/javascript/web/src/handlers/beaconHandler.ts +++ b/platforms/javascript/web/src/handlers/beaconHandler.ts @@ -1,4 +1,4 @@ -import type { BatchInsightsEventArray } from '@contentful/optimization-core' +import type { BatchInsightsEventArray } from '@contentful/optimization-core/api-schemas' /** * Send a batch of analytics events using `navigator.sendBeacon`. diff --git a/platforms/javascript/web/src/handlers/createOnlineChangeListener.ts b/platforms/javascript/web/src/handlers/createOnlineChangeListener.ts index 1e4190e9..a7d5e19d 100644 --- a/platforms/javascript/web/src/handlers/createOnlineChangeListener.ts +++ b/platforms/javascript/web/src/handlers/createOnlineChangeListener.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { CAN_ADD_LISTENERS } from '../constants' import { safeCall } from '../lib' diff --git a/platforms/javascript/web/src/handlers/createVisibilityChangeListener.ts b/platforms/javascript/web/src/handlers/createVisibilityChangeListener.ts index 6c99e3f9..d7c46ccc 100644 --- a/platforms/javascript/web/src/handlers/createVisibilityChangeListener.ts +++ b/platforms/javascript/web/src/handlers/createVisibilityChangeListener.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from '@contentful/optimization-core' +import { createScopedLogger } from '@contentful/optimization-core/logger' import { CAN_ADD_LISTENERS } from '../constants' import { safeCall } from '../lib' diff --git a/platforms/javascript/web/src/index.ts b/platforms/javascript/web/src/index.ts index 6f140d18..fea32e93 100644 --- a/platforms/javascript/web/src/index.ts +++ b/platforms/javascript/web/src/index.ts @@ -2,17 +2,18 @@ * Contentful Optimization Web SDK. * * @remarks - * Re-exports the public surface of `@contentful/optimization-core` alongside - * Web-specific utilities such as {@link Optimization}, {@link beaconHandler}, - * and {@link LocalStore}. + * Exposes Web-specific utilities such as {@link Optimization}, {@link beaconHandler}, + * and {@link LocalStore}. Core and transitive API exports are available from + * dedicated entrypoints: + * `@contentful/optimization-web/core-sdk`, + * `@contentful/optimization-web/api-client`, + * and `@contentful/optimization-web/api-schemas`. * * @packageDocumentation */ import Optimization from './Optimization' -export * from '@contentful/optimization-core' - export * from './builders/EventBuilder' export { CAN_ADD_LISTENERS, @@ -22,6 +23,7 @@ export { OPTIMIZATION_WEB_SDK_VERSION, } from './constants' export * from './handlers/beaconHandler' +export * from './Optimization' export * from './storage/LocalStore' export default Optimization diff --git a/platforms/javascript/web/src/logger.ts b/platforms/javascript/web/src/logger.ts new file mode 100644 index 00000000..6f89dc38 --- /dev/null +++ b/platforms/javascript/web/src/logger.ts @@ -0,0 +1,2 @@ +export * from '@contentful/optimization-core/logger' +export { default } from '@contentful/optimization-core/logger' diff --git a/platforms/javascript/web/src/storage/LocalStore.ts b/platforms/javascript/web/src/storage/LocalStore.ts index 1eced41e..69f2becf 100644 --- a/platforms/javascript/web/src/storage/LocalStore.ts +++ b/platforms/javascript/web/src/storage/LocalStore.ts @@ -1,9 +1,8 @@ import { ChangeArray, - createScopedLogger, Profile, SelectedPersonalizationArray, -} from '@contentful/optimization-core' +} from '@contentful/optimization-core/api-schemas' import { ANONYMOUS_ID_KEY, ANONYMOUS_ID_KEY_LEGACY, @@ -13,6 +12,7 @@ import { PERSONALIZATIONS_CACHE_KEY, PROFILE_CACHE_KEY, } from '@contentful/optimization-core/constants' +import { createScopedLogger } from '@contentful/optimization-core/logger' import type { z } from 'zod/mini' const logger = createScopedLogger('Web:LocalStore') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd7d1308..4b8f5a66 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,9 +42,6 @@ catalogs: contentful: specifier: ^11.10.3 version: 11.10.5 - diary: - specifier: ^0.4.5 - version: 0.4.5 dotenv: specifier: ^17.3.1 version: 17.3.1 @@ -180,43 +177,6 @@ importers: specifier: ^5.8.3 version: 5.9.3 - lib/logger: - dependencies: - diary: - specifier: 'catalog:' - version: 0.4.5 - devDependencies: - '@microsoft/api-extractor': - specifier: 'catalog:' - version: 7.57.6(@types/node@24.10.13) - '@rsdoctor/cli': - specifier: 'catalog:' - version: 1.5.2(@rsbuild/core@1.7.3)(@rsdoctor/client@1.5.2)(@rspack/core@1.7.6(@swc/helpers@0.5.19)) - '@rslib/core': - specifier: 'catalog:' - version: 0.19.6(@microsoft/api-extractor@7.57.6(@types/node@24.10.13))(typescript@5.9.3) - '@rstest/core': - specifier: 'catalog:' - version: 0.8.5(happy-dom@20.6.1) - '@rstest/coverage-istanbul': - specifier: 'catalog:' - version: 0.2.1(@rstest/core@0.8.5(happy-dom@20.6.1)) - '@types/node': - specifier: ^24.0.13 - version: 24.10.13 - build-tools: - specifier: workspace:* - version: link:../build-tools - rimraf: - specifier: 'catalog:' - version: 6.1.3 - tslib: - specifier: 'catalog:' - version: 2.8.1 - typescript: - specifier: ^5.8.3 - version: 5.9.3 - lib/mocks: dependencies: '@contentful/optimization-api-schemas': @@ -621,9 +581,6 @@ importers: '@contentful/optimization-api-schemas': specifier: workspace:* version: link:../api-schemas - diary: - specifier: 'catalog:' - version: 0.4.5 es-toolkit: specifier: 'catalog:' version: 1.44.0 @@ -652,9 +609,6 @@ importers: build-tools: specifier: workspace:* version: link:../../lib/build-tools - logger: - specifier: workspace:* - version: link:../../lib/logger mocks: specifier: workspace:* version: link:../../lib/mocks @@ -719,9 +673,6 @@ importers: contentful: specifier: 'catalog:' version: 11.10.5 - diary: - specifier: 'catalog:' - version: 0.4.5 es-toolkit: specifier: 'catalog:' version: 1.44.0 @@ -750,9 +701,6 @@ importers: build-tools: specifier: workspace:* version: link:../../lib/build-tools - logger: - specifier: workspace:* - version: link:../../lib/logger mocks: specifier: workspace:* version: link:../../lib/mocks @@ -3804,9 +3752,6 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} - diary@0.4.5: - resolution: {integrity: sha512-dUtG/AVG5bt9Mi+23TgTvjZ0NDJaszjs1GpYooM5cbEzk2xoqdvxCOlVw0xkenQXZw/DFxp23tj5VkP6YmlRmw==} - didyoumean2@7.0.4: resolution: {integrity: sha512-+yW4SNY7W2DOWe2Jx5H4c2qMTFbLGM6wIyoDPkAPy66X+sD1KfYjBPAIWPVsYqMxelflaMQCloZDudELIPhLqA==} engines: {node: ^18.12.0 || >=20.9.0} @@ -11359,8 +11304,6 @@ snapshots: detect-newline@3.1.0: {} - diary@0.4.5: {} - didyoumean2@7.0.4: dependencies: '@babel/runtime': 7.28.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8c0c227..bf4214e8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,7 +22,6 @@ catalog: '@types/supertest': ^6.0.3 '@vitest/coverage-v8': ^3.2.4 browserslist: ^4.25.1 - diary: ^0.4.5 contentful: ^11.10.3 dotenv: ^17.3.1 ejs: ^3.1.10 diff --git a/specs/023-react-native-hooks/spec.md b/specs/023-react-native-hooks/spec.md index b086c418..c5f8925a 100644 --- a/specs/023-react-native-hooks/spec.md +++ b/specs/023-react-native-hooks/spec.md @@ -104,7 +104,7 @@ manual tracking so I can align screen events with lifecycle/data loading constra timer. - **FR-011**: On transition from visible to below-threshold, hook MUST cancel pending dwell timer. - **FR-012**: When dwell timer completes and element remains visible, hook MUST call - `optimization.analytics.trackComponentView` with derived metadata. + `optimization.trackComponentView` with derived metadata. - **FR-013**: `useViewportTracking` MUST re-check visibility whenever scroll position or viewport height changes. - **FR-014**: `useViewportTracking` MUST clear active timers on unmount. diff --git a/specs/024-react-native-personalization-and-analytics-components/spec.md b/specs/024-react-native-personalization-and-analytics-components/spec.md index f006bde6..975d9ff3 100644 --- a/specs/024-react-native-personalization-and-analytics-components/spec.md +++ b/specs/024-react-native-personalization-and-analytics-components/spec.md @@ -100,7 +100,7 @@ and route changes, and verify expected tracking payloads. personalization value and ignore later updates. - **FR-006**: `Personalization` MUST unsubscribe from personalization state updates on unmount. - **FR-007**: `Personalization` MUST resolve display content via - `optimization.personalization.personalizeEntry(baselineEntry, lockedPersonalizations)`. + `optimization.personalizeEntry(baselineEntry, lockedPersonalizations)`. - **FR-008**: `Personalization` MUST pass resolved entry and resolved personalization metadata to `useViewportTracking`. - **FR-009**: `Personalization` MUST render `children(resolvedEntry)` inside a wrapper `View` diff --git a/tsconfig.base.json b/tsconfig.base.json index 845242a1..fa85ea1c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -30,8 +30,7 @@ "@contentful/optimization-react-native": ["./platforms/javascript/react-native/src/index.ts"], "@contentful/optimization-react-native/*": ["./platforms/javascript/react-native/src/*"], "@contentful/optimization-web": ["./platforms/javascript/web/src/index.ts"], - "@contentful/optimization-web/*": ["./platforms/javascript/web/src/*"], - "logger": ["./lib/logger/src/index.ts"] + "@contentful/optimization-web/*": ["./platforms/javascript/web/src/*"] }, "removeComments": false, "skipLibCheck": true, diff --git a/universal/api-client/package.json b/universal/api-client/package.json index c26a6296..5dea13f9 100644 --- a/universal/api-client/package.json +++ b/universal/api-client/package.json @@ -17,6 +17,26 @@ "default": "./dist/index.cjs" } }, + "./logger": { + "import": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "require": { + "types": "./dist/logger.d.cts", + "default": "./dist/logger.cjs" + } + }, + "./api-schemas": { + "import": { + "types": "./dist/api-schemas.d.mts", + "default": "./dist/api-schemas.mjs" + }, + "require": { + "types": "./dist/api-schemas.d.cts", + "default": "./dist/api-schemas.cjs" + } + }, "./package.json": "./package.json" }, "files": [ @@ -33,7 +53,6 @@ }, "dependencies": { "@contentful/optimization-api-schemas": "workspace:*", - "diary": "catalog:", "es-toolkit": "catalog:", "zod": "catalog:" }, @@ -45,7 +64,6 @@ "@types/node": "catalog:", "@rstest/core": "catalog:", "@rstest/coverage-istanbul": "catalog:", - "logger": "workspace:*", "mocks": "workspace:*", "msw": "catalog:", "rimraf": "catalog:", diff --git a/universal/api-client/rslib.config.ts b/universal/api-client/rslib.config.ts index 416bcf59..9aa8a4b1 100644 --- a/universal/api-client/rslib.config.ts +++ b/universal/api-client/rslib.config.ts @@ -14,6 +14,11 @@ const common = { export default defineConfig({ source: { + entry: { + index: './src/index.ts', + logger: './src/logger.ts', + 'api-schemas': './src/api-schemas.ts', + }, tsconfigPath: './tsconfig.build.json', decorators: { version: '2022-03' }, // stage-3 decorators }, diff --git a/universal/api-client/rstest.config.ts b/universal/api-client/rstest.config.ts index 1a0dd9a9..7355f128 100644 --- a/universal/api-client/rstest.config.ts +++ b/universal/api-client/rstest.config.ts @@ -7,7 +7,6 @@ export default defineConfig({ resolve: { alias: { '@contentful/optimization-api-schemas': resolve(__dirname, '../api-schemas/src/'), - logger: resolve(__dirname, '../../lib/logger/src/'), }, }, include: ['**/*.test.?(c|m)[jt]s?(x)'], diff --git a/universal/api-client/src/ApiClientBase.ts b/universal/api-client/src/ApiClientBase.ts index 970359c4..139fe7d7 100644 --- a/universal/api-client/src/ApiClientBase.ts +++ b/universal/api-client/src/ApiClientBase.ts @@ -1,5 +1,5 @@ -import { createScopedLogger } from 'logger' import Fetch, { type FetchMethod, type ProtectedFetchMethodOptions } from './fetch' +import { createScopedLogger } from './logger' const logger = createScopedLogger('ApiClient') diff --git a/universal/api-client/src/api-schemas.ts b/universal/api-client/src/api-schemas.ts new file mode 100644 index 00000000..a95ff349 --- /dev/null +++ b/universal/api-client/src/api-schemas.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-api-schemas' diff --git a/universal/api-client/src/experience/ExperienceApiClient.ts b/universal/api-client/src/experience/ExperienceApiClient.ts index 1df3358f..d80cb9b8 100644 --- a/universal/api-client/src/experience/ExperienceApiClient.ts +++ b/universal/api-client/src/experience/ExperienceApiClient.ts @@ -10,8 +10,8 @@ import { type ExperienceRequestOptions, type OptimizationData, } from '@contentful/optimization-api-schemas' -import { createScopedLogger } from 'logger' import ApiClientBase, { type ApiConfig } from '../ApiClientBase' +import { createScopedLogger } from '../logger' const logger = createScopedLogger('ApiClient:Experience') diff --git a/universal/api-client/src/fetch/createProtectedFetchMethod.ts b/universal/api-client/src/fetch/createProtectedFetchMethod.ts index fe0521b8..9d7d99b0 100644 --- a/universal/api-client/src/fetch/createProtectedFetchMethod.ts +++ b/universal/api-client/src/fetch/createProtectedFetchMethod.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from 'logger' +import { createScopedLogger } from '../logger' import { createRetryFetchMethod, type RetryFetchMethodOptions } from './createRetryFetchMethod' import { createTimeoutFetchMethod, diff --git a/universal/api-client/src/fetch/createRetryFetchMethod.ts b/universal/api-client/src/fetch/createRetryFetchMethod.ts index 72e19fb9..983f2511 100644 --- a/universal/api-client/src/fetch/createRetryFetchMethod.ts +++ b/universal/api-client/src/fetch/createRetryFetchMethod.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from 'logger' +import { createScopedLogger } from '../logger' import type { BaseFetchMethodOptions, FetchMethod, FetchMethodCallbackOptions } from './Fetch' const logger = createScopedLogger('ApiClient:Retry') diff --git a/universal/api-client/src/fetch/createTimeoutFetchMethod.ts b/universal/api-client/src/fetch/createTimeoutFetchMethod.ts index d0b726b0..dc82a441 100644 --- a/universal/api-client/src/fetch/createTimeoutFetchMethod.ts +++ b/universal/api-client/src/fetch/createTimeoutFetchMethod.ts @@ -1,4 +1,4 @@ -import { createScopedLogger } from 'logger' +import { createScopedLogger } from '../logger' import type { BaseFetchMethodOptions, FetchMethod, FetchMethodCallbackOptions } from './Fetch' const logger = createScopedLogger('ApiClient:Timeout') diff --git a/universal/api-client/src/index.ts b/universal/api-client/src/index.ts index b7b4d1a2..4172eadb 100644 --- a/universal/api-client/src/index.ts +++ b/universal/api-client/src/index.ts @@ -8,7 +8,6 @@ export { default as ApiClient } from './ApiClient' -export * from '@contentful/optimization-api-schemas' export * from './ApiClient' export * from './ApiClientBase' export * from './builders' diff --git a/universal/api-client/src/insights/InsightsApiClient.ts b/universal/api-client/src/insights/InsightsApiClient.ts index a86d3807..ec4e90aa 100644 --- a/universal/api-client/src/insights/InsightsApiClient.ts +++ b/universal/api-client/src/insights/InsightsApiClient.ts @@ -2,8 +2,8 @@ import { BatchInsightsEventArray, parseWithFriendlyError, } from '@contentful/optimization-api-schemas' -import { createScopedLogger } from 'logger' import ApiClientBase, { type ApiConfig } from '../ApiClientBase' +import { createScopedLogger } from '../logger' const logger = createScopedLogger('ApiClient:Insights') diff --git a/lib/logger/src/ConsoleLogSink.test.ts b/universal/api-client/src/lib/logger/ConsoleLogSink.test.ts similarity index 98% rename from lib/logger/src/ConsoleLogSink.test.ts rename to universal/api-client/src/lib/logger/ConsoleLogSink.test.ts index d8c16505..babd75bf 100644 --- a/lib/logger/src/ConsoleLogSink.test.ts +++ b/universal/api-client/src/lib/logger/ConsoleLogSink.test.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console -- testing console */ import { rs } from '@rstest/core' -import type { LogEvent, LogLevels } from 'diary' +import type { LogEvent, LogLevels } from '.' import { ConsoleLogSink } from './ConsoleLogSink' // Save the original console methods so we can restore after diff --git a/lib/logger/src/ConsoleLogSink.ts b/universal/api-client/src/lib/logger/ConsoleLogSink.ts similarity index 85% rename from lib/logger/src/ConsoleLogSink.ts rename to universal/api-client/src/lib/logger/ConsoleLogSink.ts index e6208eb3..c9a4a366 100644 --- a/lib/logger/src/ConsoleLogSink.ts +++ b/universal/api-client/src/lib/logger/ConsoleLogSink.ts @@ -1,6 +1,5 @@ /* eslint-disable no-console -- using console */ -import type { LogEvent, LogLevels } from 'diary' -import { compare } from 'diary/utils' +import { logLevelSeverity, type LogEvent, type LogLevels } from './logging' import LogSink from './LogSink' const consoleMap = { @@ -24,15 +23,13 @@ const consoleMap = { }, } -const COMPARISON_EQUALITY = 0 - /** * A {@link LogSink} that writes log events to the browser or Node.js console, * filtering by a configurable verbosity threshold. * * @example * ```typescript - * import { logger, ConsoleLogSink } from 'logger' + * import { logger, ConsoleLogSink } from '@contentful/optimization-api-client/logger' * * logger.addSink(new ConsoleLogSink('debug')) * ``` @@ -68,7 +65,7 @@ export class ConsoleLogSink extends LogSink { * @returns Nothing. */ ingest(event: LogEvent): void { - if (compare(this.verbosity, event.level) > COMPARISON_EQUALITY) return + if (logLevelSeverity[event.level] < logLevelSeverity[this.verbosity]) return consoleMap[event.level](...event.messages) } diff --git a/lib/logger/src/LogSink.ts b/universal/api-client/src/lib/logger/LogSink.ts similarity index 81% rename from lib/logger/src/LogSink.ts rename to universal/api-client/src/lib/logger/LogSink.ts index 1f5e216a..5678c2c4 100644 --- a/lib/logger/src/LogSink.ts +++ b/universal/api-client/src/lib/logger/LogSink.ts @@ -1,11 +1,4 @@ -import type { LogEvent } from 'diary' - -/** - * A log event emitted by the diary logging library. - * - * @public - */ -export type { LogEvent } +import type { LogEvent } from './logging' /** * Abstract base class for log sinks that receive and process log events. diff --git a/lib/logger/src/Logger.test.ts b/universal/api-client/src/lib/logger/Logger.test.ts similarity index 100% rename from lib/logger/src/Logger.test.ts rename to universal/api-client/src/lib/logger/Logger.test.ts diff --git a/lib/logger/src/Logger.ts b/universal/api-client/src/lib/logger/Logger.ts similarity index 80% rename from lib/logger/src/Logger.ts rename to universal/api-client/src/lib/logger/Logger.ts index 5f0283eb..5b5e457f 100644 --- a/lib/logger/src/Logger.ts +++ b/universal/api-client/src/lib/logger/Logger.ts @@ -1,13 +1,12 @@ -import { diary, enable, type Diary, type LogEvent } from 'diary' - import type LogSink from './LogSink' +import type { LogEvent, LogLevels } from './logging' /** * Central logger that routes log events through registered {@link LogSink} instances. * * @example * ```typescript - * import { logger } from 'logger' + * import { logger } from '@contentful/optimization-api-client/logger' * * logger.info('MyModule', 'Application started') * ``` @@ -15,19 +14,13 @@ import type LogSink from './LogSink' * @public */ export class Logger { - /** The logger's identifier, used as the diary scope name. */ + /** The logger's identifier, used as the event scope name. */ readonly name = '@contentful/optimization' private readonly PREFIX_PARTS = ['Ctfl', 'O10n'] private readonly DELIMITER = ':' - private readonly diary: Diary private sinks: LogSink[] = [] - constructor() { - this.diary = diary(this.name, this.onLogEvent.bind(this)) - enable(this.name) - } - private assembleLocationPrefix(logLocation: string): string { return `[${[...this.PREFIX_PARTS, logLocation].join(this.DELIMITER)}]` } @@ -40,7 +33,7 @@ export class Logger { * * @example * ```typescript - * import { logger, ConsoleLogSink } from 'logger' + * import { logger, ConsoleLogSink } from '@contentful/optimization-api-client/logger' * * logger.addSink(new ConsoleLogSink('debug')) * ``` @@ -83,7 +76,7 @@ export class Logger { * * @param logLocation - The module or component identifier. * @param message - The log message. - * @param args - Additional arguments forwarded to the diary logger. + * @param args - Additional arguments forwarded in the log event. * @returns Nothing. * * @example @@ -92,7 +85,7 @@ export class Logger { * ``` */ public debug(logLocation: string, message: string, ...args: unknown[]): void { - this.diary.debug(`${this.assembleLocationPrefix(logLocation)} ${message}`, ...args) + this.emit('debug', logLocation, message, ...args) } /** @@ -100,7 +93,7 @@ export class Logger { * * @param logLocation - The module or component identifier. * @param message - The log message. - * @param args - Additional arguments forwarded to the diary logger. + * @param args - Additional arguments forwarded in the log event. * @returns Nothing. * * @example @@ -109,7 +102,7 @@ export class Logger { * ``` */ public info(logLocation: string, message: string, ...args: unknown[]): void { - this.diary.info(`${this.assembleLocationPrefix(logLocation)} ${message}`, ...args) + this.emit('info', logLocation, message, ...args) } /** @@ -117,7 +110,7 @@ export class Logger { * * @param logLocation - The module or component identifier. * @param message - The log message. - * @param args - Additional arguments forwarded to the diary logger. + * @param args - Additional arguments forwarded in the log event. * @returns Nothing. * * @example @@ -126,7 +119,7 @@ export class Logger { * ``` */ public log(logLocation: string, message: string, ...args: unknown[]): void { - this.diary.log(`${this.assembleLocationPrefix(logLocation)} ${message}`, ...args) + this.emit('log', logLocation, message, ...args) } /** @@ -134,7 +127,7 @@ export class Logger { * * @param logLocation - The module or component identifier. * @param message - The log message. - * @param args - Additional arguments forwarded to the diary logger. + * @param args - Additional arguments forwarded in the log event. * @returns Nothing. * * @example @@ -143,7 +136,7 @@ export class Logger { * ``` */ public warn(logLocation: string, message: string, ...args: unknown[]): void { - this.diary.warn(`${this.assembleLocationPrefix(logLocation)} ${message}`, ...args) + this.emit('warn', logLocation, message, ...args) } /** @@ -151,7 +144,7 @@ export class Logger { * * @param logLocation - The module or component identifier. * @param message - The log message or Error object. - * @param args - Additional arguments forwarded to the diary logger. + * @param args - Additional arguments forwarded in the log event. * @returns Nothing. * * @example @@ -160,7 +153,7 @@ export class Logger { * ``` */ public error(logLocation: string, message: string | Error, ...args: unknown[]): void { - this.diary.error(`${this.assembleLocationPrefix(logLocation)} ${message}`, ...args) + this.emit('error', logLocation, message, ...args) } /** @@ -168,7 +161,7 @@ export class Logger { * * @param logLocation - The module or component identifier. * @param message - The log message or Error object. - * @param args - Additional arguments forwarded to the diary logger. + * @param args - Additional arguments forwarded in the log event. * @returns Nothing. * * @example @@ -177,7 +170,20 @@ export class Logger { * ``` */ public fatal(logLocation: string, message: string | Error, ...args: unknown[]): void { - this.diary.fatal(`${this.assembleLocationPrefix(logLocation)} ${message}`, ...args) + this.emit('fatal', logLocation, message, ...args) + } + + private emit( + level: LogLevels, + logLocation: string, + message: string | Error, + ...args: unknown[] + ): void { + this.onLogEvent({ + name: this.name, + level, + messages: [`${this.assembleLocationPrefix(logLocation)} ${String(message)}`, ...args], + }) } private onLogEvent(event: LogEvent): void { @@ -222,7 +228,7 @@ export interface ScopedLogger { * * @example * ```typescript - * import { createScopedLogger } from 'logger' + * import { createScopedLogger } from '@contentful/optimization-api-client/logger' * * const log = createScopedLogger('MyModule') * log.info('Initialization complete') diff --git a/universal/api-client/src/lib/logger/index.ts b/universal/api-client/src/lib/logger/index.ts new file mode 100644 index 00000000..47e98928 --- /dev/null +++ b/universal/api-client/src/lib/logger/index.ts @@ -0,0 +1,10 @@ +import LogSink from './LogSink' +import { Logger } from './Logger' + +export * from './ConsoleLogSink' +export * from './LogSink' +export * from './Logger' +export * from './logging' + +export { LogSink } +export default Logger diff --git a/universal/api-client/src/lib/logger/logging.ts b/universal/api-client/src/lib/logger/logging.ts new file mode 100644 index 00000000..8905a2f9 --- /dev/null +++ b/universal/api-client/src/lib/logger/logging.ts @@ -0,0 +1,36 @@ +/** + * Supported log levels ordered from highest to lowest severity. + * + * @public + */ +export type LogLevels = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'log' + +/** + * A log event emitted by the logger package. + * + * @public + */ +export interface LogEvent { + /** Logger scope name. */ + name: string + /** Event severity level. */ + level: LogLevels + /** Event payload, where the first entry is the formatted message. */ + messages: unknown[] + /** Optional additional context attached by sinks or middleware. */ + [other: string]: unknown +} + +/** + * Numeric severity map used for log-level threshold comparisons. + * + * @public + */ +export const logLevelSeverity: Readonly> = { + fatal: 60, + error: 50, + warn: 40, + info: 30, + debug: 20, + log: 10, +} diff --git a/universal/api-client/src/logger.ts b/universal/api-client/src/logger.ts new file mode 100644 index 00000000..5b04777a --- /dev/null +++ b/universal/api-client/src/logger.ts @@ -0,0 +1,2 @@ +export * from './lib/logger' +export { default } from './lib/logger' diff --git a/universal/api-client/src/test/setup.ts b/universal/api-client/src/test/setup.ts index f672daac..3cc36e33 100644 --- a/universal/api-client/src/test/setup.ts +++ b/universal/api-client/src/test/setup.ts @@ -2,7 +2,7 @@ import { rs } from '@rstest/core' import { experienceApiHandlers, loggerMock, resetMockLogger } from 'mocks' import { setupServer } from 'msw/node' -rs.mock('logger', () => loggerMock) +rs.mock('../logger', () => loggerMock) export const server = setupServer(...experienceApiHandlers.getHandlers()) diff --git a/universal/api-client/tsconfig.build.json b/universal/api-client/tsconfig.build.json index d637ad49..7b38423e 100644 --- a/universal/api-client/tsconfig.build.json +++ b/universal/api-client/tsconfig.build.json @@ -14,8 +14,5 @@ }, "include": ["./src/**/*", "./src/**/*.json"], "exclude": ["./src/**/*.test.*", "./src/test/**/*"], - "references": [ - { "path": "../../lib/logger/tsconfig.build.json" }, - { "path": "../api-schemas/tsconfig.build.json" } - ] + "references": [{ "path": "../api-schemas/tsconfig.build.json" }] } diff --git a/universal/api-schemas/src/contentful/CtflEntry.ts b/universal/api-schemas/src/contentful/CtflEntry.ts index b4a219dc..df92a707 100644 --- a/universal/api-schemas/src/contentful/CtflEntry.ts +++ b/universal/api-schemas/src/contentful/CtflEntry.ts @@ -1,4 +1,3 @@ -import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful' import * as z from 'zod/mini' /** @@ -208,33 +207,3 @@ export const CtflEntry = z.object({ * @public */ export type CtflEntry = z.infer - -/** - * Type guard that checks whether the given value is a Contentful {@link Entry}, - * passing through the specified skeleton, chain modifiers, and locale. - * - * @typeParam S - The entry skeleton type. - * @typeParam M - The chain modifiers type. Defaults to {@link ChainModifiers}. - * @typeParam L - The locale code type. Defaults to {@link LocaleCode}. - * - * @param entry - The value to test. - * @returns `true` if the object conforms to {@link CtflEntry} and can be treated - * as a typed {@link Entry}, otherwise `false`. - * - * @example - * ```ts - * const entry = await client.getEntry('my-entry-id'); - * if (isEntry(entry)) { - * console.log(entry.fields.myField); - * } - * ``` - * - * @public - */ -export function isEntry< - S extends EntrySkeletonType, - M extends ChainModifiers = ChainModifiers, - L extends LocaleCode = LocaleCode, ->(entry: Entry | undefined): entry is Entry { - return CtflEntry.safeParse(entry).success -} diff --git a/universal/api-schemas/src/contentful/PersonalizationConfig.ts b/universal/api-schemas/src/contentful/PersonalizationConfig.ts index e9dd1069..9033461e 100644 --- a/universal/api-schemas/src/contentful/PersonalizationConfig.ts +++ b/universal/api-schemas/src/contentful/PersonalizationConfig.ts @@ -29,25 +29,6 @@ export const EntryReplacementVariant = z.object({ */ export type EntryReplacementVariant = z.infer -/** - * Type guard for {@link EntryReplacementVariant}. - * - * @param variant - Value to test. - * @returns `true` if `variant` conforms to {@link EntryReplacementVariant}, otherwise `false`. - * - * @example - * ```ts - * if (isEntryReplacementVariant(variant)) { - * console.log(variant.id); - * } - * ``` - * - * @public - */ -export function isEntryReplacementVariant(variant: unknown): variant is EntryReplacementVariant { - return EntryReplacementVariant.safeParse(variant).success -} - /** * Zod schema describing an entry replacement personalization component. * @@ -83,27 +64,6 @@ export const EntryReplacementComponent = z.object({ */ export type EntryReplacementComponent = z.infer -/** - * Type guard for {@link EntryReplacementComponent}. - * - * @param component - Personalization component to test. - * @returns `true` if the component is an EntryReplacement component, otherwise `false`. - * - * @example - * ```ts - * if (isEntryReplacementComponent(component)) { - * console.log(component.baseline.id); - * } - * ``` - * - * @public - */ -export function isEntryReplacementComponent( - component: PersonalizationComponent, -): component is EntryReplacementComponent { - return component.type === 'EntryReplacement' || component.type === undefined -} - /** * Zod schema describing a variant for inline variables. * @@ -168,27 +128,6 @@ export const InlineVariableComponent = z.object({ */ export type InlineVariableComponent = z.infer -/** - * Type guard for {@link InlineVariableComponent}. - * - * @param component - Personalization component to test. - * @returns `true` if the component is an InlineVariable component, otherwise `false`. - * - * @example - * ```ts - * if (isInlineVariableComponent(component)) { - * console.log(component.key, component.valueType); - * } - * ``` - * - * @public - */ -export function isInlineVariableComponent( - component: PersonalizationComponent, -): component is InlineVariableComponent { - return component.type === 'InlineVariable' -} - /** * Discriminated union of all supported personalization components. * diff --git a/universal/api-schemas/src/contentful/PersonalizationEntry.ts b/universal/api-schemas/src/contentful/PersonalizationEntry.ts index 9ab58274..e70f27b6 100644 --- a/universal/api-schemas/src/contentful/PersonalizationEntry.ts +++ b/universal/api-schemas/src/contentful/PersonalizationEntry.ts @@ -137,25 +137,6 @@ export const PersonalizationEntrySkeleton = z.object({ */ export type PersonalizationEntrySkeleton = z.infer -/** - * Type guard for {@link PersonalizationEntry}. - * - * @param entry - Contentful entry or link to test. - * @returns `true` if the value conforms to {@link PersonalizationEntry}, otherwise `false`. - * - * @example - * ```ts - * if (isPersonalizationEntry(entry)) { - * console.log(entry.fields.nt_name); - * } - * ``` - * - * @public - */ -export function isPersonalizationEntry(entry: CtflEntry | Link): entry is PersonalizationEntry { - return PersonalizationEntry.safeParse(entry).success -} - /** * Zod schema describing an array of personalization entries or links. * diff --git a/universal/api-schemas/src/contentful/PersonalizedEntry.ts b/universal/api-schemas/src/contentful/PersonalizedEntry.ts index cecf0869..6c41abba 100644 --- a/universal/api-schemas/src/contentful/PersonalizedEntry.ts +++ b/universal/api-schemas/src/contentful/PersonalizedEntry.ts @@ -1,4 +1,3 @@ -import type { Entry } from 'contentful' import * as z from 'zod/mini' import { CtflEntry, EntryFields } from './CtflEntry' import { PersonalizationEntryArray } from './PersonalizationEntry' @@ -26,22 +25,3 @@ export const PersonalizedEntry = z.extend(CtflEntry, { * @public */ export type PersonalizedEntry = z.infer - -/** - * Type guard for {@link PersonalizedEntry}. - * - * @param entry - Contentful entry to test. - * @returns `true` if the entry conforms to {@link PersonalizedEntry}, otherwise `false`. - * - * @example - * ```ts - * if (isPersonalizedEntry(entry)) { - * console.log(entry.fields.nt_experiences); - * } - * ``` - * - * @public - */ -export function isPersonalizedEntry(entry: Entry | undefined): entry is PersonalizedEntry { - return PersonalizedEntry.safeParse(entry).success -} diff --git a/universal/api-schemas/src/contentful/index.ts b/universal/api-schemas/src/contentful/index.ts index 468057fc..58509a39 100644 --- a/universal/api-schemas/src/contentful/index.ts +++ b/universal/api-schemas/src/contentful/index.ts @@ -4,3 +4,4 @@ export * from './MergeTagEntry' export * from './PersonalizationConfig' export * from './PersonalizationEntry' export * from './PersonalizedEntry' +export * from './typeGuards' diff --git a/universal/api-schemas/src/contentful/typeGuards.ts b/universal/api-schemas/src/contentful/typeGuards.ts new file mode 100644 index 00000000..809cd5d6 --- /dev/null +++ b/universal/api-schemas/src/contentful/typeGuards.ts @@ -0,0 +1,152 @@ +import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful' +import { CtflEntry, type Link } from './CtflEntry' +import { MergeTagEntry } from './MergeTagEntry' +import { + type EntryReplacementComponent, + EntryReplacementVariant, + type InlineVariableComponent, + type PersonalizationComponent, +} from './PersonalizationConfig' +import { PersonalizationEntry } from './PersonalizationEntry' +import { PersonalizedEntry } from './PersonalizedEntry' + +/** + * Type guard that checks whether the given value is a Contentful {@link Entry}, + * passing through the specified skeleton, chain modifiers, and locale. + * + * @typeParam S - The entry skeleton type. + * @typeParam M - The chain modifiers type. Defaults to {@link ChainModifiers}. + * @typeParam L - The locale code type. Defaults to {@link LocaleCode}. + * + * @param entry - The value to test. + * @returns `true` if the object conforms to {@link CtflEntry} and can be treated + * as a typed {@link Entry}, otherwise `false`. + * + * @example + * ```ts + * const entry = await client.getEntry('my-entry-id'); + * if (isEntry(entry)) { + * console.log(entry.fields.myField); + * } + * ``` + * + * @public + */ +export function isEntry< + S extends EntrySkeletonType, + M extends ChainModifiers = ChainModifiers, + L extends LocaleCode = LocaleCode, +>(entry: Entry | undefined): entry is Entry { + return CtflEntry.safeParse(entry).success +} + +/** + * Type guard for {@link EntryReplacementVariant}. + * + * @param variant - Value to test. + * @returns `true` if `variant` conforms to {@link EntryReplacementVariant}, otherwise `false`. + * + * @example + * ```ts + * if (isEntryReplacementVariant(variant)) { + * console.log(variant.id); + * } + * ``` + * + * @public + */ +export function isEntryReplacementVariant(variant: unknown): variant is EntryReplacementVariant { + return EntryReplacementVariant.safeParse(variant).success +} + +/** + * Type guard for {@link EntryReplacementComponent}. + * + * @param component - Personalization component to test. + * @returns `true` if the component is an EntryReplacement component, otherwise `false`. + * + * @example + * ```ts + * if (isEntryReplacementComponent(component)) { + * console.log(component.baseline.id); + * } + * ``` + * + * @public + */ +export function isEntryReplacementComponent( + component: PersonalizationComponent, +): component is EntryReplacementComponent { + return component.type === 'EntryReplacement' || component.type === undefined +} + +/** + * Type guard for {@link InlineVariableComponent}. + * + * @param component - Personalization component to test. + * @returns `true` if the component is an InlineVariable component, otherwise `false`. + * + * @example + * ```ts + * if (isInlineVariableComponent(component)) { + * console.log(component.key, component.valueType); + * } + * ``` + * + * @public + */ +export function isInlineVariableComponent( + component: PersonalizationComponent, +): component is InlineVariableComponent { + return component.type === 'InlineVariable' +} + +/** + * Type guard for {@link PersonalizationEntry}. + * + * @param entry - Contentful entry or link to test. + * @returns `true` if the value conforms to {@link PersonalizationEntry}, otherwise `false`. + * + * @example + * ```ts + * if (isPersonalizationEntry(entry)) { + * console.log(entry.fields.nt_name); + * } + * ``` + * + * @public + */ +export function isPersonalizationEntry(entry: CtflEntry | Link): entry is PersonalizationEntry { + return PersonalizationEntry.safeParse(entry).success +} + +/** + * Type guard for {@link PersonalizedEntry}. + * + * @param entry - Contentful entry to test. + * @returns `true` if the entry conforms to {@link PersonalizedEntry}, otherwise `false`. + * + * @example + * ```ts + * if (isPersonalizedEntry(entry)) { + * console.log(entry.fields.nt_experiences); + * } + * ``` + * + * @public + */ +export function isPersonalizedEntry(entry: Entry | undefined): entry is PersonalizedEntry { + return PersonalizedEntry.safeParse(entry).success +} + +/** + * Type guard for {@link MergeTagEntry}. + * + * @param entry - Value to test. + * @returns `true` if the value conforms to {@link MergeTagEntry}, otherwise `false`. + * + * @public + */ +export function isMergeTagEntry(entry: unknown): entry is MergeTagEntry { + return MergeTagEntry.safeParse(entry).success +} diff --git a/universal/core/package.json b/universal/core/package.json index 7078df24..bac8ef19 100644 --- a/universal/core/package.json +++ b/universal/core/package.json @@ -17,6 +17,16 @@ "default": "./dist/index.cjs" } }, + "./logger": { + "import": { + "types": "./dist/logger.d.mts", + "default": "./dist/logger.mjs" + }, + "require": { + "types": "./dist/logger.d.cts", + "default": "./dist/logger.cjs" + } + }, "./constants": { "import": { "types": "./dist/constants.d.mts", @@ -37,6 +47,26 @@ "default": "./dist/symbols.cjs" } }, + "./api-client": { + "import": { + "types": "./dist/api-client.d.mts", + "default": "./dist/api-client.mjs" + }, + "require": { + "types": "./dist/api-client.d.cts", + "default": "./dist/api-client.cjs" + } + }, + "./api-schemas": { + "import": { + "types": "./dist/api-schemas.d.mts", + "default": "./dist/api-schemas.mjs" + }, + "require": { + "types": "./dist/api-schemas.d.cts", + "default": "./dist/api-schemas.cjs" + } + }, "./package.json": "./package.json" }, "files": [ @@ -55,7 +85,6 @@ "@contentful/optimization-api-client": "workspace:*", "@preact/signals-core": "catalog:", "contentful": "catalog:", - "diary": "catalog:", "es-toolkit": "catalog:", "zod": "catalog:" }, @@ -67,7 +96,6 @@ "@rstest/core": "catalog:", "@rstest/coverage-istanbul": "catalog:", "build-tools": "workspace:*", - "logger": "workspace:*", "mocks": "workspace:*", "msw": "catalog:", "rimraf": "catalog:", diff --git a/universal/core/rslib.config.ts b/universal/core/rslib.config.ts index c91b3bef..c8d35757 100644 --- a/universal/core/rslib.config.ts +++ b/universal/core/rslib.config.ts @@ -18,8 +18,11 @@ export default defineConfig({ source: { entry: { index: './src/index.ts', + logger: './src/logger.ts', constants: './src/constants.ts', symbols: './src/symbols.ts', + 'api-client': './src/api-client.ts', + 'api-schemas': './src/api-schemas.ts', }, tsconfigPath: './tsconfig.build.json', decorators: { version: '2022-03' }, // stage-3 decorators diff --git a/universal/core/rstest.config.ts b/universal/core/rstest.config.ts index dd37bf6f..ce766cf0 100644 --- a/universal/core/rstest.config.ts +++ b/universal/core/rstest.config.ts @@ -8,7 +8,6 @@ export default defineConfig({ alias: { '@contentful/optimization-api-client': resolve(__dirname, '../api-client/src/'), '@contentful/optimization-api-schemas': resolve(__dirname, '../api-schemas/src/'), - logger: resolve(__dirname, '../../lib/logger/src/'), }, }, include: ['**/*.test.?(c|m)[jt]s?(x)'], diff --git a/universal/core/src/CoreBase.test.ts b/universal/core/src/CoreBase.test.ts index 98f310a1..c1809996 100644 --- a/universal/core/src/CoreBase.test.ts +++ b/universal/core/src/CoreBase.test.ts @@ -1,16 +1,17 @@ import { EXPERIENCE_BASE_URL } from '@contentful/optimization-api-client' +import type { ChangeArray } from '@contentful/optimization-api-client/api-schemas' import { AnalyticsStateless } from './analytics' import { OPTIMIZATION_CORE_SDK_NAME } from './constants' import CoreBase, { type CoreConfig } from './CoreBase' import { PersonalizationStateless } from './personalization' class TestCore extends CoreBase { - analytics = new AnalyticsStateless({ + _analytics = new AnalyticsStateless({ api: this.api, builder: this.eventBuilder, interceptors: this.interceptors, }) - personalization = new PersonalizationStateless({ + _personalization = new PersonalizationStateless({ api: this.api, builder: this.eventBuilder, interceptors: this.interceptors, @@ -25,6 +26,32 @@ const config: CoreConfig = { environment: ENVIRONMENT, } +const CHANGES: ChangeArray = [ + { + key: 'dark-mode', + type: 'Variable', + value: true, + meta: { + experienceId: 'experience-id', + variantIndex: 0, + }, + }, + { + key: 'price', + type: 'Variable', + value: { + value: { + amount: 10, + currency: 'USD', + }, + }, + meta: { + experienceId: 'experience-id', + variantIndex: 1, + }, + }, +] + describe('CoreBase', () => { it('allows access to the original configuration options', () => { const core = new TestCore(config) @@ -63,4 +90,16 @@ describe('CoreBase', () => { expect(core.api.config.fetchOptions).toEqual(fetchOptions) }) + + it('resolves all custom flags from changes', () => { + const core = new TestCore(config) + + expect(core.getCustomFlags(CHANGES)).toEqual({ + 'dark-mode': true, + price: { + amount: 10, + currency: 'USD', + }, + }) + }) }) diff --git a/universal/core/src/CoreBase.ts b/universal/core/src/CoreBase.ts index a3439aaf..b01d825b 100644 --- a/universal/core/src/CoreBase.ts +++ b/universal/core/src/CoreBase.ts @@ -1,9 +1,7 @@ import { ApiClient, EventBuilder, - type InsightsEvent as AnalyticsEvent, type ApiClientConfig, - type ChangeArray, type ComponentClickBuilderArgs, type ComponentHoverBuilderArgs, type ComponentViewBuilderArgs, @@ -12,25 +10,36 @@ import { type GlobalApiConfigProperties, type IdentifyBuilderArgs, type InsightsApiClientConfig, - type Json, - type MergeTagEntry, - type OptimizationData, type PageViewBuilderArgs, - type PartialProfile, - type ExperienceEvent as PersonalizationEvent, - type Profile, type ScreenViewBuilderArgs, - type SelectedPersonalizationArray, type TrackBuilderArgs, } from '@contentful/optimization-api-client' +import type { + InsightsEvent as AnalyticsEvent, + ChangeArray, + Flags, + Json, + MergeTagEntry, + OptimizationData, + PartialProfile, + ExperienceEvent as PersonalizationEvent, + Profile, + SelectedPersonalizationArray, +} from '@contentful/optimization-api-client/api-schemas' +import type { LogLevels } from '@contentful/optimization-api-client/logger' +import { ConsoleLogSink, logger } from '@contentful/optimization-api-client/logger' import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful' -import type { LogLevels } from 'logger' -import { ConsoleLogSink, logger } from 'logger' import type AnalyticsBase from './analytics/AnalyticsBase' import { OPTIMIZATION_CORE_SDK_NAME, OPTIMIZATION_CORE_SDK_VERSION } from './constants' import { InterceptorManager } from './lib/interceptor' -import type { ResolvedData } from './personalization' -import type PersonalizationBase from './personalization/PersonalizationBase' +import type { + FlagsResolver, + MergeTagValueResolver, + PersonalizationBase, + PersonalizedEntryResolver, + ResolvedData, + ResolverMethods, +} from './personalization' /** * Lifecycle container for event and state interceptors. @@ -74,11 +83,11 @@ export interface CoreConfig extends Pick(entry: Entry, personalizations?: SelectedPersonalizationArray): ResolvedData { - return this.personalization.personalizeEntry(entry, personalizations) + return this._personalization.personalizeEntry(entry, personalizations) } /** @@ -187,8 +240,8 @@ abstract class CoreBase { * const name = core.getMergeTagValue(mergeTagNode, profile) * ``` */ - getMergeTagValue(embeddedEntryNodeTarget: MergeTagEntry, profile?: Profile): unknown { - return this.personalization.getMergeTagValue(embeddedEntryNodeTarget, profile) + getMergeTagValue(embeddedEntryNodeTarget: MergeTagEntry, profile?: Profile): string | undefined { + return this._personalization.getMergeTagValue(embeddedEntryNodeTarget, profile) } /** @@ -204,7 +257,7 @@ abstract class CoreBase { async identify( payload: IdentifyBuilderArgs & { profile?: PartialProfile }, ): Promise { - return await this.personalization.identify(payload) + return await this._personalization.identify(payload) } /** @@ -220,7 +273,7 @@ abstract class CoreBase { async page( payload: PageViewBuilderArgs & { profile?: PartialProfile }, ): Promise { - return await this.personalization.page(payload) + return await this._personalization.page(payload) } /** @@ -236,7 +289,7 @@ abstract class CoreBase { async screen( payload: ScreenViewBuilderArgs & { profile?: PartialProfile }, ): Promise { - return await this.personalization.screen(payload) + return await this._personalization.screen(payload) } /** @@ -252,7 +305,7 @@ abstract class CoreBase { async track( payload: TrackBuilderArgs & { profile?: PartialProfile }, ): Promise { - return await this.personalization.track(payload) + return await this._personalization.track(payload) } /** @@ -274,10 +327,10 @@ abstract class CoreBase { payload: ComponentViewBuilderArgs & { profile?: PartialProfile }, ): Promise { if (payload.sticky) { - return await this.personalization.trackComponentView(payload) + return await this._personalization.trackComponentView(payload) } - await this.analytics.trackComponentView(payload) + await this._analytics.trackComponentView(payload) } /** @@ -291,7 +344,7 @@ abstract class CoreBase { * ``` */ async trackComponentClick(payload: ComponentClickBuilderArgs): Promise { - await this.analytics.trackComponentClick(payload) + await this._analytics.trackComponentClick(payload) } /** @@ -305,7 +358,7 @@ abstract class CoreBase { * ``` */ async trackComponentHover(payload: ComponentHoverBuilderArgs): Promise { - await this.analytics.trackComponentHover(payload) + await this._analytics.trackComponentHover(payload) } /** @@ -319,7 +372,7 @@ abstract class CoreBase { * ``` */ async trackFlagView(payload: ComponentViewBuilderArgs): Promise { - await this.analytics.trackFlagView(payload) + await this._analytics.trackFlagView(payload) } } diff --git a/universal/core/src/CoreStateful.test.ts b/universal/core/src/CoreStateful.test.ts index 15cfdac8..f883964b 100644 --- a/universal/core/src/CoreStateful.test.ts +++ b/universal/core/src/CoreStateful.test.ts @@ -7,66 +7,10 @@ import CoreStateful, { type CoreStatefulConfig, type PreviewPanelSignalObject, } from './CoreStateful' +import type { QueueFlushFailureContext } from './lib/queue' import { batch, signalFns, signals } from './signals' import { PREVIEW_PANEL_SIGNAL_FNS_SYMBOL, PREVIEW_PANEL_SIGNALS_SYMBOL } from './symbols' - -const getAnalyticsQueuePolicyBaseBackoffMs = (core: CoreStateful): number | undefined => { - const flushRuntime = Reflect.get(core.analytics, 'flushRuntime') - - if (typeof flushRuntime !== 'object' || flushRuntime === null) { - return - } - - const policy = Reflect.get(flushRuntime, 'policy') - - if (typeof policy !== 'object' || policy === null) { - return - } - - const baseBackoffMs = Reflect.get(policy, 'baseBackoffMs') - - return typeof baseBackoffMs === 'number' ? baseBackoffMs : undefined -} - -const getPersonalizationQueueMaxEvents = (core: CoreStateful): number | undefined => { - const policy = Reflect.get(core.personalization, 'queuePolicy') - - if (typeof policy !== 'object' || policy === null) { - return - } - - const maxEvents = Reflect.get(policy, 'maxEvents') - - return typeof maxEvents === 'number' ? maxEvents : undefined -} - -const getPersonalizationFlushPolicyBaseBackoffMs = (core: CoreStateful): number | undefined => { - const policy = Reflect.get(core.personalization, 'queuePolicy') - - if (typeof policy !== 'object' || policy === null) { - return - } - - const flushPolicy = Reflect.get(policy, 'flushPolicy') - - if (typeof flushPolicy !== 'object' || flushPolicy === null) { - return - } - - const baseBackoffMs = Reflect.get(flushPolicy, 'baseBackoffMs') - - return typeof baseBackoffMs === 'number' ? baseBackoffMs : undefined -} - -const getPersonalizationAllowedEventTypes = (core: CoreStateful): string[] | undefined => { - const allowedEventTypes = Reflect.get(core.personalization, 'allowedEventTypes') - - if (!Array.isArray(allowedEventTypes)) { - return - } - - return allowedEventTypes.filter((eventType): eventType is string => typeof eventType === 'string') -} +import { profile as profileFixture } from './test/fixtures/profile' const config: CoreStatefulConfig = { clientId: 'key_123', @@ -128,6 +72,8 @@ describe('CoreStateful blocked event handling', () => { core?.destroy() } + + rs.restoreAllMocks() }) it('emits consent-blocked calls through callback and blockedEventStream', async () => { @@ -179,38 +125,125 @@ describe('CoreStateful blocked event handling', () => { expect(signals.blockedEvent.value).toBeUndefined() }) - it('defaults allowedEventTypes to identify/page/screen in core', () => { + it('defaults allowedEventTypes to identify/page/screen in core', async () => { const core = createCoreStateful() + const payload: TrackBuilderArgs = { event: 'purchase' } + const blockedEvents: Array = [] + const subscription = core.states.blockedEventStream.subscribe((event) => { + blockedEvents.push(event) + }) - expect(getPersonalizationAllowedEventTypes(core)).toEqual(['identify', 'page', 'screen']) - }) + await core.identify({ userId: 'user-1' }) + await core.page({}) + await core.screen({ name: 'Home', properties: {}, screen: { name: 'Home' } }) + await core.track(payload) - it('uses analytics.queuePolicy when provided', () => { - const core = createCoreStateful({ - analytics: { - queuePolicy: { - baseBackoffMs: 123, - }, - }, - }) + expect(blockedEvents.at(-1)).toEqual( + expect.objectContaining({ + reason: 'consent', + product: 'personalization', + method: 'track', + }), + ) - expect(getAnalyticsQueuePolicyBaseBackoffMs(core)).toBe(123) + subscription.unsubscribe() }) - it('uses personalization.queuePolicy when provided', () => { - const core = createCoreStateful({ - personalization: { - queuePolicy: { - maxEvents: 33, - flushPolicy: { - baseBackoffMs: 321, + it('uses analytics.queuePolicy when provided', async () => { + rs.useFakeTimers() + + try { + const onFlushFailure = rs.fn<(context: QueueFlushFailureContext) => void>() + const core = createCoreStatefulHarness({ + defaults: { + consent: true, + profile: profileFixture, + }, + analytics: { + queuePolicy: { + baseBackoffMs: 123, + jitterRatio: 0, + maxBackoffMs: 123, + onFlushFailure, }, }, - }, - }) + }) + rs.spyOn(core.api.insights, 'sendBatchEvents').mockRejectedValue(new Error('insights-down')) + + core.setOnlineState(false) + await core.trackComponentClick({ componentId: 'hero-banner' }) + + core.setOnlineState(true) + await core.flush() + + expect(onFlushFailure).toHaveBeenCalledTimes(1) + expect(onFlushFailure).toHaveBeenCalledWith( + expect.objectContaining({ + consecutiveFailures: 1, + queuedBatches: 1, + queuedEvents: 1, + retryDelayMs: 123, + }), + ) + } finally { + rs.clearAllTimers() + rs.useRealTimers() + } + }) - expect(getPersonalizationQueueMaxEvents(core)).toBe(33) - expect(getPersonalizationFlushPolicyBaseBackoffMs(core)).toBe(321) + it('uses personalization.queuePolicy when provided', async () => { + rs.useFakeTimers() + + try { + const onDrop = rs.fn() + const onFlushFailure = rs.fn<(context: QueueFlushFailureContext) => void>() + const core = createCoreStatefulHarness({ + defaults: { consent: true }, + personalization: { + queuePolicy: { + maxEvents: 2, + onDrop, + flushPolicy: { + baseBackoffMs: 321, + jitterRatio: 0, + maxBackoffMs: 321, + onFlushFailure, + }, + }, + }, + }) + rs.spyOn(core.api.experience, 'upsertProfile').mockRejectedValue(new Error('experience-down')) + + core.setOnlineState(false) + await core.track({ event: 'e1' }) + await core.track({ event: 'e2' }) + await core.track({ event: 'e3' }) + + expect(onDrop).toHaveBeenCalledTimes(1) + expect(onDrop).toHaveBeenCalledWith( + expect.objectContaining({ + droppedCount: 1, + maxEvents: 2, + queuedEvents: 2, + }), + ) + + core.setOnlineState(true) + await core.flush() + + expect(onFlushFailure).toHaveBeenCalledTimes(1) + expect(onFlushFailure).toHaveBeenCalledWith( + expect.objectContaining({ + consecutiveFailures: 1, + queuedBatches: 1, + queuedEvents: 2, + retryDelayMs: 321, + }), + ) + } finally { + rs.clearAllTimers() + rs.useRealTimers() + } }) it('supports only one stateful instance per runtime until destroy is called', () => { diff --git a/universal/core/src/CoreStateful.ts b/universal/core/src/CoreStateful.ts index 653cc876..86909208 100644 --- a/universal/core/src/CoreStateful.ts +++ b/universal/core/src/CoreStateful.ts @@ -4,7 +4,7 @@ import type { ExperienceEvent as PersonalizationEvent, Profile, SelectedPersonalizationArray, -} from '@contentful/optimization-api-client' +} from '@contentful/optimization-api-client/api-schemas' import { AnalyticsStateful, type AnalyticsProductConfig, type AnalyticsStates } from './analytics' import type { BlockedEvent } from './BlockedEvent' import type { ConsentController } from './Consent' @@ -197,9 +197,9 @@ class CoreStateful extends CoreBase implements ConsentController { private destroyed = false /** Stateful analytics product. */ - readonly analytics: AnalyticsStateful + protected _analytics: AnalyticsStateful /** Stateful personalization product. */ - readonly personalization: PersonalizationStateful + protected _personalization: PersonalizationStateful /** * Create a stateful core with optional default consent and product defaults. @@ -244,7 +244,7 @@ class CoreStateful extends CoreBase implements ConsentController { consent.value = defaultConsent } - this.analytics = new AnalyticsStateful({ + this._analytics = new AnalyticsStateful({ api: this.api, builder: this.eventBuilder, config: { @@ -259,7 +259,7 @@ class CoreStateful extends CoreBase implements ConsentController { interceptors: this.interceptors, }) - this.personalization = new PersonalizationStateful({ + this._personalization = new PersonalizationStateful({ api: this.api, builder: this.eventBuilder, config: { @@ -346,8 +346,8 @@ class CoreStateful extends CoreBase implements ConsentController { * ``` */ async flush(): Promise { - await this.analytics.flush() - await this.personalization.flush() + await this._analytics.flush() + await this._personalization.flush() } /** diff --git a/universal/core/src/CoreStateless.ts b/universal/core/src/CoreStateless.ts index 5ddd29b0..041d2bc4 100644 --- a/universal/core/src/CoreStateless.ts +++ b/universal/core/src/CoreStateless.ts @@ -34,9 +34,9 @@ export interface CoreStatelessConfig extends CoreConfig { */ class CoreStateless extends CoreBase { /** Stateless analytics product. */ - readonly analytics: AnalyticsStateless + protected _analytics: AnalyticsStateless /** Stateless personalization product. */ - readonly personalization: PersonalizationStateless + protected _personalization: PersonalizationStateless /** * Create a stateless core. Product instances share the same API client and @@ -46,21 +46,19 @@ class CoreStateless extends CoreBase { * @example * ```ts * const sdk = new CoreStateless({ clientId: 'app', environment: 'prod' }) - * core.analytics.trackFlagView({ componentId: 'hero' }) - * // or * core.trackFlagView({ componentId: 'hero' }) * ``` */ constructor(config: CoreStatelessConfig) { super(config) - this.analytics = new AnalyticsStateless({ + this._analytics = new AnalyticsStateless({ api: this.api, builder: this.eventBuilder, interceptors: this.interceptors, }) - this.personalization = new PersonalizationStateless({ + this._personalization = new PersonalizationStateless({ api: this.api, builder: this.eventBuilder, interceptors: this.interceptors, diff --git a/universal/core/src/ProductBase.ts b/universal/core/src/ProductBase.ts index 760537c4..7dc519ee 100644 --- a/universal/core/src/ProductBase.ts +++ b/universal/core/src/ProductBase.ts @@ -1,10 +1,9 @@ +import type { ApiClient, EventBuilder } from '@contentful/optimization-api-client' import type { InsightsEventType as AnalyticsEventType, - ApiClient, - EventBuilder, ExperienceEventType as PersonalizationEventType, -} from '@contentful/optimization-api-client' -import { createScopedLogger } from 'logger' +} from '@contentful/optimization-api-client/api-schemas' +import { createScopedLogger } from '@contentful/optimization-api-client/logger' import type { BlockedEvent, BlockedEventProduct, BlockedEventReason } from './BlockedEvent' import type { LifecycleInterceptors } from './CoreBase' import { blockedEvent } from './signals' diff --git a/universal/core/src/analytics/AnalyticsStateful.test.ts b/universal/core/src/analytics/AnalyticsStateful.test.ts index b7dda288..7a11d1b2 100644 --- a/universal/core/src/analytics/AnalyticsStateful.test.ts +++ b/universal/core/src/analytics/AnalyticsStateful.test.ts @@ -1,4 +1,5 @@ -import { ApiClient, EventBuilder, type Profile } from '@contentful/optimization-api-client' +import { ApiClient, EventBuilder } from '@contentful/optimization-api-client' +import type { Profile } from '@contentful/optimization-api-client/api-schemas' import type { LifecycleInterceptors } from '../CoreBase' import { InterceptorManager } from '../lib/interceptor' import type { diff --git a/universal/core/src/analytics/AnalyticsStateful.ts b/universal/core/src/analytics/AnalyticsStateful.ts index 20092e15..c60c2908 100644 --- a/universal/core/src/analytics/AnalyticsStateful.ts +++ b/universal/core/src/analytics/AnalyticsStateful.ts @@ -1,15 +1,17 @@ +import type { + ComponentClickBuilderArgs, + ComponentHoverBuilderArgs, + ComponentViewBuilderArgs, +} from '@contentful/optimization-api-client' import { InsightsEvent as AnalyticsEvent, parseWithFriendlyError, type BatchInsightsEventArray, - type ComponentClickBuilderArgs, - type ComponentHoverBuilderArgs, - type ComponentViewBuilderArgs, type InsightsEventArray, type ExperienceEvent as PersonalizationEvent, type Profile, -} from '@contentful/optimization-api-client' -import { createScopedLogger } from 'logger' +} from '@contentful/optimization-api-client/api-schemas' +import { createScopedLogger } from '@contentful/optimization-api-client/logger' import type { BlockedEvent } from '../BlockedEvent' import type { ConsentGuard } from '../Consent' import { guardedBy } from '../lib/decorators' diff --git a/universal/core/src/analytics/AnalyticsStateless.ts b/universal/core/src/analytics/AnalyticsStateless.ts index 9698d091..69850445 100644 --- a/universal/core/src/analytics/AnalyticsStateless.ts +++ b/universal/core/src/analytics/AnalyticsStateless.ts @@ -1,13 +1,15 @@ +import type { + ComponentClickBuilderArgs, + ComponentHoverBuilderArgs, + ComponentViewBuilderArgs, +} from '@contentful/optimization-api-client' import { InsightsEvent as AnalyticsEvent, BatchInsightsEventArray, parseWithFriendlyError, - type ComponentClickBuilderArgs, - type ComponentHoverBuilderArgs, - type ComponentViewBuilderArgs, type PartialProfile, -} from '@contentful/optimization-api-client' -import { createScopedLogger } from 'logger' +} from '@contentful/optimization-api-client/api-schemas' +import { createScopedLogger } from '@contentful/optimization-api-client/logger' import AnalyticsBase from './AnalyticsBase' const logger = createScopedLogger('Analytics') diff --git a/universal/core/src/api-client.ts b/universal/core/src/api-client.ts new file mode 100644 index 00000000..03b8ceca --- /dev/null +++ b/universal/core/src/api-client.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-api-client' diff --git a/universal/core/src/api-schemas.ts b/universal/core/src/api-schemas.ts new file mode 100644 index 00000000..7a1e0506 --- /dev/null +++ b/universal/core/src/api-schemas.ts @@ -0,0 +1 @@ +export * from '@contentful/optimization-api-client/api-schemas' diff --git a/universal/core/src/index.ts b/universal/core/src/index.ts index 37558270..c7a4ea66 100644 --- a/universal/core/src/index.ts +++ b/universal/core/src/index.ts @@ -14,8 +14,6 @@ export { type Signals, } from './signals' -export * from '@contentful/optimization-api-client' -export * from 'logger' export * from './analytics' export type * from './BlockedEvent' export * from './constants' diff --git a/universal/core/src/logger.ts b/universal/core/src/logger.ts new file mode 100644 index 00000000..ead66c0d --- /dev/null +++ b/universal/core/src/logger.ts @@ -0,0 +1,2 @@ +export * from '@contentful/optimization-api-client/logger' +export { default } from '@contentful/optimization-api-client/logger' diff --git a/universal/core/src/personalization/PersonalizationBase.ts b/universal/core/src/personalization/PersonalizationBase.ts index aa54899b..041e340b 100644 --- a/universal/core/src/personalization/PersonalizationBase.ts +++ b/universal/core/src/personalization/PersonalizationBase.ts @@ -1,16 +1,19 @@ import type { - ChangeArray, ComponentViewBuilderArgs, IdentifyBuilderArgs, + PageViewBuilderArgs, + ScreenViewBuilderArgs, + TrackBuilderArgs, +} from '@contentful/optimization-api-client' +import type { + ChangeArray, + Flags, Json, MergeTagEntry, OptimizationData, - PageViewBuilderArgs, Profile, - ScreenViewBuilderArgs, SelectedPersonalizationArray, - TrackBuilderArgs, -} from '@contentful/optimization-api-client' +} from '@contentful/optimization-api-client/api-schemas' import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful' import ProductBase from '../ProductBase' import { @@ -28,7 +31,7 @@ import { * This interface exists to document that the included methods should not be * considered static. */ -interface ResolverMethods { +export interface ResolverMethods { /** * Get the specified Custom Flag's value from the supplied changes. * @param name - The name or key of the Custom Flag. @@ -40,6 +43,16 @@ interface ResolverMethods { * */ getCustomFlag: (name: string, changes?: ChangeArray) => Json + /** + * Get all resolved Custom Flags from the supplied changes. + * @param changes - Optional changes array. + * @returns The resolved Custom Flag map. + * @remarks + * The changes array can be sourced from the data returned when emitting any + * personalization event. + * */ + getCustomFlags: (changes?: ChangeArray) => Flags + /** * Resolve a Contentful entry to a personalized variant using the current * or provided selected personalizations. @@ -69,7 +82,10 @@ interface ResolverMethods { * Merge tags are references to profile data that can be substituted into content. The * profile can be sourced from the data returned when emitting any personalization event. */ - getMergeTagValue: (embeddedEntryNodeTarget: MergeTagEntry, profile?: Profile) => unknown + getMergeTagValue: ( + embeddedEntryNodeTarget: MergeTagEntry, + profile?: Profile, + ) => string | undefined } /** @@ -114,7 +130,19 @@ abstract class PersonalizationBase extends ProductBase implements ResolverMethod * personalization event. * */ getCustomFlag(name: string, changes?: ChangeArray): Json { - return FlagsResolver.resolve(changes)[name] + return this.getCustomFlags(changes)[name] + } + + /** + * Get all resolved Custom Flags from the supplied changes. + * @param changes - Optional changes array. + * @returns The resolved Custom Flag map. + * @remarks + * The changes array can be sourced from the data returned when emitting any + * personalization event. + * */ + getCustomFlags(changes?: ChangeArray): Flags { + return FlagsResolver.resolve(changes) } /** @@ -149,7 +177,7 @@ abstract class PersonalizationBase extends ProductBase implements ResolverMethod * Merge tags are references to profile data that can be substituted into content. The * profile can be sourced from the data returned when emitting any personalization event. */ - getMergeTagValue(embeddedEntryNodeTarget: MergeTagEntry, profile?: Profile): unknown { + getMergeTagValue(embeddedEntryNodeTarget: MergeTagEntry, profile?: Profile): string | undefined { return MergeTagValueResolver.resolve(embeddedEntryNodeTarget, profile) } diff --git a/universal/core/src/personalization/PersonalizationStateful.test.ts b/universal/core/src/personalization/PersonalizationStateful.test.ts index 8b895c35..c415e534 100644 --- a/universal/core/src/personalization/PersonalizationStateful.test.ts +++ b/universal/core/src/personalization/PersonalizationStateful.test.ts @@ -1,14 +1,14 @@ -import { - ApiClient, - EventBuilder, - type ExperienceEventArray, - type OptimizationData, - type Profile, -} from '@contentful/optimization-api-client' +import { ApiClient, EventBuilder } from '@contentful/optimization-api-client' +import type { + ChangeArray, + ExperienceEventArray, + OptimizationData, + Profile, +} from '@contentful/optimization-api-client/api-schemas' import type { LifecycleInterceptors } from '../CoreBase' import { InterceptorManager } from '../lib/interceptor' import type { QueueFlushFailureContext, QueueFlushRecoveredContext } from '../lib/queue' -import { batch, consent, online, profile } from '../signals' +import { batch, changes as changesSignal, consent, online, profile } from '../signals' import PersonalizationStateful, { type PersonalizationOfflineQueueDropContext, type PersonalizationQueuePolicy, @@ -384,3 +384,69 @@ describe('PersonalizationStateful offline queue policy', () => { expect(upsertProfile).toHaveBeenCalledTimes(1) }) }) + +const CHANGES: ChangeArray = [ + { + key: 'dark-mode', + type: 'Variable', + value: true, + meta: { + experienceId: 'experience-id', + variantIndex: 0, + }, + }, + { + key: 'config', + type: 'Variable', + value: { + value: { + amount: 10, + currency: 'USD', + }, + }, + meta: { + experienceId: 'experience-id', + variantIndex: 1, + }, + }, +] + +describe('PersonalizationStateful custom flags', () => { + beforeEach(() => { + batch(() => { + changesSignal.value = undefined + }) + }) + + afterEach(() => { + rs.restoreAllMocks() + }) + + it('resolves all custom flags from provided changes', () => { + const personalization = createPersonalization() + + expect(personalization.getCustomFlags(CHANGES)).toEqual({ + 'dark-mode': true, + config: { + amount: 10, + currency: 'USD', + }, + }) + }) + + it('uses signal changes by default when resolving all custom flags', () => { + const personalization = createPersonalization() + + batch(() => { + changesSignal.value = CHANGES + }) + + expect(personalization.getCustomFlags()).toEqual({ + 'dark-mode': true, + config: { + amount: 10, + currency: 'USD', + }, + }) + }) +}) diff --git a/universal/core/src/personalization/PersonalizationStateful.ts b/universal/core/src/personalization/PersonalizationStateful.ts index d2c9ad41..ce1def99 100644 --- a/universal/core/src/personalization/PersonalizationStateful.ts +++ b/universal/core/src/personalization/PersonalizationStateful.ts @@ -1,24 +1,26 @@ +import type { + ComponentViewBuilderArgs, + IdentifyBuilderArgs, + PageViewBuilderArgs, + ScreenViewBuilderArgs, + TrackBuilderArgs, +} from '@contentful/optimization-api-client' import { type InsightsEvent as AnalyticsEvent, type ChangeArray, - type ComponentViewBuilderArgs, type Flags, - type IdentifyBuilderArgs, type Json, type MergeTagEntry, type OptimizationData, - type PageViewBuilderArgs, parseWithFriendlyError, ExperienceEvent as PersonalizationEvent, type ExperienceEventArray as PersonalizationEventArray, type Profile, - type ScreenViewBuilderArgs, type SelectedPersonalizationArray, - type TrackBuilderArgs, -} from '@contentful/optimization-api-client' +} from '@contentful/optimization-api-client/api-schemas' +import { createScopedLogger } from '@contentful/optimization-api-client/logger' import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful' import { isEqual } from 'es-toolkit/predicate' -import { createScopedLogger } from 'logger' import type { BlockedEvent } from '../BlockedEvent' import type { ConsentGuard } from '../Consent' import { guardedBy } from '../lib/decorators' @@ -321,6 +323,19 @@ class PersonalizationStateful extends PersonalizationBase implements ConsentGuar return super.getCustomFlag(name, changes) } + /** + * Get all resolved Custom Flags (derived from the signal). + * @param changes - Optional changes array; defaults to the current signal value. + * @returns The resolved Custom Flag map. + * @example + * ```ts + * const flags = personalization.getCustomFlags() + * ``` + */ + getCustomFlags(changes: ChangeArray | undefined = changesSignal.value): Flags { + return super.getCustomFlags(changes) + } + /** * Resolve a Contentful entry to a personalized variant using the current * or provided selections. @@ -363,7 +378,7 @@ class PersonalizationStateful extends PersonalizationBase implements ConsentGuar getMergeTagValue( embeddedEntryNodeTarget: MergeTagEntry, profile: Profile | undefined = profileSignal.value, - ): unknown { + ): string | undefined { return super.getMergeTagValue(embeddedEntryNodeTarget, profile) } diff --git a/universal/core/src/personalization/PersonalizationStateless.ts b/universal/core/src/personalization/PersonalizationStateless.ts index 974ad1f3..519eddf3 100644 --- a/universal/core/src/personalization/PersonalizationStateless.ts +++ b/universal/core/src/personalization/PersonalizationStateless.ts @@ -1,20 +1,22 @@ +import type { + ComponentViewBuilderArgs, + IdentifyBuilderArgs, + PageViewBuilderArgs, + ScreenViewBuilderArgs, + TrackBuilderArgs, +} from '@contentful/optimization-api-client' import { - type ComponentViewBuilderArgs, ComponentViewEvent, - type IdentifyBuilderArgs, IdentifyEvent, type OptimizationData, - type PageViewBuilderArgs, PageViewEvent, parseWithFriendlyError, type PartialProfile, ExperienceEvent as PersonalizationEvent, - type ScreenViewBuilderArgs, ScreenViewEvent, - type TrackBuilderArgs, TrackEvent, -} from '@contentful/optimization-api-client' -import { createScopedLogger } from 'logger' +} from '@contentful/optimization-api-client/api-schemas' +import { createScopedLogger } from '@contentful/optimization-api-client/logger' import PersonalizationBase from './PersonalizationBase' const logger = createScopedLogger('Personalization') diff --git a/universal/core/src/personalization/index.ts b/universal/core/src/personalization/index.ts index 7d266fe0..49738477 100644 --- a/universal/core/src/personalization/index.ts +++ b/universal/core/src/personalization/index.ts @@ -1,5 +1,8 @@ export * from './resolvers' +export * from './PersonalizationBase' +export { default as PersonalizationBase } from './PersonalizationBase' + export * from './PersonalizationStateful' export { default as PersonalizationStateful } from './PersonalizationStateful' diff --git a/universal/core/src/personalization/resolvers/FlagsResolver.ts b/universal/core/src/personalization/resolvers/FlagsResolver.ts index 96c6ce5f..5864af20 100644 --- a/universal/core/src/personalization/resolvers/FlagsResolver.ts +++ b/universal/core/src/personalization/resolvers/FlagsResolver.ts @@ -1,4 +1,4 @@ -import type { ChangeArray, Flags } from '@contentful/optimization-api-client' +import type { ChangeArray, Flags } from '@contentful/optimization-api-client/api-schemas' /** * Resolves a {@link Flags} map from a list of optimization changes. diff --git a/universal/core/src/personalization/resolvers/MergeTagValueResolver.test.ts b/universal/core/src/personalization/resolvers/MergeTagValueResolver.test.ts index 6d9d4add..2a636a55 100644 --- a/universal/core/src/personalization/resolvers/MergeTagValueResolver.test.ts +++ b/universal/core/src/personalization/resolvers/MergeTagValueResolver.test.ts @@ -1,3 +1,4 @@ +import { isMergeTagEntry } from '@contentful/optimization-api-client/api-schemas' import { cloneDeep } from 'es-toolkit' import { mergeTagEntry } from '../../test/fixtures/mergeTagEntry' import { profile } from '../../test/fixtures/profile' @@ -43,11 +44,11 @@ describe('MergeTagValueResolver', () => { describe('isMergeTagEntry', () => { it('returns false when argument is not a merge tag entry', () => { - expect(MergeTagValueResolver.isMergeTagEntry({})).toBeFalsy() + expect(isMergeTagEntry({})).toBeFalsy() }) it('returns true when argument is a merge tag entry', () => { - expect(MergeTagValueResolver.isMergeTagEntry(mergeTagEntry)).toBeTruthy() + expect(isMergeTagEntry(mergeTagEntry)).toBeTruthy() }) }) diff --git a/universal/core/src/personalization/resolvers/MergeTagValueResolver.ts b/universal/core/src/personalization/resolvers/MergeTagValueResolver.ts index 1215ee6d..b37ad3e1 100644 --- a/universal/core/src/personalization/resolvers/MergeTagValueResolver.ts +++ b/universal/core/src/personalization/resolvers/MergeTagValueResolver.ts @@ -1,5 +1,6 @@ -import { MergeTagEntry, Profile } from '@contentful/optimization-api-client' -import { createScopedLogger } from 'logger' +import type { MergeTagEntry } from '@contentful/optimization-api-client/api-schemas' +import { Profile, isMergeTagEntry } from '@contentful/optimization-api-client/api-schemas' +import { createScopedLogger } from '@contentful/optimization-api-client/logger' const logger = createScopedLogger('Personalization') @@ -36,22 +37,6 @@ const getAtPath = (value: unknown, path: string): unknown => { * Result values are returned as strings; numeric/boolean primitives are stringified. */ const MergeTagValueResolver = { - /** - * Type guard to ensure the input is a {@link MergeTagEntry}. - * - * @param embeddedEntryNodeTarget - Unknown value to validate. - * @returns `true` if the input is a valid merge-tag entry. - * @example - * ```ts - * if (MergeTagValueResolver.isMergeTagEntry(node)) { - * // safe to read fields - * } - * ``` - */ - isMergeTagEntry(embeddedEntryNodeTarget: unknown): embeddedEntryNodeTarget is MergeTagEntry { - return MergeTagEntry.safeParse(embeddedEntryNodeTarget).success - }, - /** * Generate a list of candidate selectors for a merge-tag ID. * @@ -122,7 +107,7 @@ const MergeTagValueResolver = { * ``` */ resolve(mergeTagEntry: MergeTagEntry | undefined, profile?: Profile): string | undefined { - if (!MergeTagValueResolver.isMergeTagEntry(mergeTagEntry)) { + if (!isMergeTagEntry(mergeTagEntry)) { logger.warn(`${RESOLUTION_WARNING_BASE} supplied entry is not a Merge Tag entry`) return } diff --git a/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.test.ts b/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.test.ts index 7443338b..8ed85f01 100644 --- a/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.test.ts +++ b/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.test.ts @@ -9,7 +9,7 @@ import { type PersonalizationEntry, type PersonalizedEntry, type SelectedPersonalizationArray, -} from '@contentful/optimization-api-client' +} from '@contentful/optimization-api-client/api-schemas' import { describe, expect, it, rs } from '@rstest/core' import type { Entry } from 'contentful' diff --git a/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.ts b/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.ts index fdf2ace8..49f216e7 100644 --- a/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.ts +++ b/universal/core/src/personalization/resolvers/PersonalizedEntryResolver.ts @@ -10,9 +10,9 @@ import { type PersonalizedEntry, type SelectedPersonalization, type SelectedPersonalizationArray, -} from '@contentful/optimization-api-client' +} from '@contentful/optimization-api-client/api-schemas' +import { createScopedLogger } from '@contentful/optimization-api-client/logger' import type { ChainModifiers, Entry, EntrySkeletonType, LocaleCode } from 'contentful' -import { createScopedLogger } from 'logger' const logger = createScopedLogger('Personalization') diff --git a/universal/core/src/signals.ts b/universal/core/src/signals.ts index 6e9c2ca6..970f5633 100644 --- a/universal/core/src/signals.ts +++ b/universal/core/src/signals.ts @@ -5,7 +5,7 @@ import type { ExperienceEvent as PersonalizationEvent, Profile, SelectedPersonalizationArray, -} from '@contentful/optimization-api-client' +} from '@contentful/optimization-api-client/api-schemas' import { batch, computed, effect, signal, type Signal, untracked } from '@preact/signals-core' import type { BlockedEvent } from './BlockedEvent' import { FlagsResolver } from './personalization/resolvers' diff --git a/universal/core/src/test/fixtures/mergeTagEntry.ts b/universal/core/src/test/fixtures/mergeTagEntry.ts index b0133560..10e4bbc5 100644 --- a/universal/core/src/test/fixtures/mergeTagEntry.ts +++ b/universal/core/src/test/fixtures/mergeTagEntry.ts @@ -1,4 +1,4 @@ -import type { MergeTagEntry } from '@contentful/optimization-api-client' +import type { MergeTagEntry } from '@contentful/optimization-api-client/api-schemas' export const mergeTagEntry: MergeTagEntry = { metadata: { diff --git a/universal/core/src/test/fixtures/profile.ts b/universal/core/src/test/fixtures/profile.ts index 03c32b1b..ef2cb03a 100644 --- a/universal/core/src/test/fixtures/profile.ts +++ b/universal/core/src/test/fixtures/profile.ts @@ -1,4 +1,4 @@ -import type { Profile } from '@contentful/optimization-api-client' +import type { Profile } from '@contentful/optimization-api-client/api-schemas' export const profile: Profile = { id: '', diff --git a/universal/core/src/test/setup.ts b/universal/core/src/test/setup.ts index f672daac..ef7cc2ea 100644 --- a/universal/core/src/test/setup.ts +++ b/universal/core/src/test/setup.ts @@ -2,7 +2,7 @@ import { rs } from '@rstest/core' import { experienceApiHandlers, loggerMock, resetMockLogger } from 'mocks' import { setupServer } from 'msw/node' -rs.mock('logger', () => loggerMock) +rs.mock('@contentful/optimization-api-client/logger', () => loggerMock) export const server = setupServer(...experienceApiHandlers.getHandlers()) diff --git a/universal/core/tsconfig.build.json b/universal/core/tsconfig.build.json index b4496858..929aeaff 100644 --- a/universal/core/tsconfig.build.json +++ b/universal/core/tsconfig.build.json @@ -15,7 +15,6 @@ "include": ["./src/**/*", "./src/**/*.json"], "exclude": ["./src/**/*.test.*", "./src/test/**/*"], "references": [ - { "path": "../../lib/logger/tsconfig.build.json" }, { "path": "../api-schemas/tsconfig.build.json" }, { "path": "../api-client/tsconfig.build.json" } ]