@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.
- package/README.md +122 -27
- package/dist/admin/MetricSourceSelect.js +3 -3
- package/dist/admin/PlacementStatus.d.ts +3 -0
- package/dist/admin/PlacementStatus.js +54 -0
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +2 -2
- package/dist/admin/TestsDashboardCards.d.ts +1 -0
- package/dist/admin/TestsDashboardCards.js +4 -8
- package/dist/admin/TestsNavLink.d.ts +1 -0
- package/dist/admin/TestsNavLink.js +4 -10
- package/dist/admin/actions/ConfirmDialog.d.ts +2 -1
- package/dist/admin/actions/ConfirmDialog.js +2 -2
- package/dist/admin/actions/DecideDialog.d.ts +2 -1
- package/dist/admin/actions/DecideDialog.js +2 -2
- package/dist/admin/api.d.ts +18 -2
- package/dist/admin/api.js +47 -2
- package/dist/admin/blocks/BlockPanel.d.ts +4 -0
- package/dist/admin/blocks/BlockPanel.js +199 -0
- package/dist/admin/blocks/TestBlockField.d.ts +5 -0
- package/dist/admin/blocks/TestBlockField.js +163 -0
- package/dist/admin/blocks/formApi.d.ts +46 -0
- package/dist/admin/blocks/formApi.js +159 -0
- package/dist/admin/blocks/inject.d.ts +7 -0
- package/dist/admin/blocks/inject.js +41 -0
- package/dist/admin/blocks/model.d.ts +17 -0
- package/dist/admin/blocks/model.js +45 -0
- package/dist/admin/blocks/statusCache.d.ts +9 -0
- package/dist/admin/blocks/statusCache.js +92 -0
- package/dist/admin/blocks/useBlockForm.d.ts +17 -0
- package/dist/admin/blocks/useBlockForm.js +46 -0
- package/dist/admin/copy/actions.d.ts +2 -0
- package/dist/admin/copy/actions.js +9 -0
- package/dist/admin/copy/blocks.d.ts +43 -0
- package/dist/admin/copy/blocks.js +64 -0
- package/dist/admin/copy/common.d.ts +1 -0
- package/dist/admin/copy/common.js +1 -1
- package/dist/admin/copy/goals.d.ts +2 -0
- package/dist/admin/copy/goals.js +4 -1
- package/dist/admin/copy/newTest.d.ts +10 -0
- package/dist/admin/copy/newTest.js +10 -0
- package/dist/admin/copy/pages.d.ts +24 -0
- package/dist/admin/copy/pages.js +24 -0
- package/dist/admin/copy/readiness.d.ts +23 -3
- package/dist/admin/copy/readiness.js +24 -3
- package/dist/admin/copy/results.d.ts +1 -0
- package/dist/admin/copy/results.js +1 -0
- package/dist/admin/copy/testPage.d.ts +4 -0
- package/dist/admin/copy/testPage.js +4 -0
- package/dist/admin/goals/XpGoalField.d.ts +4 -0
- package/dist/admin/goals/XpGoalField.js +168 -0
- package/dist/admin/goals/xpGoalField.css +48 -0
- package/dist/admin/metricSource.model.d.ts +2 -0
- package/dist/admin/metricSource.model.js +4 -0
- package/dist/admin/pageCollections.d.ts +10 -0
- package/dist/admin/pageCollections.js +20 -0
- package/dist/admin/paths.d.ts +9 -0
- package/dist/admin/paths.js +9 -0
- package/dist/admin/placementStatus.model.d.ts +2 -0
- package/dist/admin/placementStatus.model.js +19 -0
- package/dist/admin/placements/placementLabels.d.ts +11 -0
- package/dist/admin/placements/placementLabels.js +21 -0
- package/dist/admin/placements/placementLabels.model.d.ts +13 -0
- package/dist/admin/placements/placementLabels.model.js +12 -0
- package/dist/admin/placements/usePlacementLabels.d.ts +12 -0
- package/dist/admin/placements/usePlacementLabels.js +102 -0
- package/dist/admin/results/DataQualityPanel.js +4 -1
- package/dist/admin/results/results.model.d.ts +3 -0
- package/dist/admin/results/results.model.js +8 -4
- package/dist/admin/surface/TestsHome.js +39 -10
- package/dist/admin/surface/TestsSurface.js +7 -6
- package/dist/admin/surface/contracts.d.ts +6 -5
- package/dist/admin/surface/goals/GoalForm.js +71 -44
- package/dist/admin/surface/goals/GoalsList.js +5 -4
- package/dist/admin/surface/goals/data.js +4 -1
- package/dist/admin/surface/goals/goals.model.d.ts +1 -2
- package/dist/admin/surface/goals/goals.model.js +1 -3
- package/dist/admin/surface/home.model.d.ts +3 -3
- package/dist/admin/surface/home.model.js +8 -4
- package/dist/admin/surface/newTest/NewTest.js +56 -17
- package/dist/admin/surface/newTest/PageWizard.d.ts +7 -0
- package/dist/admin/surface/newTest/PageWizard.js +214 -0
- package/dist/admin/surface/pagePicker/CopyRoute.d.ts +6 -0
- package/dist/admin/surface/pagePicker/CopyRoute.js +53 -0
- package/dist/admin/surface/pagePicker/PageCombobox.d.ts +10 -0
- package/dist/admin/surface/pagePicker/PageCombobox.js +395 -0
- package/dist/admin/surface/pagePicker/pagePicker.css +10 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.d.ts +27 -0
- package/dist/admin/surface/pagePicker/pagePicker.model.js +25 -0
- package/dist/admin/surface/readiness/ActionDialogs.js +99 -64
- package/dist/admin/surface/readiness/Banner.js +30 -6
- package/dist/admin/surface/readiness/readiness.model.d.ts +8 -1
- package/dist/admin/surface/readiness/readiness.model.js +92 -9
- package/dist/admin/surface/results/ReleasePages.d.ts +3 -1
- package/dist/admin/surface/results/ReleasePages.js +3 -7
- package/dist/admin/surface/results/ResultsBody.js +21 -2
- package/dist/admin/surface/results/ResultsHeader.js +5 -13
- package/dist/admin/surface/results/useResults.d.ts +20 -1
- package/dist/admin/surface/test/AdvancedSection.js +111 -104
- package/dist/admin/surface/test/GoalSection.js +94 -14
- package/dist/admin/surface/test/LengthSection.js +1 -1
- package/dist/admin/surface/test/PageHoldout.d.ts +3 -0
- package/dist/admin/surface/test/PageHoldout.js +67 -0
- package/dist/admin/surface/test/TestPage.js +5 -9
- package/dist/admin/surface/test/VersionsSection.js +12 -5
- package/dist/admin/surface/test/fields.d.ts +13 -4
- package/dist/admin/surface/test/fields.js +46 -14
- package/dist/admin/surface/test/jumpToSection.d.ts +1 -1
- package/dist/admin/surface/test/jumpToSection.js +4 -3
- package/dist/admin/surface/test/pageParts.d.ts +4 -1
- package/dist/admin/surface/test/pageParts.js +49 -1
- package/dist/admin/surface/test/testPage.css +3 -1
- package/dist/admin/surface/testDocument.model.d.ts +1 -0
- package/dist/admin/surface/testDocument.model.js +2 -1
- package/dist/admin/surface/useTestDocument.d.ts +15 -2
- package/dist/admin/surface/useTestDocument.js +53 -15
- package/dist/admin/surfaceUrls.d.ts +7 -0
- package/dist/admin/surfaceUrls.js +19 -0
- package/dist/admin/theme.css +2 -0
- package/dist/admin/useApi.d.ts +21 -2
- package/dist/admin/useApi.js +3 -5
- package/dist/admin/views/CollectionListRedirect.d.ts +1 -0
- package/dist/admin/views/CollectionListRedirect.js +1 -1
- package/dist/admin/views/ExperimentsRootView.js +28 -133
- package/dist/admin/views/GoalEditRedirect.d.ts +4 -0
- package/dist/admin/views/GoalEditRedirect.js +14 -0
- package/dist/admin/views/GoalsCollectionListView.d.ts +2 -0
- package/dist/admin/views/GoalsCollectionListView.js +4 -0
- package/dist/admin/views/TestsCollectionListView.d.ts +2 -0
- package/dist/admin/views/TestsCollectionListView.js +4 -0
- package/dist/admin/views/TestsNavLinkServer.d.ts +1 -0
- package/dist/admin/views/TestsNavLinkServer.js +1 -1
- package/dist/admin/views/buildSurfaceProps.d.ts +8 -0
- package/dist/admin/views/buildSurfaceProps.js +143 -0
- package/dist/admin/views/renderCollectionList.d.ts +2 -0
- package/dist/admin/views/renderCollectionList.js +34 -0
- package/dist/admin/views/renderTestsSurface.d.ts +3 -0
- package/dist/admin/views/renderTestsSurface.js +34 -0
- package/dist/blocks/bindings.d.ts +3 -0
- package/dist/blocks/bindings.js +34 -0
- package/dist/blocks/createExperimentBlock.d.ts +6 -0
- package/dist/blocks/createExperimentBlock.js +77 -0
- package/dist/blocks/formState.d.ts +31 -0
- package/dist/blocks/formState.js +146 -0
- package/dist/blocks/model.d.ts +41 -0
- package/dist/blocks/model.js +34 -0
- package/dist/client/ExperimentGroup.d.ts +6 -0
- package/dist/client/ExperimentGroup.js +9 -0
- package/dist/client/ExperimentsProvider.js +20 -4
- package/dist/client/clickGoals.d.ts +4 -0
- package/dist/client/clickGoals.js +43 -0
- package/dist/collections/experiments/config.js +21 -7
- package/dist/collections/experiments/deleteGuard.d.ts +2 -1
- package/dist/collections/experiments/deleteGuard.js +12 -1
- package/dist/collections/experiments/editability.js +4 -2
- package/dist/collections/experiments/pickSlug.d.ts +2 -0
- package/dist/collections/experiments/pickSlug.js +8 -0
- package/dist/collections/labels.d.ts +3 -0
- package/dist/collections/labels.js +20 -0
- package/dist/collections/metrics.js +28 -6
- package/dist/collections/phases.js +1 -0
- package/dist/collections/serialize.js +7 -0
- package/dist/db/clickGoals.d.ts +9 -0
- package/dist/db/clickGoals.js +22 -0
- package/dist/db/delete.d.ts +1 -0
- package/dist/db/delete.js +1 -0
- package/dist/db/locks.d.ts +8 -0
- package/dist/db/locks.js +27 -4
- package/dist/db/names.d.ts +4 -0
- package/dist/db/names.js +4 -0
- package/dist/db/pageBindings.d.ts +4 -0
- package/dist/db/pageBindings.js +17 -0
- package/dist/db/pageBusy.d.ts +2 -0
- package/dist/db/pageBusy.js +10 -0
- package/dist/db/pageContent.d.ts +10 -0
- package/dist/db/pageContent.js +79 -0
- package/dist/db/placements.d.ts +33 -0
- package/dist/db/placements.js +54 -0
- package/dist/db/slugs.d.ts +8 -0
- package/dist/db/slugs.js +40 -0
- package/dist/db/snapshots.d.ts +6 -0
- package/dist/db/snapshots.js +8 -5
- package/dist/endpoints/clickKeys.d.ts +5 -0
- package/dist/endpoints/clickKeys.js +17 -0
- package/dist/endpoints/events.js +4 -1
- package/dist/endpoints/index.js +28 -0
- package/dist/endpoints/pages.d.ts +5 -0
- package/dist/endpoints/pages.js +120 -0
- package/dist/endpoints/placementStatus.d.ts +6 -0
- package/dist/endpoints/placementStatus.js +238 -0
- package/dist/endpoints/readBody.d.ts +2 -0
- package/dist/endpoints/readBody.js +24 -0
- package/dist/endpoints/results.js +5 -1
- package/dist/endpoints/summary.js +11 -4
- package/dist/endpoints/wrapBlock.d.ts +6 -0
- package/dist/endpoints/wrapBlock.js +76 -0
- package/dist/errors.d.ts +3 -3
- package/dist/errors.js +4 -4
- package/dist/events/rows.d.ts +1 -1
- package/dist/events/rows.js +5 -3
- package/dist/exports/adminClient.d.ts +5 -0
- package/dist/exports/adminClient.js +5 -0
- package/dist/exports/adminRsc.d.ts +3 -0
- package/dist/exports/adminRsc.js +3 -0
- package/dist/exports/blocks.d.ts +2 -0
- package/dist/exports/blocks.js +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/index.d.ts +6 -1
- package/dist/exports/index.js +3 -0
- package/dist/exports/puck.d.ts +3 -1
- package/dist/exports/puck.js +2 -0
- package/dist/exports/puckEditor.d.ts +3 -0
- package/dist/exports/puckEditor.js +2 -0
- package/dist/exports/rsc.d.ts +3 -0
- package/dist/exports/rsc.js +2 -0
- package/dist/fields/xpGoal.d.ts +3 -0
- package/dist/fields/xpGoal.js +16 -0
- package/dist/globals/state.js +10 -0
- package/dist/goals/cache.d.ts +3 -0
- package/dist/goals/cache.js +35 -0
- package/dist/goals/fairness.d.ts +40 -0
- package/dist/goals/fairness.js +200 -0
- package/dist/goals/key.d.ts +4 -0
- package/dist/goals/key.js +3 -0
- package/dist/goals/picker/clickGoalPicker.model.d.ts +29 -0
- package/dist/goals/picker/clickGoalPicker.model.js +45 -0
- package/dist/goals/picker/copy.d.ts +16 -0
- package/dist/goals/picker/copy.js +15 -0
- package/dist/goals/picker/useClickGoalPicker.d.ts +30 -0
- package/dist/goals/picker/useClickGoalPicker.js +220 -0
- package/dist/goals/validate.d.ts +3 -0
- package/dist/goals/validate.js +14 -0
- package/dist/hooks/pageGuard.d.ts +5 -1
- package/dist/hooks/pageGuard.js +157 -95
- package/dist/hooks/puckGoals.d.ts +3 -0
- package/dist/hooks/puckGoals.js +38 -0
- package/dist/onInit.js +29 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.js +41 -1
- package/dist/plugin.js +50 -43
- package/dist/puck/copy.d.ts +1 -0
- package/dist/puck/copy.js +1 -0
- package/dist/puck/editor/editor.chrome.css +14 -0
- package/dist/puck/editor/editor.css +106 -0
- package/dist/puck/editor/editor.model.d.ts +4 -2
- package/dist/puck/editor/editor.model.js +6 -3
- package/dist/puck/editor/integration.d.ts +10 -0
- package/dist/puck/editor/integration.js +30 -0
- package/dist/puck/editor/xpGoalPuckEditorField.d.ts +7 -0
- package/dist/puck/editor/xpGoalPuckEditorField.js +142 -0
- package/dist/puck/integration.d.ts +6 -0
- package/dist/puck/integration.js +21 -0
- package/dist/puck/xpGoalField.d.ts +3 -0
- package/dist/puck/xpGoalField.js +6 -0
- package/dist/results/health.d.ts +4 -0
- package/dist/results/health.js +27 -0
- package/dist/results/snapshot.js +15 -2
- package/dist/sanitizeReadinessDetail.js +43 -0
- package/dist/server/pageKindSeoContract.d.ts +27 -0
- package/dist/server/pageKindSeoContract.js +80 -0
- package/dist/server/recordContentChange.js +58 -8
- package/dist/server/resolveExperimentBlocks.d.ts +5 -0
- package/dist/server/resolveExperimentBlocks.js +54 -0
- package/dist/server/runResults.js +56 -15
- package/dist/server/toClientAssignments.js +2 -1
- package/dist/transitions/addMetric.d.ts +2 -1
- package/dist/transitions/addMetric.js +28 -1
- package/dist/transitions/checkStart.d.ts +14 -1
- package/dist/transitions/checkStart.js +92 -8
- package/dist/transitions/decide.js +25 -0
- package/dist/transitions/deleteExperiment.d.ts +3 -1
- package/dist/transitions/deleteExperiment.js +27 -1
- package/dist/transitions/kernel.js +4 -0
- package/dist/transitions/makeContent.d.ts +5 -0
- package/dist/transitions/makeContent.js +39 -11
- package/dist/transitions/pageCollection.d.ts +10 -0
- package/dist/transitions/pageCollection.js +20 -0
- package/dist/transitions/pageVersions.d.ts +10 -0
- package/dist/transitions/pageVersions.js +44 -0
- package/dist/transitions/phaseRecord.d.ts +1 -0
- package/dist/transitions/placementInteractions.d.ts +40 -0
- package/dist/transitions/placementInteractions.js +94 -0
- package/dist/transitions/placements.d.ts +8 -0
- package/dist/transitions/placements.js +148 -1
- package/dist/transitions/preflightData.d.ts +2 -2
- package/dist/transitions/preflightData.js +137 -5
- package/dist/transitions/resolveCaps.d.ts +1 -0
- package/dist/transitions/resolveCaps.js +6 -2
- package/dist/transitions/start.js +46 -1
- package/dist/types/admin.d.ts +1 -1
- package/dist/types/assignments.d.ts +1 -0
- package/dist/types/contracts.d.ts +15 -0
- package/dist/types/endpoints.d.ts +92 -5
- package/dist/types/index.d.ts +3 -0
- package/dist/types/puck.d.ts +7 -1
- package/dist/types/snapshot.d.ts +17 -4
- package/dist/types/surface.d.ts +13 -0
- package/dist/ui/ui.css +4 -0
- package/package.json +8 -1
package/dist/db/locks.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/db/names.d.ts
CHANGED
|
@@ -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,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;
|
package/dist/db/slugs.js
ADDED
|
@@ -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
|
+
};
|
package/dist/db/snapshots.d.ts
CHANGED
|
@@ -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;
|
package/dist/db/snapshots.js
CHANGED
|
@@ -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.
|
|
35
|
-
|
|
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
|
|
42
|
-
WHERE s.${q(s.experimentId)} = p.${q(p.experimentId)} AND s.${q(s.phase)} = p.${q(p.phase)}
|
|
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
|
+
}
|
package/dist/endpoints/events.js
CHANGED
|
@@ -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
|
|
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));
|
package/dist/endpoints/index.js
CHANGED
|
@@ -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) {
|