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 +130 -27
- package/dist/index.js +24 -9
- package/dist/index.mjs +24 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Personalize Connect SDK
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
|
24
|
+
export default function App({ children }) {
|
|
21
25
|
return (
|
|
22
26
|
<PersonalizeProvider
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
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({
|
|
55
|
-
const
|
|
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
|
-
##
|
|
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
|
-
|
|
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;
|
|
137
|
+
friendlyId: string; // Personalize Interactive Experience ID
|
|
69
138
|
contentMap: Record<string, string>; // contentKey -> datasource GUID
|
|
70
|
-
defaultKey: string;
|
|
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
|
-
|
|
77
|
-
- `
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
- `
|
|
82
|
-
|
|
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 = "[
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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:
|
|
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
|
|
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 = "[
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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/
|
|
17
|
+
"url": "https://github.com/Sitecore-Hackathon/2026-Team-Solo.git",
|
|
18
18
|
"directory": "packages/sdk"
|
|
19
19
|
},
|
|
20
20
|
"main": "dist/index.js",
|