@amplifyup/sdk 0.1.21 → 0.1.23

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
  # @amplifyup/sdk
2
2
 
3
- Official JavaScript SDK for [AmplifyUp](https://amplifyup.ai) — event tracking, page layouts, personalization, and Composer preview on your site.
3
+ Official JavaScript SDK for [AmplifyUp](https://amplifyup.ai) — event tracking, Edge-resolved page layouts, personalization, and Composer preview on your site.
4
+
5
+ **Full guide:** [docs/sdk-site-setup.md](../../docs/sdk-site-setup.md)
4
6
 
5
7
  ## Install
6
8
 
@@ -8,11 +10,11 @@ Official JavaScript SDK for [AmplifyUp](https://amplifyup.ai) — event tracking
8
10
  npm install @amplifyup/sdk
9
11
  ```
10
12
 
11
- ## Quick start
13
+ ## Quick start (production)
12
14
 
13
- ### React / Next.js
15
+ Production is the default. You only need a **tracking ID** from the AmplifyUp dashboard.
14
16
 
15
- Wrap your app with the provider and pass your site **tracking ID** from the AmplifyUp dashboard:
17
+ ### 1. Provider
16
18
 
17
19
  ```tsx
18
20
  import { AmplifyUpProvider } from '@amplifyup/sdk/react';
@@ -26,61 +28,86 @@ export default function RootLayout({ children }) {
26
28
  }
27
29
  ```
28
30
 
29
- ### Track events
31
+ ### 2. Page content
30
32
 
31
- ```typescript
32
- import { init, page, track, identify } from '@amplifyup/sdk';
33
+ ```tsx
34
+ import { AmplifyPageContent, ComponentContextProvider } from '@amplifyup/sdk/react';
35
+ import { Hero } from '@/components/Hero';
33
36
 
34
- await init({ trackingId: 'your-tracking-id' });
37
+ const registry = { Hero };
35
38
 
36
- page('/home');
39
+ function renderComponent(componentId, props, slots, context) {
40
+ const Component = registry[componentId];
41
+ if (!Component) return null;
42
+ return (
43
+ <ComponentContextProvider
44
+ props={props}
45
+ slots={slots}
46
+ layoutNodeId={context?.layoutNodeId}
47
+ componentId={componentId}
48
+ >
49
+ <Component {...props} />
50
+ </ComponentContextProvider>
51
+ );
52
+ }
37
53
 
38
- track('Product Viewed', { productId: '123' });
54
+ export default function Page() {
55
+ return (
56
+ <AmplifyPageContent
57
+ renderComponent={renderComponent}
58
+ fallback={<YourStaticPage />}
59
+ />
60
+ );
61
+ }
62
+ ```
39
63
 
40
- identify('user-123', { email: 'user@example.com' });
64
+ ### 3. Components use Field / Slot
65
+
66
+ ```tsx
67
+ import { Field, Slot } from '@amplifyup/sdk/react';
68
+
69
+ export function Hero() {
70
+ return (
71
+ <section>
72
+ <h1>
73
+ <Field name="heading" fallback="Welcome" />
74
+ </h1>
75
+ </section>
76
+ );
77
+ }
41
78
  ```
42
79
 
43
- ## Composer and hybrid pages
80
+ Field names must match the component registry in AmplifyUp. Connect CMS documents in Composer, then **Deploy** so Edge can serve the page.
44
81
 
45
- If a page has its own native content **and** can be edited in Composer, use `AmplifyPageContent`:
82
+ ## Content sources
46
83
 
47
- - **Visitors** see your AmplifyUp layout, or your native content when no layout is configured yet.
48
- - **Composer preview** shows only the layout canvas — native fallback content is hidden.
84
+ | Mode | Source | When |
85
+ |------|--------|------|
86
+ | Production visitors | Edge `/v1/resolve` | Default |
87
+ | Composer preview | Orchestrator layout provider | `?preview=true` / Composer iframe |
49
88
 
50
- ```tsx
51
- import { isPreviewRequest } from '@amplifyup/sdk';
52
- import { AmplifyPageContent } from '@amplifyup/sdk/react';
89
+ The SDK maps Edge’s resolved tree into `layoutTree` and renders it. Edge already applies personalization and CMS projection — the site does not call CMS or Decision APIs for layout.
53
90
 
54
- export default async function Page({ searchParams }) {
55
- const params = await searchParams;
91
+ ## Track events
56
92
 
57
- if (isPreviewRequest(params)) {
58
- return (
59
- <Layout>
60
- <AmplifyPageContent renderComponent={renderAmplifyComponent} />
61
- </Layout>
62
- );
63
- }
93
+ ```typescript
94
+ import { init, page, track, identify } from '@amplifyup/sdk';
64
95
 
65
- return (
66
- <Layout>
67
- <AmplifyPageContent
68
- fallback={<YourNativePage />}
69
- renderComponent={renderAmplifyComponent}
70
- />
71
- </Layout>
72
- );
73
- }
96
+ await init({ trackingId: 'your-tracking-id' });
97
+
98
+ page('/home');
99
+ track('Product Viewed', { productId: '123' });
100
+ identify('user-123', { email: 'user@example.com' });
74
101
  ```
75
102
 
76
103
  ## Debugging
77
104
 
78
- In the browser console (on your site):
79
-
80
105
  ```js
81
106
  AmplifyUp.getStatus()
82
107
  ```
83
108
 
109
+ React context: `useAmplifyUp()` → `{ pageConfig, loading, error, source, meta }`.
110
+
84
111
  ## License
85
112
 
86
113
  MIT
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { P as PageConfig } from './types-Cy5aWQ5c.mjs';
3
+ import { P as PageConfig } from './types-BxpG9Gsy.mjs';
4
4
 
5
5
  interface AmplifyRendererProps {
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { P as PageConfig } from './types-Cy5aWQ5c.js';
3
+ import { P as PageConfig } from './types-BxpG9Gsy.js';
4
4
 
5
5
  interface AmplifyRendererProps {
6
6
  /**
@@ -153,17 +153,42 @@ var AWSAdapter = class {
153
153
  };
154
154
  this.sendEvent("/v1/track", eventData);
155
155
  }
156
- identify(userId, traits) {
156
+ identify(userIdOrIdentifiers, traits = {}) {
157
157
  if (!this.initialized) {
158
158
  console.warn("AmplifyUp SDK: Not initialized. Call init() first.");
159
159
  return;
160
160
  }
161
+ const isMap = userIdOrIdentifiers !== null && typeof userIdOrIdentifiers === "object" && !Array.isArray(userIdOrIdentifiers);
162
+ const identifiers = isMap ? userIdOrIdentifiers : traits.identifiers;
163
+ const userId = isMap ? void 0 : String(userIdOrIdentifiers);
161
164
  const eventData = {
162
165
  type: "identify",
163
- userId,
164
166
  anonymousId: traits.instanceId,
165
167
  traits: {
166
- ...traits
168
+ ...traits,
169
+ ...identifiers ? { identifiers } : {}
170
+ },
171
+ ...identifiers ? { identifiers } : {},
172
+ ...userId ? { userId } : {},
173
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
174
+ };
175
+ this.sendEvent("/v1/track", eventData);
176
+ }
177
+ setProfileFields(fields, context = {}) {
178
+ if (!this.initialized) {
179
+ console.warn("AmplifyUp SDK: Not initialized. Call init() first.");
180
+ return;
181
+ }
182
+ const eventData = {
183
+ type: "set_fields",
184
+ anonymousId: context.instanceId,
185
+ fields,
186
+ traits: {
187
+ instanceId: context.instanceId,
188
+ trackingId: context.trackingId
189
+ },
190
+ properties: {
191
+ trackingId: context.trackingId
167
192
  },
168
193
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
169
194
  };
@@ -225,6 +250,7 @@ function clearIds() {
225
250
  var AWS_DEV_API = "https://api-dev.amplifyup.ai";
226
251
  var AWS_PROD_API = "https://api.amplifyup.ai";
227
252
  var PROD_ORCHESTRATOR = "https://amplifyup.ai";
253
+ var PROD_EDGE = "https://edge.amplifyup.ai";
228
254
  function isAmplifyUpTarget(value) {
229
255
  return value === "local" || value === "dev" || value === "production";
230
256
  }
@@ -260,6 +286,10 @@ function localAwsApiUrl() {
260
286
  const fromEnv = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_AMPLIFYUP_API_URL : void 0;
261
287
  return (fromEnv || AWS_DEV_API).replace(/\/$/, "");
262
288
  }
289
+ function localEdgeUrl() {
290
+ const fromEnv = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_EDGE_URL : void 0;
291
+ return (fromEnv || "http://localhost:8787").replace(/\/$/, "");
292
+ }
263
293
  function resolveAmplifyUpUrls(target) {
264
294
  if (target === "local") {
265
295
  const orchestratorUrl2 = localOrchestratorUrl();
@@ -270,18 +300,22 @@ function resolveAmplifyUpUrls(target) {
270
300
  orchestratorUrl: orchestratorUrl2,
271
301
  // Events + personalize → AWS development stack
272
302
  apiGatewayUrl,
273
- decisionApiUrl: `${apiGatewayUrl}/v1/personalize`
303
+ decisionApiUrl: `${apiGatewayUrl}/v1/personalize`,
304
+ edgeUrl: localEdgeUrl()
274
305
  };
275
306
  }
276
307
  const resolvedApi = typeof process !== "undefined" && process.env.NEXT_PUBLIC_AMPLIFYUP_API_URL?.replace(/\/$/, "") || AWS_PROD_API;
277
308
  const orchestratorFromEnv = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_ORCHESTRATOR_URL : void 0;
278
309
  const isLocalOrchestrator = !!orchestratorFromEnv && (orchestratorFromEnv.includes("localhost") || orchestratorFromEnv.includes("127.0.0.1"));
279
310
  const orchestratorUrl = isLocalOrchestrator || !orchestratorFromEnv ? PROD_ORCHESTRATOR : orchestratorFromEnv.replace(/\/$/, "");
311
+ const edgeFromEnv = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_EDGE_URL?.replace(/\/$/, "") : void 0;
312
+ const edgeUrl = edgeFromEnv && !edgeFromEnv.includes("localhost") && !edgeFromEnv.includes("127.0.0.1") ? edgeFromEnv : PROD_EDGE;
280
313
  return {
281
314
  target: "production",
282
315
  apiGatewayUrl: resolvedApi,
283
316
  orchestratorUrl,
284
- decisionApiUrl: `${resolvedApi}/v1/personalize`
317
+ decisionApiUrl: `${resolvedApi}/v1/personalize`,
318
+ edgeUrl
285
319
  };
286
320
  }
287
321
 
@@ -312,6 +346,7 @@ function isComposerPreview() {
312
346
  }
313
347
 
314
348
  // src/index.ts
349
+ var SESSION_IDLE_GAP_MS = 30 * 60 * 1e3;
315
350
  function readPreviewQueryOverrides() {
316
351
  if (typeof window === "undefined") return {};
317
352
  const params = new URLSearchParams(window.location.search);
@@ -339,6 +374,8 @@ var AmplifyUpSDK = class {
339
374
  this.apiGatewayUrl = "";
340
375
  this.orchestratorUrl = "";
341
376
  this.decisionApiUrl = "";
377
+ this.edgeUrl = "";
378
+ this.siteId = "";
342
379
  this.target = "local";
343
380
  this.initialized = false;
344
381
  /** Prevents overlapping init() from React Strict Mode double-mount */
@@ -378,7 +415,9 @@ var AmplifyUpSDK = class {
378
415
  reached: /* @__PURE__ */ new Set(),
379
416
  visibleMs: 0,
380
417
  visibleSince: null,
381
- lastDwellSecondsSent: 0
418
+ lastDwellSecondsSent: 0,
419
+ /** When the tab last became hidden — used for idle-resume re-page. */
420
+ hiddenAt: null
382
421
  };
383
422
  this.adapter = adapter || new AWSAdapter();
384
423
  if (typeof window !== "undefined") {
@@ -493,7 +532,8 @@ var AmplifyUpSDK = class {
493
532
  isInitialized: () => this.isInitialized(),
494
533
  page: (url, properties) => this.page(url, properties),
495
534
  track: (eventName, properties) => this.track(eventName, properties),
496
- identify: (userId, traits) => this.identify(userId, traits)
535
+ identify: (userIdOrIdentifiers, traits) => this.identify(userIdOrIdentifiers, traits),
536
+ setProfileFields: (fields) => this.setProfileFields(fields)
497
537
  };
498
538
  }
499
539
  async performInit(config) {
@@ -508,6 +548,8 @@ var AmplifyUpSDK = class {
508
548
  this.apiGatewayUrl = urls.apiGatewayUrl;
509
549
  this.orchestratorUrl = urls.orchestratorUrl;
510
550
  this.decisionApiUrl = urls.decisionApiUrl;
551
+ this.edgeUrl = urls.edgeUrl;
552
+ this.siteId = config.siteId || "";
511
553
  const previewOverrides = readPreviewQueryOverrides();
512
554
  if (previewOverrides.orchestratorUrl) {
513
555
  this.orchestratorUrl = previewOverrides.orchestratorUrl;
@@ -516,6 +558,7 @@ var AmplifyUpSDK = class {
516
558
  this.target = "production";
517
559
  this.apiGatewayUrl = prodUrls.apiGatewayUrl;
518
560
  this.decisionApiUrl = prodUrls.decisionApiUrl;
561
+ this.edgeUrl = prodUrls.edgeUrl;
519
562
  }
520
563
  }
521
564
  this.config = {
@@ -523,7 +566,8 @@ var AmplifyUpSDK = class {
523
566
  debug: config.debug ?? true,
524
567
  // Enable debug by default
525
568
  target: this.target,
526
- dev: config.dev
569
+ dev: config.dev,
570
+ ...config.siteId ? { siteId: config.siteId } : {}
527
571
  };
528
572
  try {
529
573
  const adapterConfig = {
@@ -609,6 +653,7 @@ var AmplifyUpSDK = class {
609
653
  state.visibleMs = 0;
610
654
  state.visibleSince = document.visibilityState === "visible" ? Date.now() : null;
611
655
  state.lastDwellSecondsSent = 0;
656
+ state.hiddenAt = document.visibilityState === "hidden" ? Date.now() : null;
612
657
  let scrollRaf = null;
613
658
  const path = () => this.currentPath();
614
659
  const measureScrollPercent = () => {
@@ -651,12 +696,12 @@ var AmplifyUpSDK = class {
651
696
  state.visibleMs += Date.now() - state.visibleSince;
652
697
  state.visibleSince = Date.now();
653
698
  };
654
- const emitDwell = (force = false) => {
699
+ const emitDwell = (_force = false) => {
655
700
  if (this.isPreviewMode()) return;
656
701
  accumulateVisible();
657
702
  const seconds = Math.floor(state.visibleMs / 1e3);
658
703
  if (seconds <= 0) return;
659
- if (!force && seconds <= state.lastDwellSecondsSent) return;
704
+ if (seconds <= state.lastDwellSecondsSent) return;
660
705
  state.lastDwellSecondsSent = seconds;
661
706
  this.track("Time on Page", {
662
707
  seconds,
@@ -666,14 +711,34 @@ var AmplifyUpSDK = class {
666
711
  this._emitPageDwell = emitDwell;
667
712
  const onVisibility = () => {
668
713
  if (document.visibilityState === "visible") {
714
+ const idleMs = state.hiddenAt != null ? Date.now() - state.hiddenAt : 0;
715
+ state.hiddenAt = null;
716
+ if (idleMs >= SESSION_IDLE_GAP_MS) {
717
+ this.resetEngagementQuiet();
718
+ const resumePath = path();
719
+ this.page(resumePath, { resumed: true });
720
+ this.lastTrackedPath = resumePath;
721
+ if (this.config?.debug) {
722
+ console.log(
723
+ "[AmplifyUp SDK] Idle resume page view",
724
+ resumePath,
725
+ `(hidden ${Math.round(idleMs / 6e4)}m)`
726
+ );
727
+ }
728
+ }
669
729
  state.visibleSince = Date.now();
670
730
  } else {
671
731
  emitDwell(true);
732
+ state.hiddenAt = Date.now();
672
733
  state.visibleSince = null;
673
734
  }
674
735
  };
675
736
  const onPageHide = () => {
676
737
  emitDwell(true);
738
+ if (state.hiddenAt == null) {
739
+ state.hiddenAt = Date.now();
740
+ }
741
+ state.visibleSince = null;
677
742
  };
678
743
  window.addEventListener("scroll", onScroll, { passive: true });
679
744
  document.addEventListener("visibilitychange", onVisibility);
@@ -702,13 +767,19 @@ var AmplifyUpSDK = class {
702
767
  console.log("[AmplifyUp SDK] Page engagement tracking enabled");
703
768
  }
704
769
  }
770
+ /** Flush dwell then reset — used on SPA route changes. */
705
771
  resetEngagementForNewPage() {
706
772
  const emit = this._emitPageDwell;
707
773
  emit?.(true);
774
+ this.resetEngagementQuiet();
775
+ }
776
+ /** Reset dwell/scroll state without emitting (idle resume already flushed on hide). */
777
+ resetEngagementQuiet() {
708
778
  const state = this.pageEngagementState;
709
779
  state.reached = /* @__PURE__ */ new Set();
710
780
  state.visibleMs = 0;
711
781
  state.lastDwellSecondsSent = 0;
782
+ state.hiddenAt = null;
712
783
  state.visibleSince = typeof document !== "undefined" && document.visibilityState === "visible" ? Date.now() : null;
713
784
  this.componentViewSeen.clear();
714
785
  }
@@ -894,12 +965,17 @@ var AmplifyUpSDK = class {
894
965
  });
895
966
  }
896
967
  /**
897
- * Identify user (when they sign up or log in)
968
+ * Identify a visitor.
969
+ * - identify('user_123') — bare string; server maps via org default type
970
+ * - identify({ email: '...', crm_id: '...' }) — explicit types
971
+ * - identify('user_123', traits) — legacy form
898
972
  */
899
- identify(userId, traits) {
973
+ identify(userIdOrIdentifiers, traits) {
900
974
  if (this.isPreviewMode()) {
901
975
  if (this.config?.debug) {
902
- console.log("AmplifyUp SDK: Identify tracking disabled in preview mode", { userId });
976
+ console.log("AmplifyUp SDK: Identify tracking disabled in preview mode", {
977
+ userIdOrIdentifiers
978
+ });
903
979
  }
904
980
  return;
905
981
  }
@@ -915,10 +991,45 @@ var AmplifyUpSDK = class {
915
991
  trackingId: this.config?.trackingId,
916
992
  ...traits
917
993
  };
918
- this.adapter.identify(userId, identifyTraits);
919
- setUserId(userId);
994
+ this.adapter.identify(userIdOrIdentifiers, identifyTraits);
995
+ if (typeof userIdOrIdentifiers === "string") {
996
+ setUserId(userIdOrIdentifiers);
997
+ }
920
998
  if (this.config?.debug) {
921
- console.log("AmplifyUp SDK: User identified", { userId, instanceId });
999
+ console.log("AmplifyUp SDK: User identified", {
1000
+ userIdOrIdentifiers,
1001
+ instanceId
1002
+ });
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Set profile fields (partial update). Omitted keys are left alone;
1007
+ * an explicit empty value clears the field.
1008
+ */
1009
+ setProfileFields(fields) {
1010
+ if (this.isPreviewMode()) {
1011
+ if (this.config?.debug) {
1012
+ console.log("AmplifyUp SDK: setProfileFields disabled in preview mode", {
1013
+ fields
1014
+ });
1015
+ }
1016
+ return;
1017
+ }
1018
+ if (!this.isInitialized()) {
1019
+ if (this.config?.debug) {
1020
+ console.warn("AmplifyUp SDK: Not initialized. setProfileFields not called.");
1021
+ }
1022
+ return;
1023
+ }
1024
+ const instanceId = getInstanceId();
1025
+ if (typeof this.adapter.setProfileFields === "function") {
1026
+ this.adapter.setProfileFields(fields, {
1027
+ instanceId: instanceId || void 0,
1028
+ trackingId: this.config?.trackingId
1029
+ });
1030
+ }
1031
+ if (this.config?.debug) {
1032
+ console.log("AmplifyUp SDK: Profile fields set", { fields, instanceId });
922
1033
  }
923
1034
  }
924
1035
  /**
@@ -1093,6 +1204,18 @@ Available Console Commands:
1093
1204
  getDecisionApiUrl() {
1094
1205
  return this.decisionApiUrl || void 0;
1095
1206
  }
1207
+ /**
1208
+ * Get Edge Worker URL (production content resolve)
1209
+ */
1210
+ getEdgeUrl() {
1211
+ return this.edgeUrl || void 0;
1212
+ }
1213
+ /**
1214
+ * Optional site id from config (Edge can resolve via trackingId alone).
1215
+ */
1216
+ getSiteId() {
1217
+ return this.siteId || this.config?.siteId || void 0;
1218
+ }
1096
1219
  /**
1097
1220
  * Build API URL for fetching page config from the Layout Provider
1098
1221
  */
@@ -2398,6 +2521,7 @@ function AmplifyRenderer({
2398
2521
  const pageConfig = serverPageConfig || clientHook.pageConfig || null;
2399
2522
  const loading = serverPageConfig ? false : clientHook.loading;
2400
2523
  const error = serverPageConfig ? null : clientHook.error;
2524
+ const contentSource = clientHook.source;
2401
2525
  const [variantCache, setVariantCache] = react.useState({});
2402
2526
  react.useEffect(() => {
2403
2527
  if (typeof window !== "undefined") {
@@ -2539,6 +2663,9 @@ function AmplifyRenderer({
2539
2663
  }, [pageConfig]);
2540
2664
  react.useEffect(() => {
2541
2665
  if (!pageConfig?.layoutTree || !pageConfig.layoutId) return;
2666
+ const roots = pageConfig.layoutTree;
2667
+ const fromEdge = contentSource === "edge" || roots.some((n) => n._contentSource === "edge");
2668
+ if (fromEdge) return;
2542
2669
  const sdk = getSDK();
2543
2670
  const instanceId = sdk.getInstanceId();
2544
2671
  const decisionApiUrl = sdk.getDecisionApiUrl();
@@ -2633,7 +2760,7 @@ function AmplifyRenderer({
2633
2760
  setVariantCache(newCache);
2634
2761
  };
2635
2762
  fetchVariants();
2636
- }, [pageConfig?.layoutTree, pageConfig?.layoutId, pageConfig?.route]);
2763
+ }, [pageConfig?.layoutTree, pageConfig?.layoutId, pageConfig?.route, contentSource]);
2637
2764
  if (loading) {
2638
2765
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loadingComponent || /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-screen bg-gray-50", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "container mx-auto px-4 py-16", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse", children: [
2639
2766
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-8 bg-gray-200 rounded w-1/4 mb-4" }),