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

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 (299) hide show
  1. package/README.md +122 -27
  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/TestsDashboardCards.d.ts +1 -0
  8. package/dist/admin/TestsDashboardCards.js +4 -8
  9. package/dist/admin/TestsNavLink.d.ts +1 -0
  10. package/dist/admin/TestsNavLink.js +4 -10
  11. package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
  12. package/dist/admin/actions/ConfirmDialog.js +2 -2
  13. package/dist/admin/actions/DecideDialog.d.ts +2 -1
  14. package/dist/admin/actions/DecideDialog.js +2 -2
  15. package/dist/admin/api.d.ts +18 -2
  16. package/dist/admin/api.js +47 -2
  17. package/dist/admin/blocks/BlockPanel.d.ts +4 -0
  18. package/dist/admin/blocks/BlockPanel.js +199 -0
  19. package/dist/admin/blocks/TestBlockField.d.ts +5 -0
  20. package/dist/admin/blocks/TestBlockField.js +163 -0
  21. package/dist/admin/blocks/formApi.d.ts +46 -0
  22. package/dist/admin/blocks/formApi.js +159 -0
  23. package/dist/admin/blocks/inject.d.ts +7 -0
  24. package/dist/admin/blocks/inject.js +41 -0
  25. package/dist/admin/blocks/model.d.ts +17 -0
  26. package/dist/admin/blocks/model.js +45 -0
  27. package/dist/admin/blocks/statusCache.d.ts +9 -0
  28. package/dist/admin/blocks/statusCache.js +92 -0
  29. package/dist/admin/blocks/useBlockForm.d.ts +17 -0
  30. package/dist/admin/blocks/useBlockForm.js +46 -0
  31. package/dist/admin/copy/actions.d.ts +2 -0
  32. package/dist/admin/copy/actions.js +9 -0
  33. package/dist/admin/copy/blocks.d.ts +43 -0
  34. package/dist/admin/copy/blocks.js +64 -0
  35. package/dist/admin/copy/common.d.ts +1 -0
  36. package/dist/admin/copy/common.js +1 -1
  37. package/dist/admin/copy/goals.d.ts +2 -0
  38. package/dist/admin/copy/goals.js +4 -1
  39. package/dist/admin/copy/newTest.d.ts +10 -0
  40. package/dist/admin/copy/newTest.js +10 -0
  41. package/dist/admin/copy/pages.d.ts +24 -0
  42. package/dist/admin/copy/pages.js +24 -0
  43. package/dist/admin/copy/readiness.d.ts +23 -3
  44. package/dist/admin/copy/readiness.js +24 -3
  45. package/dist/admin/copy/results.d.ts +1 -0
  46. package/dist/admin/copy/results.js +1 -0
  47. package/dist/admin/copy/testPage.d.ts +4 -0
  48. package/dist/admin/copy/testPage.js +4 -0
  49. package/dist/admin/goals/XpGoalField.d.ts +4 -0
  50. package/dist/admin/goals/XpGoalField.js +168 -0
  51. package/dist/admin/goals/xpGoalField.css +48 -0
  52. package/dist/admin/metricSource.model.d.ts +2 -0
  53. package/dist/admin/metricSource.model.js +4 -0
  54. package/dist/admin/pageCollections.d.ts +10 -0
  55. package/dist/admin/pageCollections.js +20 -0
  56. package/dist/admin/paths.d.ts +9 -0
  57. package/dist/admin/paths.js +9 -0
  58. package/dist/admin/placementStatus.model.d.ts +2 -0
  59. package/dist/admin/placementStatus.model.js +19 -0
  60. package/dist/admin/placements/placementLabels.d.ts +11 -0
  61. package/dist/admin/placements/placementLabels.js +21 -0
  62. package/dist/admin/placements/placementLabels.model.d.ts +13 -0
  63. package/dist/admin/placements/placementLabels.model.js +12 -0
  64. package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
  65. package/dist/admin/placements/usePlacementLabels.js +102 -0
  66. package/dist/admin/results/DataQualityPanel.js +4 -1
  67. package/dist/admin/results/results.model.d.ts +3 -0
  68. package/dist/admin/results/results.model.js +8 -4
  69. package/dist/admin/surface/TestsHome.js +39 -10
  70. package/dist/admin/surface/TestsSurface.js +7 -6
  71. package/dist/admin/surface/contracts.d.ts +6 -5
  72. package/dist/admin/surface/goals/GoalForm.js +71 -44
  73. package/dist/admin/surface/goals/GoalsList.js +5 -4
  74. package/dist/admin/surface/goals/data.js +4 -1
  75. package/dist/admin/surface/goals/goals.model.d.ts +1 -2
  76. package/dist/admin/surface/goals/goals.model.js +1 -3
  77. package/dist/admin/surface/home.model.d.ts +3 -3
  78. package/dist/admin/surface/home.model.js +8 -4
  79. package/dist/admin/surface/newTest/NewTest.js +56 -17
  80. package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
  81. package/dist/admin/surface/newTest/PageWizard.js +214 -0
  82. package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
  83. package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
  84. package/dist/admin/surface/pagePicker/PageCombobox.d.ts +10 -0
  85. package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
  86. package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
  87. package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +27 -0
  88. package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
  89. package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
  90. package/dist/admin/surface/readiness/Banner.js +30 -6
  91. package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
  92. package/dist/admin/surface/readiness/readiness.model.js +92 -9
  93. package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
  94. package/dist/admin/surface/results/ReleasePages.js +3 -7
  95. package/dist/admin/surface/results/ResultsBody.js +21 -2
  96. package/dist/admin/surface/results/ResultsHeader.js +5 -13
  97. package/dist/admin/surface/results/useResults.d.ts +20 -1
  98. package/dist/admin/surface/test/AdvancedSection.js +111 -104
  99. package/dist/admin/surface/test/GoalSection.js +94 -14
  100. package/dist/admin/surface/test/LengthSection.js +1 -1
  101. package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
  102. package/dist/admin/surface/test/PageHoldout.js +67 -0
  103. package/dist/admin/surface/test/TestPage.js +5 -9
  104. package/dist/admin/surface/test/VersionsSection.js +12 -5
  105. package/dist/admin/surface/test/fields.d.ts +13 -4
  106. package/dist/admin/surface/test/fields.js +46 -14
  107. package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
  108. package/dist/admin/surface/test/jumpToSection.js +4 -3
  109. package/dist/admin/surface/test/pageParts.d.ts +4 -1
  110. package/dist/admin/surface/test/pageParts.js +49 -1
  111. package/dist/admin/surface/test/testPage.css +3 -1
  112. package/dist/admin/surface/testDocument.model.d.ts +1 -0
  113. package/dist/admin/surface/testDocument.model.js +2 -1
  114. package/dist/admin/surface/useTestDocument.d.ts +15 -2
  115. package/dist/admin/surface/useTestDocument.js +53 -15
  116. package/dist/admin/surfaceUrls.d.ts +7 -0
  117. package/dist/admin/surfaceUrls.js +19 -0
  118. package/dist/admin/theme.css +2 -0
  119. package/dist/admin/useApi.d.ts +21 -2
  120. package/dist/admin/useApi.js +3 -5
  121. package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
  122. package/dist/admin/views/CollectionListRedirect.js +1 -1
  123. package/dist/admin/views/ExperimentsRootView.js +28 -133
  124. package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
  125. package/dist/admin/views/GoalEditRedirect.js +14 -0
  126. package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
  127. package/dist/admin/views/GoalsCollectionListView.js +4 -0
  128. package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
  129. package/dist/admin/views/TestsCollectionListView.js +4 -0
  130. package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
  131. package/dist/admin/views/TestsNavLinkServer.js +1 -1
  132. package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
  133. package/dist/admin/views/buildSurfaceProps.js +143 -0
  134. package/dist/admin/views/renderCollectionList.d.ts +2 -0
  135. package/dist/admin/views/renderCollectionList.js +34 -0
  136. package/dist/admin/views/renderTestsSurface.d.ts +3 -0
  137. package/dist/admin/views/renderTestsSurface.js +34 -0
  138. package/dist/blocks/bindings.d.ts +3 -0
  139. package/dist/blocks/bindings.js +34 -0
  140. package/dist/blocks/createExperimentBlock.d.ts +6 -0
  141. package/dist/blocks/createExperimentBlock.js +77 -0
  142. package/dist/blocks/formState.d.ts +31 -0
  143. package/dist/blocks/formState.js +146 -0
  144. package/dist/blocks/model.d.ts +41 -0
  145. package/dist/blocks/model.js +34 -0
  146. package/dist/client/ExperimentGroup.d.ts +6 -0
  147. package/dist/client/ExperimentGroup.js +9 -0
  148. package/dist/client/ExperimentsProvider.js +20 -4
  149. package/dist/client/clickGoals.d.ts +4 -0
  150. package/dist/client/clickGoals.js +43 -0
  151. package/dist/collections/experiments/config.js +21 -7
  152. package/dist/collections/experiments/deleteGuard.d.ts +2 -1
  153. package/dist/collections/experiments/deleteGuard.js +12 -1
  154. package/dist/collections/experiments/editability.js +4 -2
  155. package/dist/collections/experiments/pickSlug.d.ts +2 -0
  156. package/dist/collections/experiments/pickSlug.js +8 -0
  157. package/dist/collections/labels.d.ts +3 -0
  158. package/dist/collections/labels.js +20 -0
  159. package/dist/collections/metrics.js +28 -6
  160. package/dist/collections/phases.js +1 -0
  161. package/dist/collections/serialize.js +7 -0
  162. package/dist/db/clickGoals.d.ts +9 -0
  163. package/dist/db/clickGoals.js +22 -0
  164. package/dist/db/delete.d.ts +1 -0
  165. package/dist/db/delete.js +1 -0
  166. package/dist/db/locks.d.ts +8 -0
  167. package/dist/db/locks.js +27 -4
  168. package/dist/db/names.d.ts +4 -0
  169. package/dist/db/names.js +4 -0
  170. package/dist/db/pageBindings.d.ts +4 -0
  171. package/dist/db/pageBindings.js +17 -0
  172. package/dist/db/pageBusy.d.ts +2 -0
  173. package/dist/db/pageBusy.js +10 -0
  174. package/dist/db/pageContent.d.ts +10 -0
  175. package/dist/db/pageContent.js +79 -0
  176. package/dist/db/placements.d.ts +33 -0
  177. package/dist/db/placements.js +54 -0
  178. package/dist/db/slugs.d.ts +8 -0
  179. package/dist/db/slugs.js +40 -0
  180. package/dist/db/snapshots.d.ts +6 -0
  181. package/dist/db/snapshots.js +8 -5
  182. package/dist/endpoints/clickKeys.d.ts +5 -0
  183. package/dist/endpoints/clickKeys.js +17 -0
  184. package/dist/endpoints/events.js +4 -1
  185. package/dist/endpoints/index.js +28 -0
  186. package/dist/endpoints/pages.d.ts +5 -0
  187. package/dist/endpoints/pages.js +120 -0
  188. package/dist/endpoints/placementStatus.d.ts +6 -0
  189. package/dist/endpoints/placementStatus.js +238 -0
  190. package/dist/endpoints/readBody.d.ts +2 -0
  191. package/dist/endpoints/readBody.js +24 -0
  192. package/dist/endpoints/results.js +5 -1
  193. package/dist/endpoints/summary.js +11 -4
  194. package/dist/endpoints/wrapBlock.d.ts +6 -0
  195. package/dist/endpoints/wrapBlock.js +76 -0
  196. package/dist/errors.d.ts +3 -3
  197. package/dist/errors.js +4 -4
  198. package/dist/events/rows.d.ts +1 -1
  199. package/dist/events/rows.js +5 -3
  200. package/dist/exports/adminClient.d.ts +5 -0
  201. package/dist/exports/adminClient.js +5 -0
  202. package/dist/exports/adminRsc.d.ts +3 -0
  203. package/dist/exports/adminRsc.js +3 -0
  204. package/dist/exports/blocks.d.ts +2 -0
  205. package/dist/exports/blocks.js +1 -0
  206. package/dist/exports/client.d.ts +1 -0
  207. package/dist/exports/client.js +1 -0
  208. package/dist/exports/index.d.ts +6 -1
  209. package/dist/exports/index.js +3 -0
  210. package/dist/exports/puck.d.ts +3 -1
  211. package/dist/exports/puck.js +2 -0
  212. package/dist/exports/puckEditor.d.ts +3 -0
  213. package/dist/exports/puckEditor.js +2 -0
  214. package/dist/exports/rsc.d.ts +3 -0
  215. package/dist/exports/rsc.js +2 -0
  216. package/dist/fields/xpGoal.d.ts +3 -0
  217. package/dist/fields/xpGoal.js +16 -0
  218. package/dist/globals/state.js +10 -0
  219. package/dist/goals/cache.d.ts +3 -0
  220. package/dist/goals/cache.js +35 -0
  221. package/dist/goals/fairness.d.ts +40 -0
  222. package/dist/goals/fairness.js +200 -0
  223. package/dist/goals/key.d.ts +4 -0
  224. package/dist/goals/key.js +3 -0
  225. package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
  226. package/dist/goals/picker/clickGoalPicker.model.js +45 -0
  227. package/dist/goals/picker/copy.d.ts +16 -0
  228. package/dist/goals/picker/copy.js +15 -0
  229. package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
  230. package/dist/goals/picker/useClickGoalPicker.js +220 -0
  231. package/dist/goals/validate.d.ts +3 -0
  232. package/dist/goals/validate.js +14 -0
  233. package/dist/hooks/pageGuard.d.ts +5 -1
  234. package/dist/hooks/pageGuard.js +157 -95
  235. package/dist/hooks/puckGoals.d.ts +3 -0
  236. package/dist/hooks/puckGoals.js +38 -0
  237. package/dist/onInit.js +29 -0
  238. package/dist/options.d.ts +2 -0
  239. package/dist/options.js +41 -1
  240. package/dist/plugin.js +50 -43
  241. package/dist/puck/copy.d.ts +1 -0
  242. package/dist/puck/copy.js +1 -0
  243. package/dist/puck/editor/editor.chrome.css +14 -0
  244. package/dist/puck/editor/editor.css +106 -0
  245. package/dist/puck/editor/editor.model.d.ts +4 -2
  246. package/dist/puck/editor/editor.model.js +6 -3
  247. package/dist/puck/editor/integration.d.ts +10 -0
  248. package/dist/puck/editor/integration.js +30 -0
  249. package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
  250. package/dist/puck/editor/xpGoalPuckEditorField.js +142 -0
  251. package/dist/puck/integration.d.ts +6 -0
  252. package/dist/puck/integration.js +21 -0
  253. package/dist/puck/xpGoalField.d.ts +3 -0
  254. package/dist/puck/xpGoalField.js +6 -0
  255. package/dist/results/health.d.ts +4 -0
  256. package/dist/results/health.js +27 -0
  257. package/dist/results/snapshot.js +15 -2
  258. package/dist/sanitizeReadinessDetail.js +43 -0
  259. package/dist/server/pageKindSeoContract.d.ts +27 -0
  260. package/dist/server/pageKindSeoContract.js +80 -0
  261. package/dist/server/recordContentChange.js +58 -8
  262. package/dist/server/resolveExperimentBlocks.d.ts +5 -0
  263. package/dist/server/resolveExperimentBlocks.js +54 -0
  264. package/dist/server/runResults.js +56 -15
  265. package/dist/server/toClientAssignments.js +2 -1
  266. package/dist/transitions/addMetric.d.ts +2 -1
  267. package/dist/transitions/addMetric.js +28 -1
  268. package/dist/transitions/checkStart.d.ts +14 -1
  269. package/dist/transitions/checkStart.js +92 -8
  270. package/dist/transitions/decide.js +25 -0
  271. package/dist/transitions/deleteExperiment.d.ts +3 -1
  272. package/dist/transitions/deleteExperiment.js +27 -1
  273. package/dist/transitions/kernel.js +4 -0
  274. package/dist/transitions/makeContent.d.ts +5 -0
  275. package/dist/transitions/makeContent.js +39 -11
  276. package/dist/transitions/pageCollection.d.ts +10 -0
  277. package/dist/transitions/pageCollection.js +20 -0
  278. package/dist/transitions/pageVersions.d.ts +10 -0
  279. package/dist/transitions/pageVersions.js +44 -0
  280. package/dist/transitions/phaseRecord.d.ts +1 -0
  281. package/dist/transitions/placementInteractions.d.ts +40 -0
  282. package/dist/transitions/placementInteractions.js +94 -0
  283. package/dist/transitions/placements.d.ts +8 -0
  284. package/dist/transitions/placements.js +148 -1
  285. package/dist/transitions/preflightData.d.ts +2 -2
  286. package/dist/transitions/preflightData.js +137 -5
  287. package/dist/transitions/resolveCaps.d.ts +1 -0
  288. package/dist/transitions/resolveCaps.js +6 -2
  289. package/dist/transitions/start.js +46 -1
  290. package/dist/types/admin.d.ts +1 -1
  291. package/dist/types/assignments.d.ts +1 -0
  292. package/dist/types/contracts.d.ts +15 -0
  293. package/dist/types/endpoints.d.ts +92 -5
  294. package/dist/types/index.d.ts +3 -0
  295. package/dist/types/puck.d.ts +7 -1
  296. package/dist/types/snapshot.d.ts +17 -4
  297. package/dist/types/surface.d.ts +13 -0
  298. package/dist/ui/ui.css +4 -0
  299. package/package.json +8 -1
@@ -5,6 +5,7 @@ export declare const LOCK_NAMESPACES: {
5
5
  readonly flagPoint: "xp:flagpoint";
6
6
  readonly experiment: "xp:experiment";
7
7
  readonly pagePath: "xp:pagepath";
8
+ readonly slug: "xp:slug";
8
9
  };
9
10
  /** Transaction executor only. Call in layer → flag point → experiment order;
10
11
  * the manifest builder takes the state lock last, after transition writes.
@@ -19,3 +20,10 @@ export declare function lockTransitionScope(exec: Executor, scope: {
19
20
  * when different tests share several paths in different arm orders.
20
21
  */
21
22
  export declare function lockPagePaths(exec: Executor, paths: readonly string[]): Promise<void>;
23
+ /** Locale-independent ordering for every multi-key advisory lock acquisition. */
24
+ export declare function sortLockKeys(keys: readonly string[]): string[];
25
+ export declare function lockSlug(exec: Executor, slug: string): Promise<void>;
26
+ /** Guard takes the audit's entire E set first (KI-6), then its own path locks.
27
+ * SET LOCAL reverts on abort. Restore explicitly only after successful work.
28
+ */
29
+ export declare function lockPlacementScope(exec: Executor, experiments: readonly string[], paths: readonly string[]): Promise<void>;
package/dist/db/locks.js CHANGED
@@ -2,11 +2,13 @@ export const LOCK_NAMESPACES = {
2
2
  layer: 'xp:layer',
3
3
  flagPoint: 'xp:flagpoint',
4
4
  experiment: 'xp:experiment',
5
- pagePath: 'xp:pagepath'
5
+ pagePath: 'xp:pagepath',
6
+ slug: 'xp:slug'
6
7
  };
7
8
  const LOCK_STATEMENTS = {
8
9
  layer: `SELECT pg_advisory_xact_lock(hashtext('${LOCK_NAMESPACES.layer}'), hashtext($1))`,
9
10
  flagPoint: `SELECT pg_advisory_xact_lock(hashtext('${LOCK_NAMESPACES.flagPoint}'), hashtext($1))`,
11
+ slug: `SELECT pg_advisory_xact_lock(hashtext('${LOCK_NAMESPACES.slug}'), hashtext($1))`,
10
12
  pagePath: `SELECT pg_advisory_xact_lock(hashtext('${LOCK_NAMESPACES.pagePath}'), hashtext($1))`,
11
13
  experiment: `SELECT pg_advisory_xact_lock(hashtext('${LOCK_NAMESPACES.experiment}'), hashtext($1))`
12
14
  };
@@ -27,9 +29,30 @@ const LOCK_STATEMENTS = {
27
29
  * already be normalised with the runtime's rules. Sorted keys avoid deadlocks
28
30
  * when different tests share several paths in different arm orders.
29
31
  */ export async function lockPagePaths(exec, paths) {
30
- for (const path of [
31
- ...new Set(paths)
32
- ].sort())await exec.query(LOCK_STATEMENTS.pagePath, [
32
+ for (const path of sortLockKeys(paths))await exec.query(LOCK_STATEMENTS.pagePath, [
33
33
  path
34
34
  ]);
35
35
  }
36
+ /** Locale-independent ordering for every multi-key advisory lock acquisition. */ export function sortLockKeys(keys) {
37
+ return [
38
+ ...new Set(keys)
39
+ ].sort();
40
+ }
41
+ export async function lockSlug(exec, slug) {
42
+ await exec.query(LOCK_STATEMENTS.slug, [
43
+ slug
44
+ ]);
45
+ }
46
+ /** Guard takes the audit's entire E set first (KI-6), then its own path locks.
47
+ * SET LOCAL reverts on abort. Restore explicitly only after successful work.
48
+ */ export async function lockPlacementScope(exec, experiments, paths) {
49
+ const { rows } = await exec.query("SELECT current_setting('lock_timeout') AS timeout");
50
+ await exec.query("SELECT set_config('lock_timeout', '5s', true)");
51
+ for (const id of sortLockKeys(experiments))await lockTransitionScope(exec, {
52
+ experimentId: id
53
+ });
54
+ await lockPagePaths(exec, paths);
55
+ await exec.query("SELECT set_config('lock_timeout', $1, true)", [
56
+ rows[0].timeout
57
+ ]);
58
+ }
@@ -25,6 +25,7 @@ export declare const COLUMNS: {
25
25
  readonly revision: "revision";
26
26
  readonly manifest: "manifest";
27
27
  readonly flagDefaults: "flag_defaults";
28
+ readonly retiredSlugs: "retired_slugs";
28
29
  readonly updatedAt: "updated_at";
29
30
  readonly createdAt: "created_at";
30
31
  };
@@ -55,6 +56,7 @@ export declare const COLUMNS: {
55
56
  readonly slug: "slug";
56
57
  readonly hypothesis: "hypothesis";
57
58
  readonly kind: "kind";
59
+ readonly pageCollection: "page_collection";
58
60
  readonly flagPoint: "flag_point";
59
61
  readonly exposurePolicy: "exposure_policy";
60
62
  readonly unitType: "unit_type";
@@ -125,6 +127,7 @@ export declare const COLUMNS: {
125
127
  readonly versionSlug: "version_slug";
126
128
  readonly versionHypothesis: "version_hypothesis";
127
129
  readonly versionKind: "version_kind";
130
+ readonly versionPageCollection: "version_page_collection";
128
131
  readonly versionFlagPoint: "version_flag_point";
129
132
  readonly versionExposurePolicy: "version_exposure_policy";
130
133
  readonly versionUnitType: "version_unit_type";
@@ -181,6 +184,7 @@ export declare const COLUMNS: {
181
184
  readonly targeting: "targeting";
182
185
  readonly metricDefinitions: "metric_definitions";
183
186
  readonly analysis: "analysis";
187
+ readonly placementDocs: "placement_docs";
184
188
  readonly contentChanges: "content_changes";
185
189
  readonly updatedAt: "updated_at";
186
190
  readonly createdAt: "created_at";
package/dist/db/names.js CHANGED
@@ -24,6 +24,7 @@ export const COLUMNS = {
24
24
  revision: 'revision',
25
25
  manifest: 'manifest',
26
26
  flagDefaults: 'flag_defaults',
27
+ retiredSlugs: 'retired_slugs',
27
28
  updatedAt: 'updated_at',
28
29
  createdAt: 'created_at'
29
30
  },
@@ -54,6 +55,7 @@ export const COLUMNS = {
54
55
  slug: 'slug',
55
56
  hypothesis: 'hypothesis',
56
57
  kind: 'kind',
58
+ pageCollection: 'page_collection',
57
59
  flagPoint: 'flag_point',
58
60
  exposurePolicy: 'exposure_policy',
59
61
  unitType: 'unit_type',
@@ -124,6 +126,7 @@ export const COLUMNS = {
124
126
  versionSlug: 'version_slug',
125
127
  versionHypothesis: 'version_hypothesis',
126
128
  versionKind: 'version_kind',
129
+ versionPageCollection: 'version_page_collection',
127
130
  versionFlagPoint: 'version_flag_point',
128
131
  versionExposurePolicy: 'version_exposure_policy',
129
132
  versionUnitType: 'version_unit_type',
@@ -180,6 +183,7 @@ export const COLUMNS = {
180
183
  targeting: 'targeting',
181
184
  metricDefinitions: 'metric_definitions',
182
185
  analysis: 'analysis',
186
+ placementDocs: 'placement_docs',
183
187
  contentChanges: 'content_changes',
184
188
  updatedAt: 'updated_at',
185
189
  createdAt: 'created_at'
@@ -0,0 +1,4 @@
1
+ import type { DocId } from '../types/contracts.js';
2
+ import type { Executor } from './executor.js';
3
+ /** Current running round, independent of manifest compilation and typed targeting. */
4
+ export declare function pageContentBindings(exec: Executor, collection: string, paths: readonly string[]): Promise<DocId[]>;
@@ -0,0 +1,17 @@
1
+ import { TABLES, COLUMNS } from './names.js';
2
+ import { qualifiedTable, quoteIdentifier as q } from './sql.js';
3
+ import { normalisePath } from '../runtime/normalisePath.js';
4
+ /** Current running round, independent of manifest compilation and typed targeting. */ // Legacy tests without pageCollection match the same path in any collection; stored collections narrow the match.
5
+ export async function pageContentBindings(exec, collection, paths) {
6
+ if (!paths.length) return [];
7
+ const e = COLUMNS.experiments, p = COLUMNS.phases;
8
+ const { rows } = await exec.query(`SELECT x.${q(e.id)} AS id, p.${q(p.arms)} AS arms
9
+ FROM ${qualifiedTable(exec, TABLES.experiments)} x JOIN ${qualifiedTable(exec, TABLES.phases)} p
10
+ ON p.${q(p.experimentId)} = x.${q(e.id)} AND p.${q(p.phase)} = x.${q(e.phase)}
11
+ WHERE x.${q(e.kind)} = 'page' AND x.${q(e.status)} = 'running'
12
+ AND (x.${q(e.pageCollection)} = $1 OR x.${q(e.pageCollection)} IS NULL)`, [
13
+ collection
14
+ ]);
15
+ const wanted = new Set(paths);
16
+ return rows.filter((row)=>row.arms.some((arm)=>typeof arm.pageTarget === 'string' && wanted.has(normalisePath(arm.pageTarget)))).map((row)=>row.id);
17
+ }
@@ -0,0 +1,2 @@
1
+ /** DrizzleQueryError wraps driver errors; driver SQLSTATE can occur anywhere in cause. */
2
+ export declare function isPageBusy(error: unknown): boolean;
@@ -0,0 +1,10 @@
1
+ /** DrizzleQueryError wraps driver errors; driver SQLSTATE can occur anywhere in cause. */ export function isPageBusy(error) {
2
+ const seen = new Set();
3
+ while(error && typeof error === 'object' && !seen.has(error)){
4
+ seen.add(error);
5
+ const failure = error;
6
+ if (failure.code === '40P01' || failure.code === '55P03') return true;
7
+ error = failure.cause;
8
+ }
9
+ return false;
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { DocId } from '../types/contracts.js';
3
+ /** §7.8: version-identical compares published content raw (ids stripped). */
4
+ export declare function pageVersionContent(req: PayloadRequest, collection: string, id: DocId): Promise<(Record<string, unknown> & {
5
+ id: DocId;
6
+ }) | null>;
7
+ /** §7.8: status computes a page's published (or never-published draft) path. */
8
+ export declare function statusPageContent(req: PayloadRequest, collection: string, id: DocId): Promise<(Record<string, unknown> & {
9
+ id: DocId;
10
+ }) | null>;
@@ -0,0 +1,79 @@
1
+ /** §7.8: version-identical compares published content raw (ids stripped). */ export async function pageVersionContent(req, collection, id) {
2
+ return req.payload.db.findOne({
3
+ collection,
4
+ req,
5
+ where: {
6
+ id: {
7
+ equals: id
8
+ }
9
+ },
10
+ // Adapter read: version-identical compares published content raw (ids stripped).
11
+ overrideAccess: true
12
+ });
13
+ }
14
+ /** §7.8: status computes a page's published (or never-published draft) path. */ export async function statusPageContent(req, collection, id) {
15
+ const doc = await req.payload.db.findOne({
16
+ collection,
17
+ req,
18
+ where: {
19
+ id: {
20
+ equals: id
21
+ }
22
+ },
23
+ // Adapter read: status computes a page's published (or never-published draft) path.
24
+ overrideAccess: true
25
+ });
26
+ if (!doc || doc._status !== 'draft') return doc;
27
+ // A formerly published but currently unpublished page still uses its stored main-row path.
28
+ const published = await req.payload.db.findVersions({
29
+ collection,
30
+ req,
31
+ limit: 1,
32
+ pagination: false,
33
+ where: {
34
+ and: [
35
+ {
36
+ parent: {
37
+ equals: id
38
+ }
39
+ },
40
+ {
41
+ 'version._status': {
42
+ equals: 'published'
43
+ }
44
+ }
45
+ ]
46
+ },
47
+ // Adapter read: status computes a page's published (or never-published draft) path.
48
+ overrideAccess: true
49
+ });
50
+ if (published.docs.length) return doc;
51
+ // §7.8: status computes a page's published (or never-published draft) path.
52
+ const latest = await req.payload.db.findVersions({
53
+ collection,
54
+ req,
55
+ limit: 1,
56
+ pagination: false,
57
+ sort: '-updatedAt',
58
+ where: {
59
+ and: [
60
+ {
61
+ parent: {
62
+ equals: id
63
+ }
64
+ },
65
+ {
66
+ latest: {
67
+ equals: true
68
+ }
69
+ }
70
+ ]
71
+ },
72
+ // Adapter read: status computes a page's published (or never-published draft) path.
73
+ overrideAccess: true
74
+ });
75
+ return latest.docs[0] ? {
76
+ ...latest.docs[0].version,
77
+ id
78
+ } : doc;
79
+ }
@@ -0,0 +1,33 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { Arm, DocId } from '../types/contracts.js';
3
+ import type { Executor } from './executor.js';
4
+ export interface PlacementDoc {
5
+ collection: string;
6
+ id: DocId;
7
+ }
8
+ export interface PlacementTest {
9
+ id: DocId;
10
+ slug: string;
11
+ name: string;
12
+ status: string;
13
+ kind: string;
14
+ pageCollection: string | null;
15
+ phase: number;
16
+ winnerArm?: string | null;
17
+ discardedAt?: string | null;
18
+ pagesReleasedAt?: string | null;
19
+ arms: Arm[];
20
+ placementDocs: PlacementDoc[] | null;
21
+ }
22
+ /** Raw reads only: guard discovery includes every status, interaction checks filter under the locks. */
23
+ export declare function placementTests(exec: Executor): Promise<PlacementTest[]>;
24
+ /** §7.8: interacting-tests computes placement paths from published rows. */
25
+ export declare function placementPathContent(req: PayloadRequest, collection: string, id: DocId): Promise<(Record<string, unknown> & {
26
+ id: DocId;
27
+ }) | null>;
28
+ /** Caller holds E. Never coalesce null: old rounds must continue to scan. */
29
+ export declare function appendPlacementDoc(exec: Executor, experimentId: DocId, phase: number, doc: PlacementDoc): Promise<void>;
30
+ /** Deciding a stopped modern round restores routing on its detached rescan.
31
+ * Legacy null rounds retain scan-on-every-check semantics. Caller holds E.
32
+ */
33
+ export declare function refreshPlacementDocs(exec: Executor, experimentId: DocId, phase: number, docs: readonly PlacementDoc[]): Promise<void>;
@@ -0,0 +1,54 @@
1
+ import { COLUMNS, TABLES } from './names.js';
2
+ import { qualifiedTable, quoteIdentifier as q } from './sql.js';
3
+ /** Raw reads only: guard discovery includes every status, interaction checks filter under the locks. */ export async function placementTests(exec) {
4
+ const e = COLUMNS.experiments, p = COLUMNS.phases;
5
+ const { rows } = await exec.query(`SELECT x.${q(e.id)} AS id, x.${q(e.slug)} AS slug, x.${q(e.name)} AS name,
6
+ x.${q(e.status)} AS status, x.${q(e.kind)} AS kind, x.${q(e.pageCollection)} AS "pageCollection",
7
+ x.${q(e.decisionWinnerArm)} AS "winnerArm", x.${q(e.decisionDiscardedAt)} AS "discardedAt", x.${q(e.decisionPagesReleasedAt)} AS "pagesReleasedAt", p.${q(p.phase)} AS phase, p.${q(p.arms)} AS arms, p.${q(p.placementDocs)} AS "placementDocs"
8
+ FROM ${qualifiedTable(exec, TABLES.experiments)} x
9
+ LEFT JOIN LATERAL (SELECT * FROM ${qualifiedTable(exec, TABLES.phases)} WHERE ${q(p.experimentId)} = x.${q(e.id)}
10
+ AND ${q(p.phaseKind)} = 'test' ORDER BY ${q(p.phase)} DESC LIMIT 1) p ON true`, []);
11
+ return rows.map((row)=>({
12
+ ...row,
13
+ arms: Array.isArray(row.arms) ? row.arms : [],
14
+ placementDocs: row.placementDocs ?? null
15
+ }));
16
+ }
17
+ /** §7.8: interacting-tests computes placement paths from published rows. */ export async function placementPathContent(req, collection, id) {
18
+ return req.payload.db.findOne({
19
+ collection,
20
+ req,
21
+ where: {
22
+ id: {
23
+ equals: id
24
+ }
25
+ },
26
+ // Adapter read: interacting-tests computes placement paths from published rows.
27
+ overrideAccess: true
28
+ });
29
+ }
30
+ /** Caller holds E. Never coalesce null: old rounds must continue to scan. */ export async function appendPlacementDoc(exec, experimentId, phase, doc) {
31
+ const p = COLUMNS.phases, column = q(p.placementDocs);
32
+ await exec.query(`UPDATE ${qualifiedTable(exec, TABLES.phases)} SET ${column} = ${column} || $1::jsonb
33
+ WHERE ${q(p.experimentId)} = $2 AND ${q(p.phase)} = $3 AND ${column} IS NOT NULL
34
+ AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(${column}) entry WHERE entry->>'collection' = $4 AND entry->>'id' = $5)`, [
35
+ JSON.stringify([
36
+ doc
37
+ ]),
38
+ experimentId,
39
+ phase,
40
+ doc.collection,
41
+ String(doc.id)
42
+ ]);
43
+ }
44
+ /** Deciding a stopped modern round restores routing on its detached rescan.
45
+ * Legacy null rounds retain scan-on-every-check semantics. Caller holds E.
46
+ */ export async function refreshPlacementDocs(exec, experimentId, phase, docs) {
47
+ const p = COLUMNS.phases;
48
+ await exec.query(`UPDATE ${qualifiedTable(exec, TABLES.phases)} SET ${q(p.placementDocs)} = $1::jsonb
49
+ WHERE ${q(p.experimentId)} = $2 AND ${q(p.phase)} = $3 AND ${q(p.placementDocs)} IS NOT NULL`, [
50
+ JSON.stringify(docs),
51
+ experimentId,
52
+ phase
53
+ ]);
54
+ }
@@ -0,0 +1,8 @@
1
+ import type { GlobalBeforeChangeHook } from 'payload';
2
+ import { type Executor } from './executor.js';
3
+ /** Each candidate is locked before its live and retired checks, including suffix collisions. */
4
+ export declare function pickSlug(exec: Executor, stem: string, transactional: boolean): Promise<string>;
5
+ /** Delete owns the experiment lock; state is always acquired last. */
6
+ export declare function retireSlug(exec: Executor, slug: string): Promise<void>;
7
+ /** Payload merges the previous global into writes. Refresh the append-only column under its lock. */
8
+ export declare const preserveRetiredSlugs: GlobalBeforeChangeHook;
@@ -0,0 +1,40 @@
1
+ import { pooledExecutor, txExecutor } from './executor.js';
2
+ import { lockSlug } from './locks.js';
3
+ import { COLUMNS, TABLES } from './names.js';
4
+ import { qualifiedTable, quoteIdentifier as q } from './sql.js';
5
+ /** Each candidate is locked before its live and retired checks, including suffix collisions. */ export async function pickSlug(exec, stem, transactional) {
6
+ // Overlapping stems across creates in one host transaction can deadlock; hashtext collisions can wait. The unique index remains the backstop.
7
+ const e = COLUMNS.experiments, s = COLUMNS.state;
8
+ for(let n = 1;; n++){
9
+ const suffix = n === 1 ? '' : `-${n}`;
10
+ const candidate = stem.slice(0, 64 - suffix.length) + suffix;
11
+ if (transactional) await lockSlug(exec, candidate);
12
+ const { rows } = await exec.query(`SELECT EXISTS (SELECT 1 FROM ${qualifiedTable(exec, TABLES.experiments)} WHERE ${q(e.slug)} = $1)
13
+ OR EXISTS (SELECT 1 FROM ${qualifiedTable(exec, TABLES.state)} WHERE coalesce(${q(s.retiredSlugs)}, '[]'::jsonb) ? $1) AS used`, [
14
+ candidate
15
+ ]);
16
+ if (!rows[0]?.used) return candidate;
17
+ }
18
+ }
19
+ /** Delete owns the experiment lock; state is always acquired last. */ export async function retireSlug(exec, slug) {
20
+ await exec.query("SELECT pg_advisory_xact_lock(hashtext('xp:state'), 0)");
21
+ const s = COLUMNS.state, table = qualifiedTable(exec, TABLES.state);
22
+ await exec.query(`INSERT INTO ${table} (${q(s.revision)}, ${q(s.retiredSlugs)}, ${q(s.createdAt)}, ${q(s.updatedAt)})
23
+ SELECT 0, '[]'::jsonb, now(), now() WHERE NOT EXISTS (SELECT 1 FROM ${table})`);
24
+ await exec.query(`UPDATE ${table} SET ${q(s.retiredSlugs)} = coalesce(${q(s.retiredSlugs)}, '[]'::jsonb) || $1::jsonb
25
+ WHERE NOT (coalesce(${q(s.retiredSlugs)}, '[]'::jsonb) ? $2)`, [
26
+ JSON.stringify([
27
+ slug
28
+ ]),
29
+ slug
30
+ ]);
31
+ }
32
+ /** Payload merges the previous global into writes. Refresh the append-only column under its lock. */ export const preserveRetiredSlugs = async ({ req, data })=>{
33
+ const transactional = await req.transactionID != null;
34
+ const exec = transactional ? txExecutor(req) : pooledExecutor(req.payload);
35
+ // A host write with disableTransaction can race a retirement and lose its append; plugin writes always use a transaction.
36
+ if (transactional) await exec.query("SELECT pg_advisory_xact_lock(hashtext('xp:state'), 0)");
37
+ const { rows } = await exec.query(`SELECT ${q(COLUMNS.state.retiredSlugs)} AS retired FROM ${qualifiedTable(exec, TABLES.state)} LIMIT 1`);
38
+ data.retiredSlugs = rows[0]?.retired ?? null;
39
+ return data;
40
+ };
@@ -1,5 +1,6 @@
1
1
  import type { Payload } from 'payload';
2
2
  import type { Arm, DocId, ExperimentKind, Status, UnitType } from '../types/contracts.js';
3
+ import type { PlacementDoc } from './placements.js';
3
4
  import type { Snapshot } from '../types/snapshot.js';
4
5
  import type { AnalysisPhase, FrozenMetric } from '../results/analyse.js';
5
6
  import { type Executor } from './executor.js';
@@ -9,14 +10,19 @@ export interface ResultsPhase extends AnalysisPhase {
9
10
  allocation: number;
10
11
  metricDefinitions: Array<FrozenMetric & {
11
12
  source: string;
13
+ addedAt?: string;
12
14
  keyedBy?: UnitType;
13
15
  capNotice?: 'no-history';
14
16
  }>;
15
17
  }
16
18
  export interface ResultsCandidate {
19
+ slug?: string;
20
+ testPlacementDocs?: PlacementDoc[] | null;
17
21
  phase: ResultsPhase;
18
22
  frozenSnapshot?: Snapshot | null;
23
+ previousHealth?: Snapshot['health'] | null;
19
24
  kind?: ExperimentKind;
25
+ pageCollection?: string | null;
20
26
  currentPhase?: number;
21
27
  winnerArm?: string | null;
22
28
  unitType: UnitType;
@@ -31,21 +31,24 @@ const LATE_BEACON_GRACE_MS = 24 * 60 * 60 * 1000;
31
31
  'startedAt', p.${q(p.startedAt)}, 'endedAt', p.${q(p.endedAt)}, 'seed', p.${q(p.seed)},
32
32
  'arms', p.${q(p.arms)}, 'allocation', p.${q(p.allocation)}, 'metricDefinitions', p.${q(p.metricDefinitions)},
33
33
  'analysis', p.${q(p.analysis)}, 'contentChanges', p.${q(p.contentChanges)}) AS "phase",
34
- e.${q(e.kind)} AS "kind", e.${q(e.phase)} AS "currentPhase", e.${q(e.unitType)} AS "unitType", e.${q(e.status)} AS "status", e.${q(e.decisionWinnerArm)} AS "winnerArm",
35
- latest."computedAt" AS "latestComputedAt",
34
+ e.${q(e.slug)} AS "slug",
35
+ (SELECT t.${q(p.placementDocs)} FROM ${phases} t WHERE t.${q(p.experimentId)} = p.${q(p.experimentId)} AND t.${q(p.phaseKind)} = 'test' AND t.${q(p.phase)} <= p.${q(p.phase)} ORDER BY t.${q(p.phase)} DESC LIMIT 1) AS "testPlacementDocs",
36
+ e.${q(e.pageCollection)} AS "pageCollection", e.${q(e.kind)} AS "kind", e.${q(e.phase)} AS "currentPhase", e.${q(e.unitType)} AS "unitType", e.${q(e.status)} AS "status", e.${q(e.decisionWinnerArm)} AS "winnerArm",
37
+ latest."computedAt" AS "latestComputedAt", latest."health" AS "previousHealth",
36
38
  (SELECT t.${q(p.arms)} FROM ${phases} t WHERE t.${q(p.experimentId)} = p.${q(p.experimentId)} AND t.${q(p.phaseKind)} = 'test' AND t.${q(p.phase)} <= p.${q(p.phase)} ORDER BY t.${q(p.phase)} DESC LIMIT 1) AS "testArms",
37
39
  frozen."snapshot" AS "frozenSnapshot",
38
40
  l.${q(l.slug)} AS "layerSlug", e.${q(e.layerSlices)} AS "layerSlices"
39
41
  FROM ${phases} p JOIN ${qualifiedTable(exec, TABLES.experiments)} e ON e.${q(e.id)} = p.${q(p.experimentId)}
40
42
  LEFT JOIN ${qualifiedTable(exec, TABLES.layers)} l ON l.${q(l.id)} = e.${q(e.layerId)}
41
- LEFT JOIN LATERAL (SELECT max(s.${q(s.computedAt)}) AS "computedAt" FROM ${snapshots} s
42
- WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}) latest ON true
43
+ LEFT JOIN LATERAL (SELECT s.${q(s.computedAt)} AS "computedAt", s.${q(s.snapshot)}->'health' AS "health" FROM ${snapshots} s
44
+ WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}
45
+ ORDER BY s.${q(s.computedAt)} DESC, s.${q(s.id)} DESC LIMIT 1) latest ON true
43
46
  LEFT JOIN LATERAL (SELECT s.${q(s.snapshot)} AS "snapshot" FROM ${snapshots} s
44
47
  WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}
45
48
  AND p.${q(p.endedAt)} IS NOT NULL
46
49
  AND (s.${q(s.computedAt)} > p.${q(p.endedAt)} + $2 * interval '1 millisecond' OR p.${q(p.endedAt)} < $1)
47
50
  ORDER BY s.${q(s.computedAt)} DESC, s.${q(s.id)} DESC LIMIT 1) frozen ON true
48
- WHERE ((e.${q(e.kind)} = 'page'
51
+ WHERE (((e.${q(e.kind)} = 'page' OR EXISTS (SELECT 1 FROM jsonb_array_elements(p.${q(p.metricDefinitions)}) goal WHERE goal->>'source' LIKE 'click:%'))
49
52
  AND (p.${q(p.endedAt)} IS NULL OR p.${q(p.endedAt)} >= $1 OR EXISTS
50
53
  (SELECT 1 FROM ${snapshots} existing WHERE existing.${q(s.experimentId)} = p.${q(p.experimentId)} AND existing.${q(s.phase)} = p.${q(p.phase)}))
51
54
  AND e.${q(e.status)} IN ('running', 'decided') AND (p.${q(p.phase)} = e.${q(e.phase)}
@@ -0,0 +1,5 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { EndpointContext } from '../types/endpoints.js';
3
+ export declare function handleClickKeys<TUser extends {
4
+ id: string | number;
5
+ }>(req: PayloadRequest, { options }: EndpointContext<TUser>): Promise<Response>;
@@ -0,0 +1,17 @@
1
+ import { goalContent } from '../goals/fairness.js';
2
+ import { XpRefusal } from '../errors.js';
3
+ export async function handleClickKeys(req, { options }) {
4
+ const id = req.routeParams?.id;
5
+ if (typeof id !== 'string' && typeof id !== 'number') throw new XpRefusal('invalid-body', 'Choose a test.', undefined, 422);
6
+ const experiment = await req.payload.findByID({
7
+ collection: 'xp-experiments',
8
+ id,
9
+ req,
10
+ overrideAccess: false,
11
+ depth: 0
12
+ });
13
+ const content = await goalContent(req, options, experiment);
14
+ return Response.json({
15
+ keys: content.keys
16
+ });
17
+ }
@@ -1,3 +1,5 @@
1
+ import { clickKeys } from '../goals/cache.js';
2
+ import { validClickSource } from '../goals/key.js';
1
3
  import { safeError } from '../log.js';
2
4
  import { NextRequest } from 'next/server.js';
3
5
  import { DEFAULT_MAX_BODY_BYTES } from '../constants.js';
@@ -83,7 +85,8 @@ export async function handleEvents(req, { options }) {
83
85
  preview: isPreview(request)
84
86
  });
85
87
  if (unresolvedStaff) for (const assignment of Object.values(assignments.bySlug))assignment.exposable = false;
86
- const rows = eventRows(events, manifest, assignments, options, visitorId, userId, new Date().toISOString(), attributes);
88
+ const keys = events.some((event)=>event && typeof event === 'object' && 'eventName' in event && validClickSource(event.eventName)) ? await clickKeys(req.payload) : undefined;
89
+ const rows = eventRows(events, manifest, assignments, options, visitorId, userId, new Date().toISOString(), attributes, keys);
87
90
  // §7.8 events insert / assignment lookup exception: pooled SQL and cached manifest,
88
91
  // no Local API calls (and hence no implicit overrideAccess defaults).
89
92
  if (rows.length) await insertEvents(pooledExecutor(req.payload), rows, (message)=>req.payload.logger.error(message));
@@ -1,3 +1,4 @@
1
+ import { handleClickKeys } from './clickKeys.js';
1
2
  import { createHmac, timingSafeEqual } from 'node:crypto';
2
3
  import { XP_MANIFEST_KEY_HEADER } from '../constants.js';
3
4
  import { XpNotImplemented, XpRefusal } from '../errors.js';
@@ -9,6 +10,9 @@ import { handlePreflight } from './preflight.js';
9
10
  import { handlePreviewToken } from './previewToken.js';
10
11
  import { handleResults } from './results.js';
11
12
  import { handleRecompute } from './recompute.js';
13
+ import { handleWrapBlock } from './wrapBlock.js';
14
+ import { handlePages } from './pages.js';
15
+ import { handlePlacementStatus } from './placementStatus.js';
12
16
  import { handleSummary } from './summary.js';
13
17
  export const ROUTES = [
14
18
  {
@@ -118,6 +122,30 @@ export const ROUTES = [
118
122
  path: '/xp/experiments/summary',
119
123
  access: 'readResults',
120
124
  handler: handleSummary
125
+ },
126
+ {
127
+ method: 'post',
128
+ path: '/xp/blocks/wrap',
129
+ access: 'manage',
130
+ handler: handleWrapBlock
131
+ },
132
+ {
133
+ method: 'get',
134
+ path: '/xp/experiments/:id/click-keys',
135
+ access: 'manage',
136
+ handler: handleClickKeys
137
+ },
138
+ {
139
+ method: 'get',
140
+ path: '/xp/pages',
141
+ access: 'manage',
142
+ handler: handlePages
143
+ },
144
+ {
145
+ method: 'post',
146
+ path: '/xp/placements/status',
147
+ access: 'manage',
148
+ handler: handlePlacementStatus
121
149
  }
122
150
  ];
123
151
  function error(status, code, message, detail) {
@@ -0,0 +1,5 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { EndpointContext } from '../types/endpoints.js';
3
+ export declare function handlePages<TUser extends {
4
+ id: string | number;
5
+ }>(req: PayloadRequest, { options }: EndpointContext<TUser>): Promise<Response>;