@delmaredigital/payload-experiments 0.2.0-rc.7 → 0.3.0-rc.4

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.
Files changed (336) hide show
  1. package/README.md +202 -26
  2. package/dist/admin/PreviewLinks.d.ts +2 -1
  3. package/dist/admin/PreviewLinks.js +50 -21
  4. package/dist/admin/TestsNavLink.d.ts +3 -1
  5. package/dist/admin/TestsNavLink.js +2 -2
  6. package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
  7. package/dist/admin/actions/ConfirmDialog.js +37 -82
  8. package/dist/admin/actions/DecideDialog.d.ts +3 -1
  9. package/dist/admin/actions/DecideDialog.js +30 -13
  10. package/dist/admin/actions/MakeContentResult.d.ts +13 -2
  11. package/dist/admin/actions/MakeContentResult.js +115 -16
  12. package/dist/admin/actions/actions.model.d.ts +2 -4
  13. package/dist/admin/actions/actions.model.js +2 -3
  14. package/dist/admin/allocation.model.js +3 -5
  15. package/dist/admin/api.d.ts +10 -4
  16. package/dist/admin/api.js +11 -7
  17. package/dist/admin/armSelect/ArmSelectField.js +14 -22
  18. package/dist/admin/arms/arms.model.d.ts +1 -21
  19. package/dist/admin/arms/arms.model.js +1 -153
  20. package/dist/admin/copy/actions.d.ts +39 -3
  21. package/dist/admin/copy/actions.js +40 -4
  22. package/dist/admin/copy/armSelect.js +5 -5
  23. package/dist/admin/copy/arms.d.ts +1 -6
  24. package/dist/admin/copy/arms.js +1 -16
  25. package/dist/admin/copy/audience.d.ts +2 -1
  26. package/dist/admin/copy/audience.js +3 -2
  27. package/dist/admin/copy/common.d.ts +13 -6
  28. package/dist/admin/copy/common.js +14 -8
  29. package/dist/admin/copy/goals.d.ts +21 -6
  30. package/dist/admin/copy/goals.js +27 -6
  31. package/dist/admin/copy/readiness.d.ts +22 -0
  32. package/dist/admin/copy/readiness.js +21 -0
  33. package/dist/admin/copy/recommendation.d.ts +0 -3
  34. package/dist/admin/copy/recommendation.js +0 -3
  35. package/dist/admin/copy/results.d.ts +43 -2
  36. package/dist/admin/copy/results.js +44 -3
  37. package/dist/admin/copy/review.d.ts +2 -2
  38. package/dist/admin/copy/review.js +2 -2
  39. package/dist/admin/copy/setting.d.ts +2 -0
  40. package/dist/admin/copy/setting.js +2 -0
  41. package/dist/admin/copy/surface.d.ts +7 -5
  42. package/dist/admin/copy/surface.js +9 -5
  43. package/dist/admin/copy/testPage.d.ts +10 -0
  44. package/dist/admin/copy/testPage.js +10 -0
  45. package/dist/admin/format/date.d.ts +2 -0
  46. package/dist/admin/format/date.js +10 -0
  47. package/dist/admin/paths.d.ts +2 -1
  48. package/dist/admin/paths.js +2 -1
  49. package/dist/admin/previewLinks.model.d.ts +4 -2
  50. package/dist/admin/previewLinks.model.js +8 -7
  51. package/dist/admin/results/CiChart.js +11 -21
  52. package/dist/admin/results/DataQualityPanel.d.ts +4 -2
  53. package/dist/admin/results/DataQualityPanel.js +30 -17
  54. package/dist/admin/results/DataTable.d.ts +16 -0
  55. package/dist/admin/results/DataTable.js +152 -0
  56. package/dist/admin/results/StatTile.d.ts +7 -0
  57. package/dist/admin/results/StatTile.js +22 -0
  58. package/dist/admin/results/VerdictHero.d.ts +13 -0
  59. package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
  60. package/dist/admin/results/legacy.d.ts +16 -0
  61. package/dist/admin/results/legacy.js +27 -0
  62. package/dist/admin/results/releasePages.model.d.ts +17 -0
  63. package/dist/admin/results/releasePages.model.js +10 -0
  64. package/dist/admin/results/results.css +64 -0
  65. package/dist/admin/results/results.model.d.ts +11 -7
  66. package/dist/admin/results/results.model.js +24 -7
  67. package/dist/admin/results/snapshot.d.ts +14 -0
  68. package/dist/admin/results/snapshot.js +3 -0
  69. package/dist/admin/surface/ResultsRoute.d.ts +1 -1
  70. package/dist/admin/surface/ResultsRoute.js +59 -34
  71. package/dist/admin/surface/SurfaceShell.d.ts +11 -2
  72. package/dist/admin/surface/SurfaceShell.js +49 -14
  73. package/dist/admin/surface/TestsHome.d.ts +6 -1
  74. package/dist/admin/surface/TestsHome.js +182 -114
  75. package/dist/admin/surface/TestsSurface.js +56 -16
  76. package/dist/admin/surface/goals/GoalForm.js +341 -308
  77. package/dist/admin/surface/goals/GoalsList.js +123 -89
  78. package/dist/admin/surface/goals/data.d.ts +8 -2
  79. package/dist/admin/surface/goals/data.js +24 -16
  80. package/dist/admin/surface/goals/goals.css +10 -13
  81. package/dist/admin/surface/home.model.d.ts +8 -8
  82. package/dist/admin/surface/home.model.js +23 -12
  83. package/dist/admin/surface/newTest/NewTest.js +9 -9
  84. package/dist/admin/surface/pageBlock/parts.js +31 -26
  85. package/dist/admin/surface/readiness/ActionDialogs.js +47 -19
  86. package/dist/admin/surface/readiness/Banner.d.ts +3 -0
  87. package/dist/admin/surface/readiness/Banner.js +58 -33
  88. package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
  89. package/dist/admin/surface/readiness/SaveStatus.js +6 -3
  90. package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
  91. package/dist/admin/surface/readiness/readiness.model.js +25 -9
  92. package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
  93. package/dist/admin/surface/readiness/useTransition.js +2 -1
  94. package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
  95. package/dist/admin/surface/results/ReleasePages.js +116 -0
  96. package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
  97. package/dist/admin/surface/results/ResultsBody.js +277 -105
  98. package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
  99. package/dist/admin/surface/results/ResultsHeader.js +177 -0
  100. package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
  101. package/dist/admin/surface/results/recommendation.model.js +12 -9
  102. package/dist/admin/surface/results/useResults.d.ts +21 -3
  103. package/dist/admin/surface/results/useResults.js +2 -1
  104. package/dist/admin/surface/setting/parts.js +75 -88
  105. package/dist/admin/surface/setting/setting.css +3 -15
  106. package/dist/admin/surface/surface.css +61 -47
  107. package/dist/admin/surface/test/AdvancedSection.js +55 -48
  108. package/dist/admin/surface/test/GoalSection.js +46 -47
  109. package/dist/admin/surface/test/LengthSection.js +46 -34
  110. package/dist/admin/surface/test/Rail.d.ts +4 -0
  111. package/dist/admin/surface/test/Rail.js +83 -0
  112. package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
  113. package/dist/admin/surface/test/ReadinessBar.js +33 -0
  114. package/dist/admin/surface/test/Section.d.ts +1 -1
  115. package/dist/admin/surface/test/Section.js +36 -16
  116. package/dist/admin/surface/test/TargetingSection.js +168 -142
  117. package/dist/admin/surface/test/TestPage.d.ts +2 -1
  118. package/dist/admin/surface/test/TestPage.js +146 -68
  119. package/dist/admin/surface/test/VersionsSection.js +151 -120
  120. package/dist/admin/surface/test/fields.d.ts +11 -4
  121. package/dist/admin/surface/test/fields.js +84 -85
  122. package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
  123. package/dist/admin/surface/test/jumpToSection.js +10 -0
  124. package/dist/admin/surface/test/pageParts.js +9 -3
  125. package/dist/admin/surface/test/testPage.css +81 -14
  126. package/dist/admin/surface/test/testPage.model.d.ts +2 -3
  127. package/dist/admin/surface/test/testPage.model.js +1 -21
  128. package/dist/admin/surface/useTestDocument.js +12 -5
  129. package/dist/admin/targeting/targeting.model.d.ts +1 -3
  130. package/dist/admin/targeting/targeting.model.js +3 -8
  131. package/dist/admin/theme.css +115 -36
  132. package/dist/admin/theme.d.ts +38 -0
  133. package/dist/admin/theme.js +38 -1
  134. package/dist/admin/useApi.d.ts +17 -2
  135. package/dist/admin/useLayerUsage.js +14 -23
  136. package/dist/admin/views/ExperimentsRootView.js +12 -2
  137. package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
  138. package/dist/admin/views/TestsNavLinkServer.js +43 -0
  139. package/dist/arms/copy.d.ts +6 -0
  140. package/dist/arms/copy.js +16 -0
  141. package/dist/arms/model.d.ts +23 -0
  142. package/dist/arms/model.js +173 -0
  143. package/dist/collections/experiments/config.js +25 -0
  144. package/dist/collections/experiments/lockHook.js +3 -1
  145. package/dist/collections/identities.js +6 -1
  146. package/dist/collections/metrics.js +7 -0
  147. package/dist/collections/metricsGuards.d.ts +9 -0
  148. package/dist/collections/metricsGuards.js +33 -0
  149. package/dist/contentDone.d.ts +4 -0
  150. package/dist/contentDone.js +5 -0
  151. package/dist/db/appends.d.ts +6 -2
  152. package/dist/db/appends.js +10 -4
  153. package/dist/db/identities.d.ts +8 -7
  154. package/dist/db/identities.js +9 -6
  155. package/dist/db/locks.d.ts +6 -0
  156. package/dist/db/locks.js +13 -1
  157. package/dist/db/metricUsage.d.ts +19 -0
  158. package/dist/db/metricUsage.js +54 -0
  159. package/dist/db/names.d.ts +5 -0
  160. package/dist/db/names.js +5 -0
  161. package/dist/db/pageOccupancy.d.ts +12 -0
  162. package/dist/db/pageOccupancy.js +45 -0
  163. package/dist/db/publishedContent.d.ts +10 -0
  164. package/dist/db/publishedContent.js +43 -0
  165. package/dist/db/results.d.ts +2 -0
  166. package/dist/db/results.js +6 -2
  167. package/dist/db/snapshots.d.ts +18 -6
  168. package/dist/db/snapshots.js +25 -8
  169. package/dist/db/sweep.js +11 -6
  170. package/dist/endpoints/index.js +19 -1
  171. package/dist/endpoints/results.js +5 -0
  172. package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
  173. package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
  174. package/dist/endpoints/summary.js +20 -2
  175. package/dist/endpoints/transitions.d.ts +15 -0
  176. package/dist/endpoints/transitions.js +36 -0
  177. package/dist/errors.d.ts +8 -0
  178. package/dist/errors.js +28 -0
  179. package/dist/exports/adminRsc.d.ts +1 -0
  180. package/dist/exports/adminRsc.js +1 -0
  181. package/dist/exports/index.d.ts +2 -1
  182. package/dist/exports/index.js +1 -0
  183. package/dist/exports/puck.d.ts +2 -1
  184. package/dist/exports/puck.js +1 -1
  185. package/dist/exports/puckEditor.d.ts +4 -1
  186. package/dist/exports/puckEditor.js +3 -1
  187. package/dist/exports/rsc.d.ts +6 -1
  188. package/dist/exports/rsc.js +2 -0
  189. package/dist/hooks/pageGuard.d.ts +5 -0
  190. package/dist/hooks/pageGuard.js +139 -0
  191. package/dist/lib/fetchAllDocs.d.ts +16 -0
  192. package/dist/lib/fetchAllDocs.js +56 -0
  193. package/dist/log.js +6 -3
  194. package/dist/manifest/compile.d.ts +5 -0
  195. package/dist/manifest/compile.js +5 -1
  196. package/dist/options.js +16 -0
  197. package/dist/plugin.js +30 -2
  198. package/dist/puck/ExperimentPicker.js +47 -16
  199. package/dist/puck/ExperimentSlot.d.ts +51 -1
  200. package/dist/puck/ExperimentSlot.js +23 -12
  201. package/dist/puck/copy.d.ts +6 -5
  202. package/dist/puck/copy.js +6 -5
  203. package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
  204. package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
  205. package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
  206. package/dist/puck/editor/ResultsReadout.d.ts +6 -0
  207. package/dist/puck/editor/ResultsReadout.js +99 -0
  208. package/dist/puck/editor/VersionBuilder.d.ts +34 -0
  209. package/dist/puck/editor/VersionBuilder.js +317 -0
  210. package/dist/puck/editor/armWrites.d.ts +7 -0
  211. package/dist/puck/editor/armWrites.js +71 -0
  212. package/dist/puck/editor/armsCache.d.ts +14 -0
  213. package/dist/puck/editor/armsCache.js +78 -0
  214. package/dist/puck/editor/copy.d.ts +33 -1
  215. package/dist/puck/editor/copy.js +45 -8
  216. package/dist/puck/editor/createTest.d.ts +1 -1
  217. package/dist/puck/editor/createTest.js +25 -14
  218. package/dist/puck/editor/editor.chrome.css +121 -0
  219. package/dist/puck/editor/editor.css +288 -204
  220. package/dist/puck/editor/editor.model.d.ts +4 -1
  221. package/dist/puck/editor/editor.model.js +10 -7
  222. package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
  223. package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
  224. package/dist/puck/experimentSlotField.d.ts +2 -0
  225. package/dist/puck/experimentSlotField.js +11 -3
  226. package/dist/puck/hostContext.d.ts +10 -0
  227. package/dist/puck/hostContext.js +12 -0
  228. package/dist/puck/routes.d.ts +12 -0
  229. package/dist/puck/routes.js +11 -0
  230. package/dist/puck/slotConfig.d.ts +1 -1
  231. package/dist/puck/slotConfig.js +2 -2
  232. package/dist/results/aggregate.js +4 -3
  233. package/dist/results/analyse.d.ts +4 -1
  234. package/dist/results/analyse.js +27 -11
  235. package/dist/results/cuped.js +3 -2
  236. package/dist/results/identityCredit.d.ts +15 -7
  237. package/dist/results/identityCredit.js +32 -33
  238. package/dist/results/rolloutUnits.js +2 -1
  239. package/dist/results/snapshot.js +8 -2
  240. package/dist/results/srm.js +2 -1
  241. package/dist/sanitizeReadinessDetail.d.ts +7 -0
  242. package/dist/sanitizeReadinessDetail.js +63 -0
  243. package/dist/server/linkIdentity.d.ts +10 -1
  244. package/dist/server/linkIdentity.js +17 -15
  245. package/dist/server/metadata.d.ts +5 -0
  246. package/dist/server/metadata.js +18 -3
  247. package/dist/server/normaliseContentRelationships.d.ts +7 -0
  248. package/dist/server/normaliseContentRelationships.js +62 -0
  249. package/dist/server/recordContentChange.d.ts +1 -1
  250. package/dist/server/recordContentChange.js +17 -8
  251. package/dist/server/runResults.d.ts +3 -2
  252. package/dist/server/runResults.js +157 -35
  253. package/dist/targeting/values.d.ts +4 -0
  254. package/dist/targeting/values.js +10 -0
  255. package/dist/transitions/addMetric.js +8 -0
  256. package/dist/transitions/archive.js +2 -3
  257. package/dist/transitions/checkStart.d.ts +10 -3
  258. package/dist/transitions/checkStart.js +25 -0
  259. package/dist/transitions/contentFields.d.ts +7 -0
  260. package/dist/transitions/contentFields.js +20 -0
  261. package/dist/transitions/decide.js +22 -2
  262. package/dist/transitions/dismissContentEntry.d.ts +8 -0
  263. package/dist/transitions/dismissContentEntry.js +45 -0
  264. package/dist/transitions/makeContent.d.ts +5 -2
  265. package/dist/transitions/makeContent.js +217 -118
  266. package/dist/transitions/phaseRecord.d.ts +8 -2
  267. package/dist/transitions/placements.d.ts +3 -6
  268. package/dist/transitions/placements.js +12 -1
  269. package/dist/transitions/preflightData.d.ts +9 -1
  270. package/dist/transitions/preflightData.js +138 -20
  271. package/dist/transitions/releasePages.d.ts +8 -0
  272. package/dist/transitions/releasePages.js +26 -0
  273. package/dist/transitions/restoreContentEntry.d.ts +8 -0
  274. package/dist/transitions/restoreContentEntry.js +47 -0
  275. package/dist/transitions/start.js +62 -7
  276. package/dist/types/contracts.d.ts +24 -4
  277. package/dist/types/endpoints.d.ts +34 -3
  278. package/dist/types/index.d.ts +1 -0
  279. package/dist/types/index.js +1 -0
  280. package/dist/types/reasons.d.ts +3 -0
  281. package/dist/types/reasons.js +1 -0
  282. package/dist/types/snapshot.d.ts +6 -1
  283. package/dist/ui/Banner.d.ts +2 -0
  284. package/dist/ui/Banner.js +2 -0
  285. package/dist/ui/Breadcrumb.d.ts +7 -0
  286. package/dist/ui/Breadcrumb.js +25 -0
  287. package/dist/ui/Button.d.ts +15 -0
  288. package/dist/ui/Button.js +32 -0
  289. package/dist/ui/Card.d.ts +12 -0
  290. package/dist/ui/Card.js +40 -0
  291. package/dist/ui/Checkbox.d.ts +2 -0
  292. package/dist/ui/Checkbox.js +2 -0
  293. package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
  294. package/dist/{admin/components → ui}/ChipsInput.js +20 -12
  295. package/dist/ui/Dialog.d.ts +13 -0
  296. package/dist/ui/Dialog.js +85 -0
  297. package/dist/ui/EmptyState.d.ts +2 -0
  298. package/dist/ui/EmptyState.js +2 -0
  299. package/dist/ui/Feedback.d.ts +24 -0
  300. package/dist/ui/Feedback.js +40 -0
  301. package/dist/ui/Inputs.d.ts +34 -0
  302. package/dist/ui/Inputs.js +225 -0
  303. package/dist/ui/NumberInput.d.ts +2 -0
  304. package/dist/ui/NumberInput.js +2 -0
  305. package/dist/ui/Range.d.ts +2 -0
  306. package/dist/ui/Range.js +2 -0
  307. package/dist/ui/SearchInput.d.ts +2 -0
  308. package/dist/ui/SearchInput.js +2 -0
  309. package/dist/ui/SectionCard.d.ts +2 -0
  310. package/dist/ui/SectionCard.js +2 -0
  311. package/dist/ui/Select.d.ts +2 -0
  312. package/dist/ui/Select.js +2 -0
  313. package/dist/ui/StatusChip.d.ts +2 -0
  314. package/dist/ui/StatusChip.js +2 -0
  315. package/dist/ui/Tabs.d.ts +16 -0
  316. package/dist/ui/Tabs.js +61 -0
  317. package/dist/ui/TextInput.d.ts +2 -0
  318. package/dist/ui/TextInput.js +2 -0
  319. package/dist/ui/Textarea.d.ts +2 -0
  320. package/dist/ui/Textarea.js +2 -0
  321. package/dist/ui/Toggle.d.ts +2 -0
  322. package/dist/ui/Toggle.js +2 -0
  323. package/dist/ui/accent.d.ts +40 -0
  324. package/dist/ui/accent.js +139 -0
  325. package/dist/ui/index.d.ts +9 -0
  326. package/dist/ui/index.js +9 -0
  327. package/dist/ui/ui.css +92 -0
  328. package/package.json +5 -2
  329. package/dist/admin/components/chipsInput.css +0 -11
  330. package/dist/admin/results/ArmTable.d.ts +0 -8
  331. package/dist/admin/results/ArmTable.js +0 -113
  332. package/dist/admin/results/VerdictCard.d.ts +0 -9
  333. package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
  334. package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
  335. package/dist/admin/surface/test/TestHeader.d.ts +0 -4
  336. package/dist/admin/surface/test/TestHeader.js +0 -70
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @delmaredigital/payload-experiments
2
2
 
3
+ > ⚠️ Upgrading to 0.3? For 0.3.0-rc.1, every `linkIdentity` call must supply `verified`; it now returns an outcome. Run `payload migrate:create` for the nullable `xp_identities.verified_at` column and `user_id` index, plus the release-pages columns listed in the CHANGELOG (`decision_pages_released_at` / `decision_pages_released_by` on `xp_experiments` and their `version_` counterparts on `_xp_experiments_v`). Existing identity links become unverified; see the CHANGELOG for optional backfill.
4
+
3
5
  > ⚠️ Upgrading to 0.2? After upgrading, run `payload generate:importmap`: the Tests surface is registered through admin.dependencies, and without it the Tests routes show a 'not installed' message instead of the Tests screen. In 0.2.0-rc.2, the Tests server views moved from `/rsc` to `/admin/rsc`, so the import map must be regenerated when upgrading from rc.1 too. The old experiments edit-form components are gone from `/admin/client`, and the `xp-experiments` admin list and edit views now redirect to the Tests surface.
4
6
 
5
7
  A/B testing for Payload CMS and Next.js, with results inside the admin and data
@@ -87,7 +89,7 @@ permission is granted only for `true`. Optional `access.admin` and
87
89
  `access.readResults` default to `manage`; `access.readIdentities` defaults to
88
90
  `admin`. Use `admin` for code experiments and changing the analysis method.
89
91
  Other plugin options are `flagPoints`, `metricSources`, `eventSources`,
90
- `attributeKeys`, `puck` and `events`:
92
+ `attributeKeys`, `pages`, `puck` and `events`:
91
93
 
92
94
  - `attributeKeys` declares targeting attributes as `{ type, values? }`, with
93
95
  `type` one of `string`, `number`, `boolean`; optional string `values` supplies
@@ -261,29 +263,93 @@ Other server integrations:
261
263
  outside a Next request; a user-only server conversion can supply `userId`.
262
264
  Record revenue through trusted server code or a metric source, never a client
263
265
  beacon.
264
- - `linkIdentity(serverRuntime, { headers, userId })` belongs on **every**
265
- user-creation and sign-in path. It links the signed anonymous cookie to the
266
- opaque user id using insert-ignore on its own connection and never throws into
267
- sign-in. For visitor experiments using user-keyed outcomes, credit goes to the
268
- earliest-exposed linked visitor; cross-group links are reported separately.
266
+ - `linkIdentity(serverRuntime, { headers, userId, verified })` belongs on every
267
+ user-creation and proving sign-in path; see Identity linking below.
269
268
  - `eraseUser(payload, { userId })` deletes the user's identities and associated
270
269
  events transactionally and returns `{ identities, events }` counts. Call it
271
270
  from the host's authorized user-erasure flow.
272
271
  - `recordContentChange({ doc, previousDoc, req, collection })` can be called
273
272
  from a host collection's `afterChange` for content tagged with the arm-select
274
273
  field; for a collection with drafts, also add `captureContentBaseline` to its
275
- `beforeChange` hooks so the previously published content can be compared. Supply that hook's transaction request. Puck collections get the hook
274
+ `beforeChange` hooks so the previously published content can be compared.
275
+ Collections without drafts can also add `captureContentBaseline` for an exact
276
+ raw comparison; without it, the helper normalises relationships to ids. No host
277
+ change is required for collections without drafts. Supply that hook's transaction request. Puck collections get the hook
276
278
  automatically when enabled. Markers compare published content; drafts and
277
279
  autosaves do not add markers (collections without drafts record on save).
278
280
  Neither the helper nor the installed hooks ever throw into your save: a
279
281
  failure is logged and the content save proceeds.
280
282
  - `experimentMetadata(serverRuntime, path)` returns metadata to merge into the
281
283
  page's Next metadata: live variant targets get noindex and an Original
282
- canonical; archived losing paths point canonically to the winner.
284
+ canonical; archived losing paths point canonically to the winner. It reads
285
+ the signed request context via `headers()`, so it decides from the URL the
286
+ visitor requested, not the rewritten route, and the Original's URL is never
287
+ noindex. While a test is live the Original's URL also gets a canonical to
288
+ itself, even when it is served with a variant's route: merge this result after
289
+ your own metadata so its `alternates.canonical` wins over one derived from the
290
+ rendered route. Call it only from a dynamic `generateMetadata`, never inside
291
+ `'use cache'`; `path` is the fallback when the proxy did not run.
283
292
  `experimentSitemapFilter(path, manifest)` is a synchronous build-time filter:
284
293
  pass a privately server-loaded manifest and keep paths for which it returns
285
294
  `true`, excluding live variants and archived losing paths.
286
295
 
296
+ ## Identity linking
297
+
298
+ Call `linkIdentity(serverRuntime, { headers, userId, verified })` on every
299
+ user-creation and proving sign-in path. It links the signed anonymous cookie
300
+ to an opaque user id on its own connection and never throws into sign-in.
301
+ `verified` is required. Pass `true` only when **this request proves, in this
302
+ browser, that the person owns the account**: a magic-link or one-time-code
303
+ callback, password sign-in, or OAuth callback. It does not mean "a session
304
+ exists" or "the account's email is verified". Registration alone uses `false`.
305
+
306
+ ```ts
307
+ // Registration, before proof of account ownership:
308
+ await linkIdentity(serverRuntime, { headers, userId, verified: false })
309
+ // After successful magic-link/code verification in this browser:
310
+ await linkIdentity(serverRuntime, { headers, userId, verified: true })
311
+ // After a successful password sign-in (or OAuth callback):
312
+ await linkIdentity(serverRuntime, { headers, userId, verified: true })
313
+ ```
314
+
315
+ An auth "after" hook that fires whenever a new session cookie is set also
316
+ fires on routine session refreshes; pass `verified: true` only from proving
317
+ sign-in paths, never from a refresh.
318
+
319
+ The result is `'linked'` for an inserted link, `'verified'` for an upgraded
320
+ unverified link, `'skipped'` for no consent, no valid unit cookie or an existing
321
+ link with nothing to change, and `'failed'` for an error (safely logged).
322
+ First link per browser wins; a different account never replaces it.
323
+
324
+ For visitor experiments with user-keyed events, verified links are always
325
+ eligible. Unverified links are eligible only before the first verification
326
+ across all of that user's browsers, or indefinitely if nobody ever verifies.
327
+ Each event goes to the earliest exposed eligible visitor. Eligible links
328
+ spanning groups are reported as a diagnostic. User-keyed CUPED covariates use
329
+ the same eligibility rule at each visitor's first exposure and credit one
330
+ aggregate value to the earliest eligible visitor per user.
331
+
332
+ Accepted limits:
333
+
334
+ - Cross-device: register in A, verify on B, keep using A. A's events after B's
335
+ proof are not credited until a proving sign-in on A upgrades A. Events before
336
+ B's proof, including the registration funnel, remain credited. Once A is
337
+ upgraded, its link is verified and eligible for all its events, so the next
338
+ results run also credits A's events from between B's proof and the upgrade.
339
+ - Password set before verification: a later password sign-in is proof the
340
+ plugin accepts. Whether unverified registration may set a password is a host
341
+ authentication concern.
342
+ - Shared device: a different user's verified sign-in never relinks an already
343
+ linked browser and returns `'skipped'`; that user's events get no credit
344
+ through that browser.
345
+ - If someone registers another person's email from their own browser and that
346
+ person never signs in, a later user-keyed event the host records against that
347
+ account (for example an offline gift) is credited to the registering browser.
348
+ It needs a deliberate registration of someone else's email plus an event
349
+ matched to that account, and it moves one unit's value, never assignment.
350
+ There is no time window, so recurring gifts from never-signed-in donors keep
351
+ counting.
352
+
287
353
  ## Results
288
354
 
289
355
  The host owns scheduling and authentication. Call
@@ -368,7 +434,8 @@ a user-keyed source. Visitor experiments can use a user-keyed source through
368
434
  linked identities.
369
435
 
370
436
  Optional `covariate({ units: [{ unitId, firstExposureTs }], lookbackDays })`
371
- returns `[{ unitId, value }]` for pre-exposure CUPED adjustment (the plugin asks for a 14-day lookback). Percentile
437
+ returns `[{ unitId, value }]`, one aggregate value per unit for pre-exposure
438
+ CUPED adjustment (the plugin asks for a 14-day lookback). Percentile
372
439
  revenue capping reuses `query` over the 90 days before Start; the resolved cap
373
440
  is frozen for the round. Capping affects decision statistics, while results
374
441
  also show uncapped totals. Count and binomial metrics are never capped.
@@ -389,16 +456,61 @@ are recorded with `trackConversion`. Beacon conversions for a non-writable name
389
456
  are dropped silently. Only listed names become `events:<name>` sources. They
390
457
  support visitor and user units and do not represent counts or revenue in v1.
391
458
 
459
+ ### Whole-page options
460
+
461
+ | Option | Purpose |
462
+ | --- | --- |
463
+ | `pages.collections` | Optional array of `{ slug, pathOf, findByPath }`. `pathOf(doc)` derives a public path from the slug (used to protect live test paths); `findByPath(path, req)` resolves a published page to `{ id, published: true }`, or returns `null`. Start and readiness check every non-holdout version, including the Original. Registered collections must exist when this plugin runs. Localized publication status and field-level `beforeChange` hooks that rewrite paths after the collection guard are unsupported; derive paths from fields finalized before collection hooks. Without this option, readiness warns that pages are unchecked and Start keeps its existing behaviour. |
464
+
465
+ Do not swallow errors from nested updates of guarded page collections inside host hooks: a swallowed error leaves the guard's operation frame in place for the rest of the request because Payload's Local API does not call `afterError`.
466
+
467
+ For a collection with drafts enabled, use a published-only slug lookup with the
468
+ manager's request and `overrideAccess: false`:
469
+
470
+ ```ts
471
+ pages: {
472
+ collections: [{
473
+ slug: 'pages',
474
+ pathOf: doc => typeof doc.slug === 'string' ? `/${doc.slug}` : null,
475
+ findByPath: async (path, req) => {
476
+ const { docs } = await req.payload.find({
477
+ collection: 'pages', req, overrideAccess: false, draft: false, depth: 0, limit: 1,
478
+ where: { and: [
479
+ { slug: { equals: path.slice(1) } },
480
+ { _status: { equals: 'published' } },
481
+ ] },
482
+ })
483
+ return docs[0] ? { id: docs[0].id, published: true } : null
484
+ },
485
+ }],
486
+ },
487
+ ```
488
+
489
+ Adapt both callbacks for the site's homepage path. These checks run before Start;
490
+ they do not protect pages from later deletion, unpublishing or renaming. Keep an
491
+ archived winner published at the same address: archived test URLs still point to it.
492
+ No migration is needed.
493
+
392
494
  ### `admin` options
393
495
 
394
496
  Host links and kind descriptions are optional. For a host using payload-puck's
395
- editor route and a `puck-templates` collection, pass:
497
+ editor route, public pages and a `puck-templates` collection:
498
+
499
+ | Option | Signature and behavior |
500
+ | --- | --- |
501
+ | `admin.previewUrl` | `(placement: PreviewPlacement) => string \| null \| undefined \| Promise<string \| null \| undefined>`. Receives `collection`, `id`, `title`, `published`, and optional `slug`. May be async; return `null` to hide preview for that placement. Each call has a 2 s limit, with at most 8 lookups awaited concurrently. |
502
+
503
+ For example, pass:
396
504
 
397
505
  ```ts
398
506
  admin: {
399
507
  editorUrl: ({ collection, id, draft }) =>
400
508
  `/admin/puck-editor/${encodeURIComponent(collection)}/${encodeURIComponent(String(id))}`,
401
- previewUrl: ({ id }) => `/preview/template/${encodeURIComponent(String(id))}`,
509
+ previewUrl: async ({ collection, slug }) => {
510
+ if (collection !== 'pages' || slug === undefined) return null
511
+ // Match the site's public page routing, including its homepage convention.
512
+ return slug === 'home' ? '/' : `/${slug}`
513
+ },
402
514
  descriptions: {
403
515
  block: 'Try different content on a landing page.',
404
516
  document: 'Try different versions of a donation ask.',
@@ -407,11 +519,21 @@ admin: {
407
519
  }
408
520
  ```
409
521
 
410
- Use your host's preview route for `previewUrl`. Preflight resolves both URL
411
- callbacks on the server for block placements; it passes only URL strings to the
412
- client. Missing or failed callbacks leave the URL unset, and the client uses the
413
- admin collection route when no editor URL is supplied. Kind descriptions are
414
- available to the server views through `config.custom.xp`.
522
+ `PreviewPlacement` is exported from the package root and `/rsc`. Its `slug` is
523
+ the published main row's string slug, including for a placement found in a newer
524
+ draft with a different slug. It is absent for unpublished documents. `published`
525
+ describes the placement's version; the callback receives no other document fields.
526
+ An async callback can also look up the public path by `collection` and `id` using
527
+ the host's own data access. The old `({ collection, id }) => string` form still works.
528
+
529
+ Preflight resolves the URL callbacks on the server for block placements and sends
530
+ only URL strings to the client. A null/undefined result, a non-string, an exception,
531
+ a rejected promise or a timeout leaves preview hidden for that placement. When an
532
+ editor URL exists, the admin offers "Edit on page"; when neither URL exists, it
533
+ omits the row. Timed-out host work cannot be cancelled and keeps its concurrency
534
+ slot until it settles; if all 8 slots remain occupied, further preview lookups
535
+ are skipped for that preflight. Kind descriptions are available to the server views through
536
+ `config.custom.xp`.
415
537
 
416
538
  A flag point can also declare `description?: string` to explain the setting below its label.
417
539
 
@@ -471,7 +593,8 @@ dashboard also includes **Tests** and **Goals** cards above its widgets. Goals
471
593
  appears only for users who can manage tests. Both additions follow the host's
472
594
  admin route and keep host-provided navigation and dashboard entries first.
473
595
  After upgrading, run `payload generate:importmap` to register the dashboard card
474
- component.
596
+ component and the server nav wrapper.
597
+ A host nav group also labelled "Tests" shares Payload's NavGroup DOM id and open-state preference with the plugin's group, so pick another label.
475
598
 
476
599
  ### Client assignments and exposure
477
600
 
@@ -524,19 +647,41 @@ configured to allow client writes; record revenue on the server.
524
647
 
525
648
  ### Puck render and editor configs
526
649
 
527
- Register `ExperimentSlot` from `@delmaredigital/payload-experiments/puck` in
528
- your render config and `ExperimentSlotEditor` from
529
- `@delmaredigital/payload-experiments/puck/editor` in your editor config, using
530
- the same `ExperimentSlot` component name:
650
+ Register the factories from `/puck` and `/puck/editor`, using the same
651
+ `ExperimentSlot` component name in both configs. Pass the host's Payload routes:
531
652
 
532
653
  ```ts
654
+ import { createExperimentSlot } from '@delmaredigital/payload-experiments/puck'
655
+ import { createExperimentSlotEditor, experimentOutlinePlugin } from '@delmaredigital/payload-experiments/puck/editor'
656
+
657
+ const routes = { admin: '/manage', api: '/content-api' } // Same as Payload config.routes
658
+ const accent = '#1f3a68' // Same as the plugin's theme.accent
659
+
533
660
  // Render config
534
- components: { ExperimentSlot }
661
+ components: { ExperimentSlot: createExperimentSlot({ routes }) }
535
662
 
536
663
  // Editor config
537
- components: { ExperimentSlot: ExperimentSlotEditor }
664
+ components: { ExperimentSlot: createExperimentSlotEditor({ routes, accent }) }
665
+ ```
666
+
667
+ Add `experimentOutlinePlugin()` to Puck's `plugins` alongside this block config
668
+ (with payload-puck, use its `plugins` option). It composes with the host's outline
669
+ override and hides empty version slots that the block's test doesn't use. Content
670
+ in an unused slot stays visible in the outline and gets a warning in the block.
671
+
672
+ ```ts
673
+ plugins: [experimentOutlinePlugin()]
538
674
  ```
539
675
 
676
+ Routes default to Payload's `/admin` and `/api` when omitted. The existing
677
+ `ExperimentSlot` and `ExperimentSlotEditor` exports remain available with these
678
+ defaults. Hosts on non-default routes switch to the factories. The standalone
679
+ `createExperimentSlotField({ routes })` also accepts the host's routes.
680
+
681
+ Pass the same accent as the plugin's `theme.accent`: the editor cannot read
682
+ Payload config. Both use the same validator and report the same error for an
683
+ invalid colour. Without an accent the editor uses the neutral palette.
684
+
540
685
  Load the editor stylesheet inside the Puck iframe. With Tailwind v4, add the
541
686
  package import **after** Tailwind in the CSS that builds your editor iframe
542
687
  stylesheet (the stylesheet listed in payload-puck's `editorStylesheets`):
@@ -552,6 +697,15 @@ The editor CSS includes its own light and dark palette, selected by
552
697
  `data-theme="dark"` or the `dark` class on the iframe's `<html>`. The client component's CSS import alone
553
698
  does not reach an iframe configured with `syncHostStyles: false`.
554
699
 
700
+ If Puck runs outside Payload's admin, the page needs Payload's admin styles (the plugin's tokens map onto them) and the plugin's admin styles, plus `data-theme="light"` or `data-theme="dark"` on `<html>`. Inside the admin all of this is already in place:
701
+
702
+ ```css
703
+ @import "@payloadcms/next/css";
704
+ @import "@payloadcms/ui/styles.css";
705
+ @import "@delmaredigital/payload-experiments/admin/theme.css";
706
+ @import "@delmaredigital/payload-experiments/admin/ui.css";
707
+ ```
708
+
555
709
  Resolve the Puck data on the server with `resolveExperimentData` before
556
710
  rendering it. The render component shows only the selected group's slot. The
557
711
  editor lets you select a block experiment, edit each group's content, copy the
@@ -632,10 +786,32 @@ changes block actions**; save first so they use the saved experiment.
632
786
  - The Puck picker lists 100 experiments.
633
787
  - The Puck experiment picker always calls `/api`; hosts with a custom API route
634
788
  see an empty picker.
635
- - Making a winner permanent skips pages in collections that are localized,
636
- have hidden fields, or contain fields the manager cannot read or update. Those pages
637
- are listed with a note to update them by hand.
638
789
  - Content-change markers are recorded on publish only (on save for collections
639
790
  without drafts).
640
791
  - Results cover the whole round; breakdowns by segment or dimension are not
641
792
  available in v1.
793
+
794
+ ### Limits
795
+
796
+ | Capability | Limit |
797
+ |---|---|
798
+ | Make it the content: localized collections | not supported yet |
799
+ | Make it the content: field defaults | An update-denied field empty in both versions may publish its default because Payload applies defaults when reading. |
800
+
801
+ ## Theme
802
+
803
+ The admin inherits Payload's neutral palette by default. Set one host accent:
804
+
805
+ ```ts
806
+ theme: { accent: '#1f3a68' }
807
+ ```
808
+
809
+ `theme.accent` accepts `#rgb` or `#rrggbb`; empty or undefined uses the default.
810
+ The plugin derives darker hover/active fills (clamped to keep text contrast), a
811
+ contrasting focus ring, and an opaque 8% subtle tint.
812
+ In dark mode it raises HSL lightness when needed to reach 3:1 against the
813
+ reference dark surface, then selects white or near-black foreground text.
814
+ The WCAG 2.x gate requires at least 4.5:1 **foreground text on the accent fill**
815
+ in each theme (the accent is used as a fill, not as page text). A failing
816
+ colour is refused with its ratio. Status chips retain Payload semantic colours.
817
+ `deriveAccent` is also exported from `/puck/editor` for editor integrations.
@@ -1,6 +1,7 @@
1
1
  import type { DocId } from '../types/contracts.js';
2
2
  import type { PreflightResult } from '../types/endpoints.js';
3
- export declare function PreviewLinks({ experimentId, placements, arms }: {
3
+ export declare function PreviewLinks({ experimentId, placements, arms, flagPoint }: {
4
+ flagPoint?: string;
4
5
  experimentId: DocId;
5
6
  placements: PreflightResult['placements'];
6
7
  arms: readonly {
@@ -1,13 +1,14 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useRef, useState } from 'react';
4
- import { Button } from '@payloadcms/ui';
4
+ import { Button } from '../ui/Button.js';
5
5
  import { armLabel } from './labels.js';
6
6
  import { useApi } from './useApi.js';
7
7
  import { REVIEW_COPY as C } from './copy/review.js';
8
- import { API_MESSAGES } from './copy/common.js';
9
- import { previewPages } from './previewLinks.model.js';
10
- export function PreviewLinks({ experimentId, placements, arms }) {
8
+ import { TEST_PAGE } from './copy/testPage.js';
9
+ import { API_MESSAGES, refusalMessage } from './copy/common.js';
10
+ import { actionablePreviewPages } from './previewLinks.model.js';
11
+ export function PreviewLinks({ experimentId, placements, arms, flagPoint }) {
11
12
  const api = useApi();
12
13
  const inFlight = useRef(false);
13
14
  const [busy, setBusy] = useState(false);
@@ -17,20 +18,37 @@ export function PreviewLinks({ experimentId, placements, arms }) {
17
18
  inFlight.current = true;
18
19
  setBusy(true);
19
20
  setError(null);
21
+ let win = null;
20
22
  try {
23
+ win = window.open('', '_blank');
24
+ if (!win) {
25
+ setError(C.previewBlocked);
26
+ return;
27
+ }
28
+ // Clearing opener immediately gives the same opener protection as noopener: the
29
+ // previewed page cannot reach window.opener. The admin URL may be sent as
30
+ // referrer to the host's own site (resolved against location.origin); accepted.
31
+ try {
32
+ win.opener = null;
33
+ } catch {}
21
34
  const result = await api.previewToken(experimentId, arm);
35
+ // The person closed the blank tab while the token loaded: nothing to show or report.
36
+ if (win.closed) return;
22
37
  if (!result.ok) {
23
- setError(result.message);
38
+ win.close();
39
+ setError(result.status === 0 ? API_MESSAGES.network : refusalMessage(result.code, result.status));
24
40
  return;
25
41
  }
26
- if (!result.data?.token) {
42
+ if (typeof result.data?.token !== 'string' || !result.data.token) {
43
+ win.close();
27
44
  setError(API_MESSAGES.unreadable);
28
45
  return;
29
46
  }
30
47
  const target = new URL(url, window.location.origin);
31
48
  target.searchParams.set('xp_preview', result.data.token);
32
- window.open(target.href, '_blank', 'noopener,noreferrer');
49
+ win.location.replace(target.href);
33
50
  } catch {
51
+ win?.close();
34
52
  setError(API_MESSAGES.failed);
35
53
  } finally{
36
54
  inFlight.current = false;
@@ -40,7 +58,8 @@ export function PreviewLinks({ experimentId, placements, arms }) {
40
58
  return /*#__PURE__*/ _jsxs("div", {
41
59
  className: "xp-preview-links",
42
60
  children: [
43
- previewPages(placements).map((placement)=>/*#__PURE__*/ _jsxs("div", {
61
+ actionablePreviewPages(placements, flagPoint).map((placement)=>/*#__PURE__*/ _jsxs("div", {
62
+ className: "xp-preview-row",
44
63
  children: [
45
64
  /*#__PURE__*/ _jsxs("p", {
46
65
  children: [
@@ -48,19 +67,29 @@ export function PreviewLinks({ experimentId, placements, arms }) {
48
67
  placement.slotCount > 1 ? ` ${C.placedTimes(placement.slotCount)}` : null
49
68
  ]
50
69
  }),
51
- placement.url ? arms.filter((arm)=>arm.kind !== 'holdout' && arm.key !== 'holdout').map((arm)=>/*#__PURE__*/ _jsxs(Button, {
52
- disabled: busy,
53
- onClick: ()=>{
54
- void preview(placement.url, arm.key);
55
- },
56
- children: [
57
- C.preview,
58
- " ",
59
- arm.label?.trim() || armLabel(arm.key)
60
- ]
61
- }, arm.key)) : /*#__PURE__*/ _jsx(Button, {
62
- disabled: true,
63
- children: C.noPreview
70
+ /*#__PURE__*/ _jsxs("div", {
71
+ className: "xp-ui-button-group",
72
+ children: [
73
+ placement.url ? arms.filter((arm)=>arm.kind !== 'holdout' && arm.key !== 'holdout').map((arm)=>/*#__PURE__*/ _jsxs(Button, {
74
+ variant: "secondary",
75
+ size: "sm",
76
+ disabled: busy,
77
+ onClick: ()=>{
78
+ void preview(placement.url, arm.key);
79
+ },
80
+ children: [
81
+ C.preview,
82
+ " ",
83
+ arm.label?.trim() || armLabel(arm.key)
84
+ ]
85
+ }, arm.key)) : null,
86
+ placement.editorUrl ? /*#__PURE__*/ _jsx(Button, {
87
+ variant: "link",
88
+ size: "sm",
89
+ href: placement.editorUrl,
90
+ children: TEST_PAGE.editOnPage
91
+ }) : null
92
+ ]
64
93
  })
65
94
  ]
66
95
  }, placement.key)),
@@ -1,3 +1,5 @@
1
1
  import './theme.css';
2
- declare function TestsNav(): import("react").JSX.Element | null;
2
+ declare function TestsNav({ isOpen }: {
3
+ isOpen?: boolean;
4
+ }): import("react").JSX.Element | null;
3
5
  export { TestsNav as TestsNavLink };
@@ -5,7 +5,7 @@ import { Link, NavGroup, useAuth, useConfig } from '@payloadcms/ui';
5
5
  import { formatAdminURL } from 'payload/shared';
6
6
  import { usePathname } from 'next/navigation.js';
7
7
  import { SURFACE_COPY } from './copy/surface.js';
8
- function TestsNav() {
8
+ function TestsNav({ isOpen = true }) {
9
9
  const { config } = useConfig();
10
10
  const { permissions } = useAuth();
11
11
  const pathname = usePathname();
@@ -38,9 +38,9 @@ function TestsNav() {
38
38
  active: goalsActive
39
39
  }
40
40
  ];
41
- // getPreference is async and can fetch; let NavGroup default to open.
42
41
  return /*#__PURE__*/ _jsx(NavGroup, {
43
42
  label: SURFACE_COPY.title,
43
+ isOpen: isOpen,
44
44
  children: entries.map((entry)=>{
45
45
  const props = {
46
46
  className: 'nav__link',
@@ -1,13 +1,16 @@
1
- import '../theme.css';
2
1
  import type { ReactNode } from 'react';
3
2
  import type { DocId } from '../../types/contracts.js';
4
3
  import type { ExperimentAction } from './actions.model.js';
5
- export declare function DialogFrame({ title, onClose, children }: {
4
+ export declare function DialogFrame({ title, onClose, children, initialFocus, dismissible }: {
5
+ dismissible?: boolean;
6
+ initialFocus?: 'cancel';
6
7
  title: string;
7
8
  onClose: () => void;
8
9
  children: ReactNode;
9
- }): import("react").ReactPortal;
10
- export declare function ConfirmDialog({ id, action, winnerLabel, onClose }: {
10
+ }): import("react").JSX.Element;
11
+ export declare function ConfirmDialog({ id, action, winnerLabel, onClose, canManage, isDecided }: {
12
+ canManage?: boolean;
13
+ isDecided?: boolean;
11
14
  id: DocId;
12
15
  action: ExperimentAction;
13
16
  winnerLabel: string;