@delmaredigital/payload-experiments 0.3.0-rc.4 → 0.4.0-rc.1

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 (265) hide show
  1. package/README.md +113 -20
  2. package/dist/admin/MetricSourceSelect.js +3 -3
  3. package/dist/admin/PlacementStatus.d.ts +3 -0
  4. package/dist/admin/PlacementStatus.js +54 -0
  5. package/dist/admin/PreviewLinks.d.ts +2 -1
  6. package/dist/admin/PreviewLinks.js +2 -2
  7. package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
  8. package/dist/admin/actions/ConfirmDialog.js +2 -2
  9. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  10. package/dist/admin/actions/DecideDialog.js +2 -2
  11. package/dist/admin/api.d.ts +11 -2
  12. package/dist/admin/api.js +20 -2
  13. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  14. package/dist/admin/blocks/BlockPanel.js +153 -0
  15. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  16. package/dist/admin/blocks/TestBlockField.js +163 -0
  17. package/dist/admin/blocks/formApi.d.ts +43 -0
  18. package/dist/admin/blocks/formApi.js +153 -0
  19. package/dist/admin/blocks/inject.d.ts +7 -0
  20. package/dist/admin/blocks/inject.js +41 -0
  21. package/dist/admin/blocks/model.d.ts +17 -0
  22. package/dist/admin/blocks/model.js +45 -0
  23. package/dist/admin/blocks/statusCache.d.ts +7 -0
  24. package/dist/admin/blocks/statusCache.js +83 -0
  25. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  26. package/dist/admin/blocks/useBlockForm.js +46 -0
  27. package/dist/admin/copy/actions.d.ts +2 -0
  28. package/dist/admin/copy/actions.js +9 -0
  29. package/dist/admin/copy/blocks.d.ts +27 -0
  30. package/dist/admin/copy/blocks.js +38 -0
  31. package/dist/admin/copy/goals.d.ts +2 -0
  32. package/dist/admin/copy/goals.js +4 -1
  33. package/dist/admin/copy/newTest.d.ts +10 -0
  34. package/dist/admin/copy/newTest.js +10 -0
  35. package/dist/admin/copy/pages.d.ts +26 -0
  36. package/dist/admin/copy/pages.js +26 -0
  37. package/dist/admin/copy/readiness.d.ts +22 -3
  38. package/dist/admin/copy/readiness.js +23 -3
  39. package/dist/admin/copy/results.d.ts +1 -0
  40. package/dist/admin/copy/results.js +1 -0
  41. package/dist/admin/copy/testPage.d.ts +4 -0
  42. package/dist/admin/copy/testPage.js +4 -0
  43. package/dist/admin/goals/XpGoalField.d.ts +2 -0
  44. package/dist/admin/goals/XpGoalField.js +152 -0
  45. package/dist/admin/metricSource.model.d.ts +2 -0
  46. package/dist/admin/metricSource.model.js +4 -0
  47. package/dist/admin/pageCollections.d.ts +10 -0
  48. package/dist/admin/pageCollections.js +20 -0
  49. package/dist/admin/paths.d.ts +6 -0
  50. package/dist/admin/paths.js +6 -0
  51. package/dist/admin/placementStatus.model.d.ts +2 -0
  52. package/dist/admin/placementStatus.model.js +19 -0
  53. package/dist/admin/placements/placementLabels.d.ts +11 -0
  54. package/dist/admin/placements/placementLabels.js +21 -0
  55. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  56. package/dist/admin/placements/placementLabels.model.js +12 -0
  57. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  58. package/dist/admin/placements/usePlacementLabels.js +102 -0
  59. package/dist/admin/results/DataQualityPanel.js +4 -1
  60. package/dist/admin/results/results.model.d.ts +3 -0
  61. package/dist/admin/results/results.model.js +8 -4
  62. package/dist/admin/surface/TestsHome.js +37 -6
  63. package/dist/admin/surface/TestsSurface.js +5 -1
  64. package/dist/admin/surface/contracts.d.ts +6 -5
  65. package/dist/admin/surface/goals/GoalForm.js +67 -34
  66. package/dist/admin/surface/goals/GoalsList.js +3 -3
  67. package/dist/admin/surface/goals/data.js +4 -1
  68. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  69. package/dist/admin/surface/goals/goals.model.js +1 -3
  70. package/dist/admin/surface/home.model.d.ts +1 -1
  71. package/dist/admin/surface/home.model.js +8 -4
  72. package/dist/admin/surface/newTest/NewTest.js +54 -13
  73. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  74. package/dist/admin/surface/newTest/PageWizard.js +209 -0
  75. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  76. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  77. package/dist/admin/surface/pagePicker/PagePicker.d.ts +10 -0
  78. package/dist/admin/surface/pagePicker/PagePicker.js +172 -0
  79. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +25 -0
  80. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  81. package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
  82. package/dist/admin/surface/readiness/Banner.js +30 -6
  83. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  84. package/dist/admin/surface/readiness/readiness.model.js +91 -9
  85. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  86. package/dist/admin/surface/results/ReleasePages.js +3 -7
  87. package/dist/admin/surface/results/ResultsBody.js +21 -2
  88. package/dist/admin/surface/results/ResultsHeader.js +2 -5
  89. package/dist/admin/surface/results/useResults.d.ts +10 -1
  90. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  91. package/dist/admin/surface/test/GoalSection.js +90 -7
  92. package/dist/admin/surface/test/LengthSection.js +1 -1
  93. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  94. package/dist/admin/surface/test/PageHoldout.js +67 -0
  95. package/dist/admin/surface/test/TestPage.js +2 -1
  96. package/dist/admin/surface/test/VersionsSection.js +12 -5
  97. package/dist/admin/surface/test/fields.d.ts +13 -4
  98. package/dist/admin/surface/test/fields.js +46 -14
  99. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  100. package/dist/admin/surface/test/jumpToSection.js +4 -3
  101. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  102. package/dist/admin/surface/test/pageParts.js +79 -2
  103. package/dist/admin/surface/test/testPage.css +8 -1
  104. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  105. package/dist/admin/surface/testDocument.model.js +2 -1
  106. package/dist/admin/surface/useTestDocument.d.ts +11 -2
  107. package/dist/admin/surface/useTestDocument.js +53 -15
  108. package/dist/admin/useApi.d.ts +11 -2
  109. package/dist/admin/useApi.js +3 -5
  110. package/dist/admin/views/ExperimentsRootView.js +58 -16
  111. package/dist/blocks/bindings.d.ts +3 -0
  112. package/dist/blocks/bindings.js +34 -0
  113. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  114. package/dist/blocks/createExperimentBlock.js +77 -0
  115. package/dist/blocks/formState.d.ts +31 -0
  116. package/dist/blocks/formState.js +146 -0
  117. package/dist/blocks/model.d.ts +41 -0
  118. package/dist/blocks/model.js +34 -0
  119. package/dist/client/ExperimentGroup.d.ts +6 -0
  120. package/dist/client/ExperimentGroup.js +9 -0
  121. package/dist/client/ExperimentsProvider.js +20 -4
  122. package/dist/client/clickGoals.d.ts +4 -0
  123. package/dist/client/clickGoals.js +43 -0
  124. package/dist/collections/experiments/config.js +12 -2
  125. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  126. package/dist/collections/experiments/deleteGuard.js +12 -1
  127. package/dist/collections/experiments/editability.js +4 -2
  128. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  129. package/dist/collections/experiments/pickSlug.js +8 -0
  130. package/dist/collections/labels.d.ts +3 -0
  131. package/dist/collections/labels.js +20 -0
  132. package/dist/collections/metrics.js +12 -2
  133. package/dist/collections/phases.js +1 -0
  134. package/dist/collections/serialize.js +7 -0
  135. package/dist/db/clickGoals.d.ts +9 -0
  136. package/dist/db/clickGoals.js +22 -0
  137. package/dist/db/delete.d.ts +1 -0
  138. package/dist/db/delete.js +1 -0
  139. package/dist/db/locks.d.ts +8 -0
  140. package/dist/db/locks.js +27 -4
  141. package/dist/db/names.d.ts +4 -0
  142. package/dist/db/names.js +4 -0
  143. package/dist/db/pageBindings.d.ts +4 -0
  144. package/dist/db/pageBindings.js +17 -0
  145. package/dist/db/pageBusy.d.ts +2 -0
  146. package/dist/db/pageBusy.js +10 -0
  147. package/dist/db/pageContent.d.ts +10 -0
  148. package/dist/db/pageContent.js +79 -0
  149. package/dist/db/placements.d.ts +33 -0
  150. package/dist/db/placements.js +54 -0
  151. package/dist/db/slugs.d.ts +8 -0
  152. package/dist/db/slugs.js +40 -0
  153. package/dist/db/snapshots.d.ts +6 -0
  154. package/dist/db/snapshots.js +8 -5
  155. package/dist/endpoints/clickKeys.d.ts +5 -0
  156. package/dist/endpoints/clickKeys.js +17 -0
  157. package/dist/endpoints/events.js +4 -1
  158. package/dist/endpoints/index.js +28 -0
  159. package/dist/endpoints/pages.d.ts +5 -0
  160. package/dist/endpoints/pages.js +120 -0
  161. package/dist/endpoints/placementStatus.d.ts +6 -0
  162. package/dist/endpoints/placementStatus.js +238 -0
  163. package/dist/endpoints/readBody.d.ts +2 -0
  164. package/dist/endpoints/readBody.js +24 -0
  165. package/dist/endpoints/results.js +5 -1
  166. package/dist/endpoints/summary.js +11 -4
  167. package/dist/endpoints/wrapBlock.d.ts +6 -0
  168. package/dist/endpoints/wrapBlock.js +76 -0
  169. package/dist/errors.d.ts +3 -3
  170. package/dist/errors.js +4 -4
  171. package/dist/events/rows.d.ts +1 -1
  172. package/dist/events/rows.js +5 -3
  173. package/dist/exports/adminClient.d.ts +5 -0
  174. package/dist/exports/adminClient.js +5 -0
  175. package/dist/exports/blocks.d.ts +2 -0
  176. package/dist/exports/blocks.js +1 -0
  177. package/dist/exports/client.d.ts +1 -0
  178. package/dist/exports/client.js +1 -0
  179. package/dist/exports/index.d.ts +6 -1
  180. package/dist/exports/index.js +3 -0
  181. package/dist/exports/puck.d.ts +3 -1
  182. package/dist/exports/puck.js +2 -0
  183. package/dist/exports/puckEditor.d.ts +3 -0
  184. package/dist/exports/puckEditor.js +2 -0
  185. package/dist/exports/rsc.d.ts +3 -0
  186. package/dist/exports/rsc.js +2 -0
  187. package/dist/fields/xpGoal.d.ts +3 -0
  188. package/dist/fields/xpGoal.js +28 -0
  189. package/dist/globals/state.js +10 -0
  190. package/dist/goals/cache.d.ts +3 -0
  191. package/dist/goals/cache.js +35 -0
  192. package/dist/goals/fairness.d.ts +40 -0
  193. package/dist/goals/fairness.js +200 -0
  194. package/dist/goals/key.d.ts +4 -0
  195. package/dist/goals/key.js +3 -0
  196. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  197. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  198. package/dist/goals/picker/copy.d.ts +16 -0
  199. package/dist/goals/picker/copy.js +15 -0
  200. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  201. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  202. package/dist/hooks/pageGuard.d.ts +5 -1
  203. package/dist/hooks/pageGuard.js +157 -95
  204. package/dist/onInit.js +29 -0
  205. package/dist/options.d.ts +2 -0
  206. package/dist/options.js +41 -1
  207. package/dist/plugin.js +41 -28
  208. package/dist/puck/copy.d.ts +1 -0
  209. package/dist/puck/copy.js +1 -0
  210. package/dist/puck/editor/editor.chrome.css +1 -0
  211. package/dist/puck/editor/editor.css +1 -0
  212. package/dist/puck/editor/editor.model.d.ts +4 -2
  213. package/dist/puck/editor/editor.model.js +6 -3
  214. package/dist/puck/editor/integration.d.ts +10 -0
  215. package/dist/puck/editor/integration.js +30 -0
  216. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  217. package/dist/puck/editor/xpGoalPuckEditorField.js +134 -0
  218. package/dist/puck/integration.d.ts +6 -0
  219. package/dist/puck/integration.js +21 -0
  220. package/dist/puck/xpGoalField.d.ts +3 -0
  221. package/dist/puck/xpGoalField.js +6 -0
  222. package/dist/results/health.d.ts +4 -0
  223. package/dist/results/health.js +27 -0
  224. package/dist/results/snapshot.js +15 -2
  225. package/dist/sanitizeReadinessDetail.js +41 -0
  226. package/dist/server/pageKindSeoContract.d.ts +27 -0
  227. package/dist/server/pageKindSeoContract.js +80 -0
  228. package/dist/server/recordContentChange.js +58 -8
  229. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  230. package/dist/server/resolveExperimentBlocks.js +54 -0
  231. package/dist/server/runResults.js +56 -15
  232. package/dist/server/toClientAssignments.js +2 -1
  233. package/dist/transitions/addMetric.d.ts +2 -1
  234. package/dist/transitions/addMetric.js +28 -1
  235. package/dist/transitions/checkStart.d.ts +13 -1
  236. package/dist/transitions/checkStart.js +89 -7
  237. package/dist/transitions/decide.js +24 -0
  238. package/dist/transitions/deleteExperiment.d.ts +3 -1
  239. package/dist/transitions/deleteExperiment.js +27 -1
  240. package/dist/transitions/kernel.js +4 -0
  241. package/dist/transitions/makeContent.d.ts +5 -0
  242. package/dist/transitions/makeContent.js +39 -11
  243. package/dist/transitions/pageCollection.d.ts +10 -0
  244. package/dist/transitions/pageCollection.js +20 -0
  245. package/dist/transitions/pageVersions.d.ts +10 -0
  246. package/dist/transitions/pageVersions.js +44 -0
  247. package/dist/transitions/phaseRecord.d.ts +1 -0
  248. package/dist/transitions/placementInteractions.d.ts +40 -0
  249. package/dist/transitions/placementInteractions.js +94 -0
  250. package/dist/transitions/placements.d.ts +8 -0
  251. package/dist/transitions/placements.js +148 -1
  252. package/dist/transitions/preflightData.d.ts +2 -2
  253. package/dist/transitions/preflightData.js +137 -5
  254. package/dist/transitions/resolveCaps.d.ts +1 -0
  255. package/dist/transitions/resolveCaps.js +6 -2
  256. package/dist/transitions/start.js +45 -1
  257. package/dist/types/admin.d.ts +1 -1
  258. package/dist/types/assignments.d.ts +1 -0
  259. package/dist/types/contracts.d.ts +15 -0
  260. package/dist/types/endpoints.d.ts +92 -5
  261. package/dist/types/index.d.ts +3 -0
  262. package/dist/types/puck.d.ts +7 -1
  263. package/dist/types/snapshot.d.ts +17 -4
  264. package/dist/types/surface.d.ts +13 -0
  265. package/package.json +8 -1
package/dist/plugin.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { buildCollections } from './collections/index.js';
2
2
  import { buildEndpoints } from './endpoints/index.js';
3
3
  import { contentChangesBeforeHook, contentChangesHook } from './hooks/contentChanges.js';
4
- import { pageGuardAfterOperation, pageGuardBeforeChange, pageGuardBeforeDelete, pageGuardBeforeOperation } from './hooks/pageGuard.js';
4
+ import { pageGuardAfterChange, pageGuardAfterOperation, pageGuardBeforeChange, pageGuardBeforeDelete, pageGuardBeforeOperation } from './hooks/pageGuard.js';
5
5
  import { createOnInit } from './onInit.js';
6
6
  import { puckCollections, resolveOptions } from './options.js';
7
+ import { injectTestBlock } from './admin/blocks/inject.js';
7
8
  import * as paths from './admin/paths.js';
8
9
  export function createExperimentsPlugin(input) {
9
10
  const options = resolveOptions(input);
@@ -15,6 +16,9 @@ export function createExperimentsPlugin(input) {
15
16
  for (const { slug } of options.pages?.collections ?? []){
16
17
  if (!config.collections?.some((collection)=>collection.slug === slug)) throw new Error(`Unknown experiments page collection: ${slug}`);
17
18
  }
19
+ for (const { slug } of options.blocks?.collections ?? []){
20
+ if (!config.collections?.some((collection)=>collection.slug === slug)) throw new Error(`Unknown experiments blocks collection: ${slug}`);
21
+ }
18
22
  // The frozen factories consume registration/access metadata, never TUser callbacks.
19
23
  // Erase the user subtype only at this non-generic factory boundary.
20
24
  const { collections, globals } = buildCollections(options);
@@ -59,40 +63,49 @@ export function createExperimentsPlugin(input) {
59
63
  ...(config.collections ?? []).map((collection)=>{
60
64
  const puck = options.puck?.enabled && registrations.some((entry)=>entry.slug === collection.slug);
61
65
  const page = options.pages?.collections.some((entry)=>entry.slug === collection.slug);
62
- if (!puck && !page) return collection;
66
+ const blocks = options.blocks?.collections.find((entry)=>entry.slug === collection.slug);
67
+ if (!puck && !page && !blocks) return collection;
63
68
  return {
64
69
  ...collection,
70
+ fields: [
71
+ ...page ? [
72
+ {
73
+ name: 'xpPlacementStatus',
74
+ type: 'ui',
75
+ admin: {
76
+ position: 'sidebar',
77
+ components: {
78
+ Field: paths.XP_PLACEMENT_STATUS
79
+ }
80
+ }
81
+ }
82
+ ] : [],
83
+ ...blocks ? injectTestBlock(collection.fields, blocks) : collection.fields
84
+ ],
65
85
  hooks: {
66
86
  ...collection.hooks,
67
87
  beforeChange: [
68
88
  ...collection.hooks?.beforeChange ?? [],
69
- ...page ? [
70
- pageGuardBeforeChange
71
- ] : [],
72
- ...puck ? [
73
- contentChangesBeforeHook
74
- ] : []
89
+ pageGuardBeforeChange,
90
+ contentChangesBeforeHook
91
+ ],
92
+ beforeOperation: [
93
+ ...collection.hooks?.beforeOperation ?? [],
94
+ pageGuardBeforeOperation
95
+ ],
96
+ beforeDelete: [
97
+ ...collection.hooks?.beforeDelete ?? [],
98
+ pageGuardBeforeDelete
99
+ ],
100
+ afterOperation: [
101
+ pageGuardAfterOperation,
102
+ ...collection.hooks?.afterOperation ?? []
75
103
  ],
76
- ...page ? {
77
- beforeOperation: [
78
- ...collection.hooks?.beforeOperation ?? [],
79
- pageGuardBeforeOperation
80
- ],
81
- beforeDelete: [
82
- ...collection.hooks?.beforeDelete ?? [],
83
- pageGuardBeforeDelete
84
- ],
85
- afterOperation: [
86
- pageGuardAfterOperation,
87
- ...collection.hooks?.afterOperation ?? []
88
- ]
89
- } : {},
90
- ...puck ? {
91
- afterChange: [
92
- ...collection.hooks?.afterChange ?? [],
93
- contentChangesHook
94
- ]
95
- } : {}
104
+ afterChange: [
105
+ ...collection.hooks?.afterChange ?? [],
106
+ pageGuardAfterChange,
107
+ contentChangesHook
108
+ ]
96
109
  }
97
110
  };
98
111
  }),
@@ -1,3 +1,4 @@
1
+ export declare const categoryTitle = "A/B tests";
1
2
  export declare const slotLabels: {
2
3
  readonly control: "Original";
3
4
  readonly variantA: "Variant A";
package/dist/puck/copy.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ARM_LABELS } from '../labels/arms.js';
2
+ export const categoryTitle = 'A/B tests';
2
3
  export const slotLabels = {
3
4
  control: ARM_LABELS.control,
4
5
  variantA: ARM_LABELS.variantA,
@@ -119,3 +119,4 @@
119
119
  .xp-slot-readout { display: grid; gap: var(--xp-space-2); }
120
120
  .xp-slot-readout > .xp-ui-button { justify-self: start; }
121
121
  .xp-slot-editor .xp-ui-tablist { flex: 1 1 auto; min-width: 0; flex-wrap: wrap; }
122
+ .xp-slot-root.xp-puck-field { display: grid; gap: var(--xp-space-3); min-width: 0; }
@@ -384,3 +384,4 @@
384
384
  .xp-slot-readout { display: grid; gap: var(--xp-space-2); }
385
385
  .xp-slot-readout > .xp-ui-button { justify-self: start; }
386
386
  .xp-slot-editor .xp-ui-tablist { flex: 1 1 auto; min-width: 0; flex-wrap: wrap; }
387
+ .xp-slot-root.xp-puck-field { display: grid; gap: var(--xp-space-3); min-width: 0; }
@@ -17,10 +17,11 @@ export declare function bannerFor(status?: Status | null, winner?: string): stri
17
17
  export declare function slugify(name: string): string;
18
18
  /** Complete draft defaults, shared by the page hand-off and setting creation. */
19
19
  export declare function newTestBody(input: {
20
- kind: 'block' | 'flag';
20
+ kind: 'block' | 'flag' | 'page';
21
21
  name: string;
22
22
  slug: string;
23
23
  unitType: UnitType;
24
+ pageCollection?: string;
24
25
  flagPoint?: string;
25
26
  layer?: DocId;
26
27
  hypothesis?: string;
@@ -28,9 +29,10 @@ export declare function newTestBody(input: {
28
29
  hypothesis?: string | undefined;
29
30
  layer?: DocId | undefined;
30
31
  flagPoint?: string | undefined;
32
+ pageCollection?: string | undefined;
31
33
  name: string;
32
34
  slug: string;
33
- kind: "block" | "flag";
35
+ kind: "block" | "page" | "flag";
34
36
  unitType: UnitType;
35
37
  exposurePolicy: string;
36
38
  arms: Arm[];
@@ -32,7 +32,7 @@ export function slugify(name) {
32
32
  return name.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64).replace(/-+$/g, '');
33
33
  }
34
34
  /** Complete draft defaults, shared by the page hand-off and setting creation. */ export function newTestBody(input) {
35
- const { kind, name, slug, unitType, flagPoint, layer, hypothesis } = input;
35
+ const { kind, name, slug, unitType, flagPoint, layer, hypothesis, pageCollection } = input;
36
36
  const arms = [
37
37
  {
38
38
  key: 'control',
@@ -57,13 +57,16 @@ export function slugify(name) {
57
57
  name,
58
58
  slug,
59
59
  kind,
60
- unitType,
61
- exposurePolicy: kind === 'block' ? 'render' : 'manual',
60
+ unitType: kind === 'page' ? 'visitor' : unitType,
61
+ exposurePolicy: kind === 'block' ? 'render' : kind === 'page' ? 'pageview' : 'manual',
62
62
  arms,
63
63
  allocation: 100,
64
64
  analysis: {
65
65
  plannedDurationDays: 28
66
66
  },
67
+ ...pageCollection === undefined ? {} : {
68
+ pageCollection
69
+ },
67
70
  ...flagPoint === undefined ? {} : {
68
71
  flagPoint
69
72
  },
@@ -0,0 +1,10 @@
1
+ import { type ExperimentsPuckIntegration } from '../../types/puck.js';
2
+ import type { PuckRoutes } from '../routes.js';
3
+ /**
4
+ * Call once at module scope in a 'use client' module. A new object per render
5
+ * rebuilds Puck's config and plugin list.
6
+ */
7
+ export declare function experimentsPuckEditor({ routes, accent }?: {
8
+ routes?: PuckRoutes;
9
+ accent?: string;
10
+ }): ExperimentsPuckIntegration;
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+ import { EXPERIMENT_SLOT_TYPE } from '../../types/puck.js';
3
+ import { categoryTitle } from '../copy.js';
4
+ import { createExperimentSlotEditor } from './ExperimentSlotEditor.js';
5
+ import { experimentOutlinePlugin } from './experimentOutlinePlugin.js';
6
+ /**
7
+ * Call once at module scope in a 'use client' module. A new object per render
8
+ * rebuilds Puck's config and plugin list.
9
+ */ export function experimentsPuckEditor({ routes, accent } = {}) {
10
+ return {
11
+ name: 'experiments',
12
+ components: {
13
+ [EXPERIMENT_SLOT_TYPE]: createExperimentSlotEditor({
14
+ routes,
15
+ accent
16
+ })
17
+ },
18
+ categories: {
19
+ experiments: {
20
+ title: categoryTitle,
21
+ components: [
22
+ EXPERIMENT_SLOT_TYPE
23
+ ]
24
+ }
25
+ },
26
+ plugins: [
27
+ experimentOutlinePlugin()
28
+ ]
29
+ };
30
+ }
@@ -0,0 +1,7 @@
1
+ import type { CustomField } from '@puckeditor/core';
2
+ import { type PuckRoutes } from '../routes.js';
3
+ import './editor.css';
4
+ /** Editor config only. The render config keeps xpGoalPuckField() from /puck. */
5
+ export declare function xpGoalPuckEditorField(options?: {
6
+ routes?: PuckRoutes;
7
+ }): CustomField<string>;
@@ -0,0 +1,134 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useId } from 'react';
4
+ import { useClickGoalPicker } from '../../goals/picker/useClickGoalPicker.js';
5
+ import { CLICK_GOAL_COPY as C } from '../../goals/picker/copy.js';
6
+ import { TextInput, Select } from '../../ui/Inputs.js';
7
+ import { Button } from '../../ui/Button.js';
8
+ import { resolvePuckRoutes } from '../routes.js';
9
+ import './editor.css';
10
+ function XpGoalPuckEditor({ value, onChange, api, readOnly }) {
11
+ const picker = useClickGoalPicker(api, {
12
+ onSelect: onChange
13
+ });
14
+ const id = useId();
15
+ const stopEnter = (event)=>{
16
+ if (event.key === 'Enter') {
17
+ event.preventDefault();
18
+ event.stopPropagation();
19
+ }
20
+ };
21
+ return /*#__PURE__*/ _jsxs("div", {
22
+ className: "xp-slot-root xp-puck-field",
23
+ children: [
24
+ /*#__PURE__*/ _jsx(TextInput, {
25
+ id: `${id}-value`,
26
+ label: C.label,
27
+ value: value ?? '',
28
+ readOnly: readOnly,
29
+ onChange: (event)=>onChange(event.currentTarget.value)
30
+ }),
31
+ picker.canManage && !readOnly ? /*#__PURE__*/ _jsxs(_Fragment, {
32
+ children: [
33
+ /*#__PURE__*/ _jsxs(Select, {
34
+ id: `${id}-picker`,
35
+ label: C.choose,
36
+ value: picker.goals.some((goal)=>goal.slug === value) ? value : '',
37
+ onChange: (event)=>{
38
+ if (event.currentTarget.value) onChange(event.currentTarget.value);
39
+ },
40
+ children: [
41
+ /*#__PURE__*/ _jsx("option", {
42
+ value: "",
43
+ children: C.choose
44
+ }),
45
+ picker.goals.map((goal)=>/*#__PURE__*/ _jsx("option", {
46
+ value: goal.slug,
47
+ children: goal.name
48
+ }, goal.id))
49
+ ]
50
+ }),
51
+ picker.undefinedKey(value ?? '') ? /*#__PURE__*/ _jsx("p", {
52
+ className: "xp-ui-help",
53
+ role: "status",
54
+ children: C.undefinedKey
55
+ }) : null,
56
+ !picker.panel.open ? /*#__PURE__*/ _jsx(Button, {
57
+ type: "button",
58
+ onClick: picker.open,
59
+ children: C.newGoal
60
+ }) : /*#__PURE__*/ _jsxs("section", {
61
+ "aria-labelledby": `${id}-title`,
62
+ children: [
63
+ /*#__PURE__*/ _jsx("h3", {
64
+ id: `${id}-title`,
65
+ children: C.title
66
+ }),
67
+ /*#__PURE__*/ _jsx(TextInput, {
68
+ id: `${id}-name`,
69
+ label: C.name,
70
+ value: picker.panel.name,
71
+ disabled: picker.panel.pending,
72
+ onKeyDown: stopEnter,
73
+ onChange: (event)=>picker.setName(event.currentTarget.value)
74
+ }),
75
+ /*#__PURE__*/ _jsxs("details", {
76
+ open: picker.panel.advanced,
77
+ onToggle: (event)=>picker.setAdvanced(event.currentTarget.open),
78
+ children: [
79
+ /*#__PURE__*/ _jsx("summary", {
80
+ children: C.advanced
81
+ }),
82
+ /*#__PURE__*/ _jsx(TextInput, {
83
+ id: `${id}-slug`,
84
+ label: C.slug,
85
+ value: picker.panel.slug,
86
+ disabled: picker.panel.pending,
87
+ onKeyDown: stopEnter,
88
+ error: picker.panel.errors.slug,
89
+ onChange: (event)=>picker.setSlug(event.currentTarget.value)
90
+ })
91
+ ]
92
+ }),
93
+ picker.panel.errors.general ? /*#__PURE__*/ _jsx("p", {
94
+ className: "xp-ui-error",
95
+ role: "alert",
96
+ children: picker.panel.errors.general
97
+ }) : null,
98
+ /*#__PURE__*/ _jsxs("div", {
99
+ className: "xp-ui-button-group",
100
+ children: [
101
+ /*#__PURE__*/ _jsx(Button, {
102
+ type: "button",
103
+ disabled: picker.panel.pending || !picker.panel.name.trim(),
104
+ onClick: ()=>void picker.create(),
105
+ children: C.createGoal
106
+ }),
107
+ /*#__PURE__*/ _jsx(Button, {
108
+ type: "button",
109
+ disabled: picker.panel.pending,
110
+ onClick: picker.cancel,
111
+ children: C.cancel
112
+ })
113
+ ]
114
+ })
115
+ ]
116
+ })
117
+ ]
118
+ }) : null
119
+ ]
120
+ });
121
+ }
122
+ /** Editor config only. The render config keeps xpGoalPuckField() from /puck. */ export function xpGoalPuckEditorField(options) {
123
+ const routes = resolvePuckRoutes(options?.routes);
124
+ return {
125
+ type: 'custom',
126
+ label: C.label,
127
+ render: ({ value, onChange, readOnly })=>/*#__PURE__*/ _jsx(XpGoalPuckEditor, {
128
+ value: value,
129
+ onChange: onChange,
130
+ api: routes.api,
131
+ readOnly: readOnly
132
+ })
133
+ };
134
+ }
@@ -0,0 +1,6 @@
1
+ import { type ExperimentsPuckIntegration } from '../types/puck.js';
2
+ import type { PuckRoutes } from './routes.js';
3
+ /** Bundle the test block and its sidebar category for a host render config. */
4
+ export declare function experimentsPuck({ routes }?: {
5
+ routes?: PuckRoutes;
6
+ }): ExperimentsPuckIntegration;
@@ -0,0 +1,21 @@
1
+ import { EXPERIMENT_SLOT_TYPE } from '../types/puck.js';
2
+ import { createExperimentSlot } from './ExperimentSlot.js';
3
+ import { categoryTitle } from './copy.js';
4
+ /** Bundle the test block and its sidebar category for a host render config. */ export function experimentsPuck({ routes } = {}) {
5
+ return {
6
+ name: 'experiments',
7
+ components: {
8
+ [EXPERIMENT_SLOT_TYPE]: createExperimentSlot({
9
+ routes
10
+ })
11
+ },
12
+ categories: {
13
+ experiments: {
14
+ title: categoryTitle,
15
+ components: [
16
+ EXPERIMENT_SLOT_TYPE
17
+ ]
18
+ }
19
+ }
20
+ };
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { TextField } from '@puckeditor/core';
2
+ /** Prop config only. Components render xpGoal as data-xp-goal. */
3
+ export declare function xpGoalPuckField(): TextField;
@@ -0,0 +1,6 @@
1
+ /** Prop config only. Components render xpGoal as data-xp-goal. */ export function xpGoalPuckField() {
2
+ return {
3
+ type: 'text',
4
+ label: 'Click goal'
5
+ };
6
+ }
@@ -0,0 +1,4 @@
1
+ import type { Snapshot, SnapshotHealth } from '../types/snapshot.js';
2
+ /** Refresh page health while retaining health produced by other checks. */
3
+ export declare function mergePageHealth(previous: readonly SnapshotHealth[], pages: readonly SnapshotHealth[]): SnapshotHealth[];
4
+ export declare function canonicalHealth(snapshot: Pick<Snapshot, 'health' | 'goalFairnessNotice'>, ignoreUncheckedPlacements?: boolean): string;
@@ -0,0 +1,27 @@
1
+ const healthKey = (row)=>JSON.stringify(row.code === 'placement-unchecked' ? [
2
+ row.code,
3
+ row.detail.collection ?? '',
4
+ row.detail.id ?? '',
5
+ row.detail.path ?? ''
6
+ ] : [
7
+ row.code,
8
+ 'path' in row ? row.path : '',
9
+ 'metricId' in row ? row.metricId : ''
10
+ ]);
11
+ /** Refresh page health while retaining health produced by other checks. */ export function mergePageHealth(previous, pages) {
12
+ return [
13
+ ...new Map([
14
+ ...previous.filter((row)=>row.code === 'goal-in-some-versions' || row.code === 'placement-unchecked'),
15
+ ...pages
16
+ ].map((row)=>[
17
+ healthKey(row),
18
+ row
19
+ ])).values()
20
+ ];
21
+ }
22
+ export function canonicalHealth(snapshot, ignoreUncheckedPlacements = false) {
23
+ return JSON.stringify([
24
+ snapshot.goalFairnessNotice ?? '',
25
+ ...(snapshot.health ?? []).filter((row)=>!ignoreUncheckedPlacements || row.code !== 'placement-unchecked').map(healthKey).sort()
26
+ ]);
27
+ }
@@ -7,6 +7,7 @@ const count = (value)=>number(value) && Number.isInteger(value) && value >= 0;
7
7
  const string = (value)=>typeof value === 'string';
8
8
  const boolean = (value)=>typeof value === 'boolean';
9
9
  const nullable = (check)=>(value)=>value === null || check(value);
10
+ const union = (...checks)=>(value)=>checks.some((check)=>check(value));
10
11
  const list = (check)=>(value)=>Array.isArray(value) && value.every(check);
11
12
  const oneOf = (...values)=>(value)=>values.includes(value);
12
13
  const isObject = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -140,10 +141,22 @@ const shape = object({
140
141
  by: nullable(id)
141
142
  }))
142
143
  }, {
143
- health: list(object({
144
+ goalFairnessNotice: string,
145
+ health: list(union(object({
144
146
  code: oneOf('version-page-unavailable', 'version-page-unchecked'),
145
147
  path: string
146
- }))
148
+ }), object({
149
+ code: oneOf('goal-in-some-versions'),
150
+ metricId: id,
151
+ name: string
152
+ }), object({
153
+ code: oneOf('placement-unchecked'),
154
+ detail: object({}, {
155
+ collection: string,
156
+ id,
157
+ path: string
158
+ })
159
+ })))
147
160
  });
148
161
  /** Validate the frozen JSON shape and headline/rollout invariants before storage.
149
162
  * This also accepts the shared CORE-13/admin fixtures without any I/O.
@@ -16,6 +16,47 @@ const failure = (value)=>value === 'placements-failed' || value === 'forbidden'
16
16
  * counts and layer capacity, which are not identifiers).
17
17
  */ export function sanitizeReadinessDetail(code, detail) {
18
18
  switch(code){
19
+ case 'goal-in-some-versions':
20
+ {
21
+ const rows = Array.isArray(detail) ? detail : [
22
+ detail
23
+ ];
24
+ return rows.filter(object).filter((row)=>key(row.metricId) || typeof row.metricId === 'number' && Number.isSafeInteger(row.metricId)).map((row)=>({
25
+ metricId: row.metricId
26
+ }));
27
+ }
28
+ case 'placement-unchecked':
29
+ return object(detail) ? Object.fromEntries(Object.entries(detail).filter(([k, v])=>k === 'path' ? path(v) : [
30
+ 'collection',
31
+ 'id'
32
+ ].includes(k) && (key(v) || typeof v === 'number' && Number.isSafeInteger(v)))) : undefined;
33
+ case 'placement-unpublished':
34
+ case 'duplicate-version-row':
35
+ case 'interacting-tests':
36
+ return Array.isArray(detail) ? detail.filter(object).map((row)=>Object.fromEntries(Object.entries(row).filter(([k, v])=>[
37
+ 'collection',
38
+ 'id',
39
+ 'experimentId',
40
+ 'slug'
41
+ ].includes(k) ? key(v) || typeof v === 'number' && Number.isSafeInteger(v) : k === 'path' ? path(v) : k === 'status' ? [
42
+ 'running',
43
+ 'decided'
44
+ ].includes(String(v)) : k === 'version' && typeof v === 'boolean'))) : undefined;
45
+ case 'version-same-address':
46
+ return object(detail) && arm(detail.arm) && arm(detail.otherArm) && path(detail.path) ? {
47
+ arm: detail.arm,
48
+ otherArm: detail.otherArm,
49
+ path: detail.path
50
+ } : undefined;
51
+ case 'version-identical':
52
+ return object(detail) && arm(detail.arm) && key(detail.collection) && (key(detail.id) || typeof detail.id === 'number' && Number.isSafeInteger(detail.id)) ? {
53
+ arm: detail.arm,
54
+ collection: detail.collection,
55
+ id: detail.id,
56
+ ...path(detail.path) ? {
57
+ path: detail.path
58
+ } : {}
59
+ } : undefined;
19
60
  case 'block-path-rule':
20
61
  return object(detail) && Array.isArray(detail.rules) && detail.rules.length && detail.rules.every((index)=>Number.isSafeInteger(index) && index >= 0) ? {
21
62
  rules: detail.rules
@@ -0,0 +1,27 @@
1
+ import type { Manifest } from '../types/manifest.js';
2
+ export interface PageKindSeoContractOptions {
3
+ secret: string;
4
+ original: string;
5
+ versions: string[];
6
+ state: 'running' | 'decided' | 'archived';
7
+ winner?: string;
8
+ }
9
+ export interface PageKindSeoContractRow {
10
+ requestPath: string;
11
+ servedPath: string;
12
+ headers: {
13
+ 'x-xp-request': string;
14
+ };
15
+ expect: {
16
+ robots?: {
17
+ index: false;
18
+ };
19
+ canonical?: string;
20
+ inSitemap: boolean;
21
+ };
22
+ }
23
+ /** Host-test fixture: use this manifest in the runtime that serves the signed row headers. */
24
+ export declare function pageKindSeoContract({ secret, original, versions, state, winner }: PageKindSeoContractOptions): {
25
+ manifest: Manifest;
26
+ rows: PageKindSeoContractRow[];
27
+ };
@@ -0,0 +1,80 @@
1
+ import { sign } from '../runtime/signing.js';
2
+ import { normalisePath } from '../runtime/normalisePath.js';
3
+ /** Host-test fixture: use this manifest in the runtime that serves the signed row headers. */ export function pageKindSeoContract({ secret, original, versions, state, winner }) {
4
+ const paths = [
5
+ original,
6
+ ...versions
7
+ ];
8
+ if (!secret || paths.some((path)=>!path.startsWith('/') || /[\\\r\n#|]/.test(path)) || new Set(paths.map(normalisePath)).size !== paths.length || !versions.length || versions.length > 3 || state !== 'running' && (!winner || !paths.includes(winner))) throw new Error('Invalid page SEO contract');
9
+ const manifest = {
10
+ v: 1,
11
+ revision: 1,
12
+ experiments: [],
13
+ flagDefaults: {},
14
+ archivedWinners: {},
15
+ archivedPages: {}
16
+ };
17
+ if (state === 'archived') {
18
+ for (const path of paths)if (path !== winner) manifest.archivedPages[path] = {
19
+ slug: 'page-seo-contract',
20
+ winnerPath: winner
21
+ };
22
+ } else {
23
+ manifest.experiments.push({
24
+ id: 'page-seo-contract',
25
+ slug: 'page-seo-contract',
26
+ kind: 'page',
27
+ unitType: 'visitor',
28
+ status: state,
29
+ phase: 1,
30
+ phaseKind: 'test',
31
+ seed: 'page-seo-contract|',
32
+ coverage: 1,
33
+ targeting: [],
34
+ exposurePolicy: 'pageview',
35
+ deferrable: false,
36
+ arms: paths.map((path, index)=>({
37
+ key: index === 0 ? 'control' : `variant${String.fromCharCode(64 + index)}`,
38
+ kind: index === 0 ? 'control' : 'variant',
39
+ weight: index === paths.length - 1 ? 100 - Math.floor(100 / paths.length) * index : Math.floor(100 / paths.length),
40
+ pageTarget: path
41
+ })),
42
+ ...winner ? {
43
+ winnerArm: paths.indexOf(winner) === 0 ? 'control' : `variant${String.fromCharCode(64 + paths.indexOf(winner))}`
44
+ } : {}
45
+ });
46
+ }
47
+ const rows = [];
48
+ for (const requestPath of paths){
49
+ const servedPaths = state === 'running' ? paths : [
50
+ winner
51
+ ];
52
+ for (const servedPath of servedPaths){
53
+ const canonical = state === 'archived' ? requestPath === winner ? undefined : winner : original;
54
+ rows.push({
55
+ requestPath,
56
+ servedPath,
57
+ headers: {
58
+ 'x-xp-request': `GET|${requestPath}|c1|${sign(`xp:request:GET|${requestPath}|c1`, [
59
+ secret
60
+ ])}`
61
+ },
62
+ expect: {
63
+ ...state !== 'archived' && requestPath !== original ? {
64
+ robots: {
65
+ index: false
66
+ }
67
+ } : {},
68
+ ...canonical ? {
69
+ canonical
70
+ } : {},
71
+ inSitemap: state === 'archived' ? requestPath === winner : requestPath === original
72
+ }
73
+ });
74
+ }
75
+ }
76
+ return {
77
+ manifest,
78
+ rows
79
+ };
80
+ }