personalize-connect-sdk 1.3.1 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Personalize Connect SDK
2
2
 
3
- A lightweight Next.js package that bridges Sitecore Personalize Interactive Experiences with XM Cloud component datasources at runtime. The SDK reads configuration authored by the Marketplace app, calls Personalize for a decision, and swaps component content accordingly — with zero per-component code.
3
+ Runtime SDK for [Personalize Connect](https://github.com/Sitecore-Hackathon/2026-Team-Solo) a zero-code bridge between Sitecore XM Cloud components and Sitecore Personalize Full Stack Interactive Experiences.
4
+
5
+ The SDK reads configuration authored by the Marketplace app (stored in the content tree), calls Personalize for a decision via the Edge proxy, resolves the matching datasource from Experience Edge, and swaps component content — with zero per-component code. In Page Builder, components with personalization get a visual indicator.
4
6
 
5
7
  ## Installation
6
8
 
@@ -10,23 +12,21 @@ npm install personalize-connect-sdk
10
12
 
11
13
  Peer dependency: `react` >= 18.
12
14
 
13
- ## Usage
15
+ ## Quick Start (XM Cloud)
14
16
 
15
17
  ### 1. Wrap your app with `PersonalizeProvider`
16
18
 
19
+ In `_app.tsx` (Pages Router) or `layout.tsx` (App Router):
20
+
17
21
  ```tsx
18
22
  import { PersonalizeProvider } from "personalize-connect-sdk";
19
23
 
20
- export default function RootLayout({ children }) {
24
+ export default function App({ children }) {
21
25
  return (
22
26
  <PersonalizeProvider
23
- clientKey={process.env.NEXT_PUBLIC_PERSONALIZE_CLIENT_KEY!}
24
- pointOfSale={process.env.NEXT_PUBLIC_PERSONALIZE_POINT_OF_SALE!}
25
- resolveDatasource={async (datasourceId) => {
26
- // Fetch datasource fields via Experience Edge GraphQL or Layout Service
27
- const res = await fetch(`/api/datasource/${datasourceId}`);
28
- return res.json();
29
- }}
27
+ sitecoreEdgeContextId={process.env.SITECORE_EDGE_CONTEXT_ID}
28
+ siteName={process.env.SITECORE_SITE_NAME}
29
+ debug // remove in production
30
30
  >
31
31
  {children}
32
32
  </PersonalizeProvider>
@@ -34,49 +34,152 @@ export default function RootLayout({ children }) {
34
34
  }
35
35
  ```
36
36
 
37
+ That's it for provider setup. One prop (`sitecoreEdgeContextId`) drives everything:
38
+
39
+ - **Browser ID** — fetched from `edge-platform.sitecorecloud.io/v1/init`
40
+ - **Personalize calls** — routed through the Edge proxy (`/v1/personalize`)
41
+ - **Datasource resolution** — built-in via Edge proxy GraphQL
42
+ - **Config loading** — auto-discovered from the content tree via Edge
43
+ - **Editing detection** — auto-detected from JSS Sitecore context
44
+
37
45
  ### 2. Wrap components with `withPersonalizeConnect`
38
46
 
39
47
  ```tsx
40
48
  import { withPersonalizeConnect } from "personalize-connect-sdk";
41
- import MyComponent from "./MyComponent";
42
49
 
43
- // Config is read from props.rendering.personalizeConnect (or customize via getConfig)
44
- export default withPersonalizeConnect(MyComponent);
50
+ const PromoCard = ({ fields }) => (
51
+ <div>
52
+ <h2>{fields?.title?.value}</h2>
53
+ <p>{fields?.body?.value}</p>
54
+ </div>
55
+ );
56
+
57
+ export default withPersonalizeConnect(PromoCard);
45
58
  ```
46
59
 
47
- The HOC renders immediately with the default datasource, calls Personalize asynchronously, and re-renders with personalized content when resolved. No changes needed inside the component.
60
+ The HOC:
61
+ 1. Looks up config from the content tree (loaded by the provider on mount)
62
+ 2. Renders with the default datasource immediately
63
+ 3. Calls Personalize asynchronously for a content decision
64
+ 4. Resolves the matching datasource from Experience Edge
65
+ 5. Re-renders with personalized `fields`
66
+ 6. In Page Builder, shows a visual indicator (purple border + badge)
48
67
 
49
68
  ### 3. Or use the `usePersonalizeExperience` hook
50
69
 
51
70
  ```tsx
52
71
  import { usePersonalizeExperience } from "personalize-connect-sdk";
53
72
 
54
- function MyComponent({ personalizeConnect }) {
55
- const { contentKey, resolvedFields, isLoading, error } = usePersonalizeExperience(personalizeConnect);
73
+ function MyComponent({ rendering }) {
74
+ const config = /* get config from context or props */;
75
+ const { contentKey, resolvedFields, isLoading, error } = usePersonalizeExperience(config);
56
76
 
57
77
  if (isLoading) return <Skeleton />;
58
- return <div>{resolvedFields?.heading}</div>;
78
+ return <div>{resolvedFields?.heading?.value}</div>;
59
79
  }
60
80
  ```
61
81
 
62
- ## Config shape
82
+ ## Provider Props
83
+
84
+ ### XM Cloud (recommended)
85
+
86
+ | Prop | Required | Description |
87
+ |------|----------|-------------|
88
+ | `sitecoreEdgeContextId` | Yes | Edge Context ID — drives all Edge proxy calls |
89
+ | `siteName` | Yes | XM Cloud site name |
90
+ | `sitecoreEdgeUrl` | No | Edge platform URL (defaults to `https://edge-platform.sitecorecloud.io`) |
91
+
92
+ ### Legacy (direct credentials)
93
+
94
+ | Prop | Required | Description |
95
+ |------|----------|-------------|
96
+ | `clientKey` | Yes | Personalize API client key |
97
+ | `pointOfSale` | Yes | Point of sale identifier |
98
+ | `edgeUrl` | No | Experience Edge GraphQL endpoint |
99
+ | `apiKey` | No | Sitecore API key for Edge |
100
+
101
+ ### Common
102
+
103
+ | Prop | Default | Description |
104
+ |------|---------|-------------|
105
+ | `channel` | `"WEB"` | Channel for Personalize calls |
106
+ | `language` | `"EN"` | Language code |
107
+ | `currencyCode` | `"USD"` | Currency code |
108
+ | `timeout` | `600` | Personalize call timeout (ms) |
109
+ | `debug` | `false` | Enable `[PersonalizeConnect]` console logging |
110
+ | `isEditing` | auto | Override Page Builder editing detection |
111
+ | `sitePath` | auto | Override site root path auto-discovery |
112
+ | `resolveDatasource` | built-in | Custom datasource resolver (overrides built-in Edge resolution) |
113
+
114
+ ## How Config Loading Works
115
+
116
+ The Marketplace app stores configs in the content tree at:
117
+
118
+ ```
119
+ {sitePath}/Data/PersonalizeConnect/{pageItemId}/config-{renderingId}
120
+ ```
63
121
 
64
- Config is attached to each rendering in XMC layout data (authored by the Marketplace app):
122
+ On mount, the SDK:
123
+ 1. Reads the page item ID from `__NEXT_DATA__` (JSS layout data)
124
+ 2. Queries Edge for the page item's content tree path
125
+ 3. Derives the site root path (first 4 path segments)
126
+ 4. Fetches all config children for that page in one GraphQL query
127
+ 5. Caches them in context, keyed by rendering instance ID
128
+
129
+ Each HOC looks up its config via `props.rendering.uid`. No config on the rendering means no personalization — the component renders normally.
130
+
131
+ ## Config Shape
132
+
133
+ Authored by the Marketplace app, stored as JSON in the content tree:
65
134
 
66
135
  ```ts
67
136
  interface PersonalizeConnectConfig {
68
- friendlyId: string; // Personalize Interactive Experience ID
137
+ friendlyId: string; // Personalize Interactive Experience ID
69
138
  contentMap: Record<string, string>; // contentKey -> datasource GUID
70
- defaultKey: string; // Fallback key if experience fails
139
+ defaultKey: string; // Fallback key
71
140
  }
72
141
  ```
73
142
 
143
+ ## Debug Logging
144
+
145
+ Pass `debug` to the provider to trace the full flow in the browser console:
146
+
147
+ ```
148
+ [PersonalizeConnect] Provider mounting { mode: 'Context ID', ... }
149
+ [PersonalizeConnect] BrowserId (edge): from cookie abc123...
150
+ [PersonalizeConnect] Config loader: Auto-discovered site path: /sitecore/content/company/company
151
+ [PersonalizeConnect] Config loader: loaded config for rendering xyz → experience homepage_promo
152
+ [PersonalizeConnect] [PromoCard] Config active: { friendlyId: 'homepage_promo', ... }
153
+ [PersonalizeConnect] callPersonalize [homepage_promo] → contentKey: returning-visitor
154
+ [PersonalizeConnect] [PromoCard] Fields resolved — swapping props.fields
155
+ ```
156
+
74
157
  ## Exports
75
158
 
76
- - `PersonalizeProvider`, `usePersonalizeContext` — Provider and context
77
- - `withPersonalizeConnect` — HOC for zero-code personalization
159
+ **Provider & Context**
160
+ - `PersonalizeProvider` — Wrap your app
161
+ - `usePersonalizeContext` — Access context directly
162
+
163
+ **HOC & Hook**
164
+ - `withPersonalizeConnect` — Zero-code personalization HOC
78
165
  - `usePersonalizeExperience` — Hook for manual control
79
- - `callPersonalize` — Low-level API client
80
- - `resolveContent` — Map contentKey to datasource fields
81
- - `getBrowserId` — Browser ID cookie helper
82
- - Types: `PersonalizeConnectConfig`, `PersonalizeConnectProviderProps`, etc.
166
+
167
+ **Config**
168
+ - `loadPageConfigs` — Load configs from Edge (used internally, exported for advanced use)
169
+
170
+ **Edge Resolution**
171
+ - `createEdgeResolver` — Direct Edge GraphQL resolver (legacy)
172
+ - `createEdgeProxyResolver` — Edge proxy resolver (Context ID mode)
173
+
174
+ **Browser ID**
175
+ - `getBrowserId` — Legacy local cookie
176
+ - `getEdgeBrowserId` — Edge proxy init
177
+
178
+ **Editing**
179
+ - `isEditingMode` — Page Builder detection
180
+
181
+ **Debug**
182
+ - `setDebug`, `isDebugEnabled` — Control logging
183
+
184
+ **Types**
185
+ - `PersonalizeConnectConfig`, `PersonalizeConnectProviderProps`, `PersonalizeContextValue`, `ComponentFields`, `CallFlowsRequest`, etc.
package/dist/index.js CHANGED
@@ -43,7 +43,7 @@ module.exports = __toCommonJS(index_exports);
43
43
  var import_react = require("react");
44
44
 
45
45
  // src/logger.ts
46
- var PREFIX = "[PersonalizeConnect]";
46
+ var PREFIX = "[PersonalizeConnectSDK]";
47
47
  var enabled = false;
48
48
  function setDebug(on) {
49
49
  enabled = on;
@@ -164,6 +164,11 @@ var ITEM_QUERY = `
164
164
  }
165
165
  }
166
166
  `;
167
+ function formatGuidForEdge(id) {
168
+ const clean = id.replace(/[{}\-\s]/g, "").toLowerCase();
169
+ if (clean.length !== 32 || !/^[0-9a-f]+$/.test(clean)) return id;
170
+ return `{${clean.slice(0, 8)}-${clean.slice(8, 12)}-${clean.slice(12, 16)}-${clean.slice(16, 20)}-${clean.slice(20)}}`;
171
+ }
167
172
  function mapFields(fields) {
168
173
  const result = {};
169
174
  for (const field of fields) {
@@ -174,13 +179,14 @@ function mapFields(fields) {
174
179
  return result;
175
180
  }
176
181
  async function queryEdge(url, headers, datasourceId, language) {
177
- log("Edge GraphQL request:", { url, datasourceId, language });
182
+ const formattedId = formatGuidForEdge(datasourceId);
183
+ log("Edge GraphQL request:", { url, datasourceId, formattedId, language });
178
184
  const res = await fetch(url, {
179
185
  method: "POST",
180
186
  headers: { "Content-Type": "application/json", ...headers },
181
187
  body: JSON.stringify({
182
188
  query: ITEM_QUERY,
183
- variables: { itemId: datasourceId, language }
189
+ variables: { itemId: formattedId, language }
184
190
  })
185
191
  });
186
192
  log("Edge GraphQL response status:", res.status);
@@ -312,9 +318,12 @@ function parseConfigJson(json, renderingId) {
312
318
  if (!contentMap || typeof contentMap !== "object" || !friendlyId) return null;
313
319
  const keys = Object.keys(contentMap);
314
320
  return {
315
- friendlyId,
316
- contentMap,
317
- defaultKey: raw.defaultKey ?? keys[0] ?? ""
321
+ config: {
322
+ friendlyId,
323
+ contentMap,
324
+ defaultKey: raw.defaultKey ?? keys[0] ?? ""
325
+ },
326
+ instanceId: raw.instanceId ?? void 0
318
327
  };
319
328
  } catch {
320
329
  warn("Failed to parse config JSON for rendering", renderingId);
@@ -388,8 +397,13 @@ async function loadPageConfigs(edgeUrl, pageItemId, language, headers = {}, site
388
397
  const normalizedRid = normalizeGuid(renderingId);
389
398
  const parsed = parseConfigJson(configJson, normalizedRid);
390
399
  if (parsed) {
391
- configs.set(normalizedRid, parsed);
392
- log("Config loader: loaded config for rendering", normalizedRid, "\u2192 experience", parsed.friendlyId);
400
+ configs.set(normalizedRid, parsed.config);
401
+ log("Config loader: stored under renderingId", normalizedRid, "\u2192", parsed.config.friendlyId);
402
+ if (parsed.instanceId) {
403
+ const normalizedIid = normalizeGuid(parsed.instanceId);
404
+ configs.set(normalizedIid, parsed.config);
405
+ log("Config loader: also stored under instanceId", normalizedIid);
406
+ }
393
407
  }
394
408
  }
395
409
  log("Config loader: total configs loaded:", configs.size);
@@ -397,6 +411,7 @@ async function loadPageConfigs(edgeUrl, pageItemId, language, headers = {}, site
397
411
  error("Config loader fetch error:", e);
398
412
  }
399
413
  groupEnd();
414
+ log("Config loader: result", Object.fromEntries(configs));
400
415
  return configs;
401
416
  }
402
417
 
@@ -778,7 +793,7 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
778
793
  });
779
794
  config = fromContext;
780
795
  } else if (context.configsLoaded) {
781
- log(`[${componentName}] No config in context for rendering uid ${normalizedUid} \u2014 passthrough`);
796
+ log(`[${componentName}] No config match for uid "${normalizedUid}". All configs:`, Object.fromEntries(context.configs));
782
797
  } else {
783
798
  log(`[${componentName}] Configs still loading for uid ${normalizedUid}...`);
784
799
  }
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
3
3
 
4
4
  // src/logger.ts
5
- var PREFIX = "[PersonalizeConnect]";
5
+ var PREFIX = "[PersonalizeConnectSDK]";
6
6
  var enabled = false;
7
7
  function setDebug(on) {
8
8
  enabled = on;
@@ -123,6 +123,11 @@ var ITEM_QUERY = `
123
123
  }
124
124
  }
125
125
  `;
126
+ function formatGuidForEdge(id) {
127
+ const clean = id.replace(/[{}\-\s]/g, "").toLowerCase();
128
+ if (clean.length !== 32 || !/^[0-9a-f]+$/.test(clean)) return id;
129
+ return `{${clean.slice(0, 8)}-${clean.slice(8, 12)}-${clean.slice(12, 16)}-${clean.slice(16, 20)}-${clean.slice(20)}}`;
130
+ }
126
131
  function mapFields(fields) {
127
132
  const result = {};
128
133
  for (const field of fields) {
@@ -133,13 +138,14 @@ function mapFields(fields) {
133
138
  return result;
134
139
  }
135
140
  async function queryEdge(url, headers, datasourceId, language) {
136
- log("Edge GraphQL request:", { url, datasourceId, language });
141
+ const formattedId = formatGuidForEdge(datasourceId);
142
+ log("Edge GraphQL request:", { url, datasourceId, formattedId, language });
137
143
  const res = await fetch(url, {
138
144
  method: "POST",
139
145
  headers: { "Content-Type": "application/json", ...headers },
140
146
  body: JSON.stringify({
141
147
  query: ITEM_QUERY,
142
- variables: { itemId: datasourceId, language }
148
+ variables: { itemId: formattedId, language }
143
149
  })
144
150
  });
145
151
  log("Edge GraphQL response status:", res.status);
@@ -271,9 +277,12 @@ function parseConfigJson(json, renderingId) {
271
277
  if (!contentMap || typeof contentMap !== "object" || !friendlyId) return null;
272
278
  const keys = Object.keys(contentMap);
273
279
  return {
274
- friendlyId,
275
- contentMap,
276
- defaultKey: raw.defaultKey ?? keys[0] ?? ""
280
+ config: {
281
+ friendlyId,
282
+ contentMap,
283
+ defaultKey: raw.defaultKey ?? keys[0] ?? ""
284
+ },
285
+ instanceId: raw.instanceId ?? void 0
277
286
  };
278
287
  } catch {
279
288
  warn("Failed to parse config JSON for rendering", renderingId);
@@ -347,8 +356,13 @@ async function loadPageConfigs(edgeUrl, pageItemId, language, headers = {}, site
347
356
  const normalizedRid = normalizeGuid(renderingId);
348
357
  const parsed = parseConfigJson(configJson, normalizedRid);
349
358
  if (parsed) {
350
- configs.set(normalizedRid, parsed);
351
- log("Config loader: loaded config for rendering", normalizedRid, "\u2192 experience", parsed.friendlyId);
359
+ configs.set(normalizedRid, parsed.config);
360
+ log("Config loader: stored under renderingId", normalizedRid, "\u2192", parsed.config.friendlyId);
361
+ if (parsed.instanceId) {
362
+ const normalizedIid = normalizeGuid(parsed.instanceId);
363
+ configs.set(normalizedIid, parsed.config);
364
+ log("Config loader: also stored under instanceId", normalizedIid);
365
+ }
352
366
  }
353
367
  }
354
368
  log("Config loader: total configs loaded:", configs.size);
@@ -356,6 +370,7 @@ async function loadPageConfigs(edgeUrl, pageItemId, language, headers = {}, site
356
370
  error("Config loader fetch error:", e);
357
371
  }
358
372
  groupEnd();
373
+ log("Config loader: result", Object.fromEntries(configs));
359
374
  return configs;
360
375
  }
361
376
 
@@ -737,7 +752,7 @@ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG
737
752
  });
738
753
  config = fromContext;
739
754
  } else if (context.configsLoaded) {
740
- log(`[${componentName}] No config in context for rendering uid ${normalizedUid} \u2014 passthrough`);
755
+ log(`[${componentName}] No config match for uid "${normalizedUid}". All configs:`, Object.fromEntries(context.configs));
741
756
  } else {
742
757
  log(`[${componentName}] Configs still loading for uid ${normalizedUid}...`);
743
758
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "personalize-connect-sdk",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Runtime SDK for Personalize Connect - resolves active experience outcomes and datasources in your rendering host",
5
5
  "author": "Dylan Young",
6
6
  "keywords": [
@@ -14,7 +14,7 @@
14
14
  "license": "MIT",
15
15
  "repository": {
16
16
  "type": "git",
17
- "url": "https://github.com/2026-Team-Solo/2026-Team-Solo.git",
17
+ "url": "https://github.com/Sitecore-Hackathon/2026-Team-Solo.git",
18
18
  "directory": "packages/sdk"
19
19
  },
20
20
  "main": "dist/index.js",