@delmaredigital/payload-experiments 0.2.0-rc.6 → 0.3.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.
- package/README.md +129 -11
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +24 -16
- package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
- package/dist/admin/actions/ConfirmDialog.js +37 -82
- package/dist/admin/actions/DecideDialog.d.ts +3 -1
- package/dist/admin/actions/DecideDialog.js +30 -13
- package/dist/admin/actions/MakeContentResult.d.ts +13 -2
- package/dist/admin/actions/MakeContentResult.js +115 -16
- package/dist/admin/actions/actions.model.d.ts +2 -4
- package/dist/admin/actions/actions.model.js +2 -3
- package/dist/admin/allocation.model.js +3 -5
- package/dist/admin/api.d.ts +10 -4
- package/dist/admin/api.js +11 -7
- package/dist/admin/copy/actions.d.ts +39 -3
- package/dist/admin/copy/actions.js +40 -4
- package/dist/admin/copy/armSelect.js +5 -5
- package/dist/admin/copy/arms.d.ts +1 -7
- package/dist/admin/copy/arms.js +1 -7
- package/dist/admin/copy/audience.d.ts +2 -1
- package/dist/admin/copy/audience.js +3 -2
- package/dist/admin/copy/common.d.ts +13 -6
- package/dist/admin/copy/common.js +14 -8
- package/dist/admin/copy/goals.d.ts +21 -6
- package/dist/admin/copy/goals.js +27 -6
- package/dist/admin/copy/readiness.d.ts +19 -0
- package/dist/admin/copy/readiness.js +19 -0
- package/dist/admin/copy/recommendation.d.ts +0 -3
- package/dist/admin/copy/recommendation.js +0 -3
- package/dist/admin/copy/results.d.ts +41 -2
- package/dist/admin/copy/results.js +42 -3
- package/dist/admin/copy/review.d.ts +1 -1
- package/dist/admin/copy/review.js +1 -1
- package/dist/admin/copy/setting.d.ts +2 -0
- package/dist/admin/copy/setting.js +2 -0
- package/dist/admin/copy/surface.d.ts +7 -4
- package/dist/admin/copy/surface.js +9 -4
- package/dist/admin/copy/testPage.d.ts +12 -0
- package/dist/admin/copy/testPage.js +12 -0
- package/dist/admin/format/date.d.ts +2 -0
- package/dist/admin/format/date.js +10 -0
- package/dist/admin/labels.d.ts +2 -7
- package/dist/admin/labels.js +2 -7
- package/dist/admin/previewLinks.model.d.ts +2 -2
- package/dist/admin/previewLinks.model.js +5 -7
- package/dist/admin/results/CiChart.js +11 -21
- package/dist/admin/results/DataQualityPanel.d.ts +4 -2
- package/dist/admin/results/DataQualityPanel.js +30 -17
- package/dist/admin/results/DataTable.d.ts +16 -0
- package/dist/admin/results/DataTable.js +152 -0
- package/dist/admin/results/StatTile.d.ts +7 -0
- package/dist/admin/results/StatTile.js +22 -0
- package/dist/admin/results/VerdictHero.d.ts +13 -0
- package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
- package/dist/admin/results/releasePages.model.d.ts +17 -0
- package/dist/admin/results/releasePages.model.js +10 -0
- package/dist/admin/results/results.css +64 -0
- package/dist/admin/results/results.model.d.ts +6 -2
- package/dist/admin/results/results.model.js +12 -5
- package/dist/admin/surface/ResultsRoute.d.ts +1 -1
- package/dist/admin/surface/ResultsRoute.js +60 -31
- package/dist/admin/surface/SurfaceShell.d.ts +11 -2
- package/dist/admin/surface/SurfaceShell.js +49 -14
- package/dist/admin/surface/TestsHome.d.ts +6 -1
- package/dist/admin/surface/TestsHome.js +183 -112
- package/dist/admin/surface/TestsSurface.js +56 -16
- package/dist/admin/surface/contracts.d.ts +1 -0
- package/dist/admin/surface/goals/GoalForm.js +341 -308
- package/dist/admin/surface/goals/GoalsList.js +123 -89
- package/dist/admin/surface/goals/data.d.ts +8 -2
- package/dist/admin/surface/goals/data.js +11 -3
- package/dist/admin/surface/goals/goals.css +10 -13
- package/dist/admin/surface/home.model.d.ts +7 -8
- package/dist/admin/surface/home.model.js +23 -12
- package/dist/admin/surface/newTest/NewTest.js +5 -5
- package/dist/admin/surface/pageBlock/parts.js +31 -26
- package/dist/admin/surface/readiness/ActionDialogs.js +43 -14
- package/dist/admin/surface/readiness/Banner.d.ts +3 -0
- package/dist/admin/surface/readiness/Banner.js +63 -36
- package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
- package/dist/admin/surface/readiness/SaveStatus.js +6 -3
- package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
- package/dist/admin/surface/readiness/readiness.model.js +18 -9
- package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
- package/dist/admin/surface/readiness/useTransition.js +2 -1
- package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
- package/dist/admin/surface/results/ReleasePages.js +116 -0
- package/dist/admin/surface/results/ResultsBody.d.ts +15 -2
- package/dist/admin/surface/results/ResultsBody.js +281 -107
- package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
- package/dist/admin/surface/results/ResultsHeader.js +177 -0
- package/dist/admin/surface/results/recommendation.model.js +2 -6
- package/dist/admin/surface/results/useResults.d.ts +21 -3
- package/dist/admin/surface/results/useResults.js +2 -1
- package/dist/admin/surface/setting/parts.js +75 -88
- package/dist/admin/surface/setting/setting.css +3 -15
- package/dist/admin/surface/surface.css +62 -47
- package/dist/admin/surface/test/AdvancedSection.js +55 -48
- package/dist/admin/surface/test/GoalSection.js +39 -32
- package/dist/admin/surface/test/LengthSection.js +46 -34
- package/dist/admin/surface/test/Rail.d.ts +4 -0
- package/dist/admin/surface/test/Rail.js +83 -0
- package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
- package/dist/admin/surface/test/ReadinessBar.js +33 -0
- package/dist/admin/surface/test/Section.d.ts +1 -1
- package/dist/admin/surface/test/Section.js +36 -16
- package/dist/admin/surface/test/TargetingSection.js +168 -142
- package/dist/admin/surface/test/TestPage.d.ts +2 -1
- package/dist/admin/surface/test/TestPage.js +146 -59
- package/dist/admin/surface/test/VersionsSection.js +151 -120
- package/dist/admin/surface/test/fields.d.ts +11 -4
- package/dist/admin/surface/test/fields.js +84 -85
- package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
- package/dist/admin/surface/test/jumpToSection.js +10 -0
- package/dist/admin/surface/test/pageParts.js +9 -3
- package/dist/admin/surface/test/testPage.css +81 -14
- package/dist/admin/surface/useTestDocument.d.ts +1 -0
- package/dist/admin/surface/useTestDocument.js +40 -9
- package/dist/admin/targeting/targeting.model.d.ts +1 -3
- package/dist/admin/targeting/targeting.model.js +3 -8
- package/dist/admin/theme.css +115 -36
- package/dist/admin/theme.d.ts +38 -0
- package/dist/admin/theme.js +38 -1
- package/dist/admin/useApi.d.ts +17 -2
- package/dist/admin/views/ExperimentsRootView.js +12 -2
- package/dist/collections/experiments/config.js +44 -1
- package/dist/collections/experiments/lockHook.js +16 -2
- package/dist/collections/identities.js +6 -1
- package/dist/collections/metrics.js +7 -0
- package/dist/collections/metricsGuards.d.ts +9 -0
- package/dist/collections/metricsGuards.js +33 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/contentDone.d.ts +4 -0
- package/dist/contentDone.js +5 -0
- package/dist/db/identities.d.ts +8 -7
- package/dist/db/identities.js +9 -6
- package/dist/db/metricUsage.d.ts +19 -0
- package/dist/db/metricUsage.js +54 -0
- package/dist/db/names.d.ts +5 -0
- package/dist/db/names.js +5 -0
- package/dist/db/publishedContent.d.ts +10 -0
- package/dist/db/publishedContent.js +43 -0
- package/dist/db/results.d.ts +2 -0
- package/dist/db/results.js +6 -2
- package/dist/db/snapshots.d.ts +18 -6
- package/dist/db/snapshots.js +23 -7
- package/dist/db/staleEdit.d.ts +6 -0
- package/dist/db/staleEdit.js +10 -0
- package/dist/db/sweep.js +11 -6
- package/dist/endpoints/events.js +3 -3
- package/dist/endpoints/index.js +19 -1
- package/dist/endpoints/recompute.js +2 -1
- package/dist/endpoints/results.js +5 -0
- package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
- package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
- package/dist/endpoints/summary.js +20 -2
- package/dist/endpoints/transitions.d.ts +15 -0
- package/dist/endpoints/transitions.js +36 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +28 -0
- package/dist/exports/adminClient.d.ts +1 -0
- package/dist/exports/adminClient.js +1 -0
- package/dist/exports/index.d.ts +2 -1
- package/dist/exports/index.js +1 -0
- package/dist/exports/puckEditor.d.ts +1 -0
- package/dist/exports/puckEditor.js +1 -0
- package/dist/exports/rsc.d.ts +5 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/hooks/hostHook.js +3 -2
- package/dist/hooks/pageGuard.d.ts +5 -0
- package/dist/hooks/pageGuard.js +121 -0
- package/dist/labels/arms.d.ts +7 -0
- package/dist/labels/arms.js +7 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +38 -0
- package/dist/manifest/compile.d.ts +5 -0
- package/dist/manifest/compile.js +5 -1
- package/dist/options.js +23 -0
- package/dist/plugin.js +26 -2
- package/dist/puck/copy.d.ts +1 -1
- package/dist/puck/copy.js +6 -6
- package/dist/puck/editor/editor.chrome.css +235 -0
- package/dist/puck/editor/editor.css +241 -43
- package/dist/results/aggregate.js +4 -3
- package/dist/results/analyse.d.ts +4 -1
- package/dist/results/analyse.js +27 -11
- package/dist/results/cuped.js +3 -2
- package/dist/results/identityCredit.d.ts +15 -7
- package/dist/results/identityCredit.js +32 -33
- package/dist/results/rolloutUnits.js +2 -1
- package/dist/results/snapshot.js +8 -2
- package/dist/results/srm.js +2 -1
- package/dist/results/verdict.d.ts +2 -0
- package/dist/results/verdict.js +18 -1
- package/dist/sanitizeReadinessDetail.d.ts +7 -0
- package/dist/sanitizeReadinessDetail.js +54 -0
- package/dist/server/linkIdentity.d.ts +10 -1
- package/dist/server/linkIdentity.js +20 -18
- package/dist/server/metadata.d.ts +5 -0
- package/dist/server/metadata.js +9 -1
- package/dist/server/recordContentChange.js +2 -1
- package/dist/server/recordExposures.js +3 -3
- package/dist/server/runResults.d.ts +3 -2
- package/dist/server/runResults.js +125 -34
- package/dist/server/trackConversion.js +5 -6
- package/dist/targeting/values.d.ts +4 -0
- package/dist/targeting/values.js +10 -0
- package/dist/transitions/addMetric.js +8 -0
- package/dist/transitions/archive.js +2 -3
- package/dist/transitions/checkStart.d.ts +6 -3
- package/dist/transitions/checkStart.js +15 -0
- package/dist/transitions/contentFields.d.ts +7 -0
- package/dist/transitions/contentFields.js +20 -0
- package/dist/transitions/decide.js +6 -1
- package/dist/transitions/deleteExperiment.js +2 -1
- package/dist/transitions/dismissContentEntry.d.ts +8 -0
- package/dist/transitions/dismissContentEntry.js +45 -0
- package/dist/transitions/makeContent.d.ts +5 -2
- package/dist/transitions/makeContent.js +217 -118
- package/dist/transitions/phaseRecord.d.ts +8 -2
- package/dist/transitions/placements.d.ts +1 -0
- package/dist/transitions/placements.js +4 -0
- package/dist/transitions/preflightData.d.ts +1 -1
- package/dist/transitions/preflightData.js +42 -4
- package/dist/transitions/releasePages.d.ts +8 -0
- package/dist/transitions/releasePages.js +26 -0
- package/dist/transitions/restoreContentEntry.d.ts +8 -0
- package/dist/transitions/restoreContentEntry.js +47 -0
- package/dist/transitions/start.js +30 -5
- package/dist/types/contracts.d.ts +14 -0
- package/dist/types/endpoints.d.ts +34 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/reasons.d.ts +3 -0
- package/dist/types/reasons.js +1 -0
- package/dist/types/snapshot.d.ts +6 -1
- package/dist/ui/Banner.d.ts +2 -0
- package/dist/ui/Banner.js +2 -0
- package/dist/ui/Breadcrumb.d.ts +7 -0
- package/dist/ui/Breadcrumb.js +25 -0
- package/dist/ui/Button.d.ts +15 -0
- package/dist/ui/Button.js +32 -0
- package/dist/ui/Card.d.ts +12 -0
- package/dist/ui/Card.js +40 -0
- package/dist/ui/Checkbox.d.ts +2 -0
- package/dist/ui/Checkbox.js +2 -0
- package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
- package/dist/{admin/components → ui}/ChipsInput.js +20 -12
- package/dist/ui/Dialog.d.ts +13 -0
- package/dist/ui/Dialog.js +85 -0
- package/dist/ui/EmptyState.d.ts +2 -0
- package/dist/ui/EmptyState.js +2 -0
- package/dist/ui/Feedback.d.ts +24 -0
- package/dist/ui/Feedback.js +40 -0
- package/dist/ui/Inputs.d.ts +34 -0
- package/dist/ui/Inputs.js +225 -0
- package/dist/ui/NumberInput.d.ts +2 -0
- package/dist/ui/NumberInput.js +2 -0
- package/dist/ui/Range.d.ts +2 -0
- package/dist/ui/Range.js +2 -0
- package/dist/ui/SearchInput.d.ts +2 -0
- package/dist/ui/SearchInput.js +2 -0
- package/dist/ui/SectionCard.d.ts +2 -0
- package/dist/ui/SectionCard.js +2 -0
- package/dist/ui/Select.d.ts +2 -0
- package/dist/ui/Select.js +2 -0
- package/dist/ui/StatusChip.d.ts +2 -0
- package/dist/ui/StatusChip.js +2 -0
- package/dist/ui/Tabs.d.ts +14 -0
- package/dist/ui/Tabs.js +54 -0
- package/dist/ui/TextInput.d.ts +2 -0
- package/dist/ui/TextInput.js +2 -0
- package/dist/ui/Textarea.d.ts +2 -0
- package/dist/ui/Textarea.js +2 -0
- package/dist/ui/Toggle.d.ts +2 -0
- package/dist/ui/Toggle.js +2 -0
- package/dist/ui/accent.d.ts +21 -0
- package/dist/ui/accent.js +81 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/ui.css +92 -0
- package/package.json +1 -1
- package/dist/admin/components/chipsInput.css +0 -11
- package/dist/admin/results/ArmTable.d.ts +0 -8
- package/dist/admin/results/ArmTable.js +0 -113
- package/dist/admin/results/VerdictCard.d.ts +0 -9
- package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
- package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
- package/dist/admin/surface/test/TestHeader.d.ts +0 -4
- package/dist/admin/surface/test/TestHeader.js +0 -69
package/dist/db/sweep.js
CHANGED
|
@@ -11,6 +11,13 @@ import { ResultsBudgetError } from './pgExecutor.js';
|
|
|
11
11
|
const events = qualifiedTable(exec, TABLES.events), phases = qualifiedTable(exec, TABLES.phases);
|
|
12
12
|
const revision = `(SELECT ${q(s.revision)} FROM ${qualifiedTable(exec, TABLES.state)} LIMIT 1)`;
|
|
13
13
|
let swept = 0;
|
|
14
|
+
const warnNoProgress = ()=>logger?.warn('Experiment retention sweep made no progress before its budget expired; eligible rows may remain.');
|
|
15
|
+
// Every budget-limited exit with nothing swept is reported, whether the budget ran out mid-statement (the catch
|
|
16
|
+
// below) or before the next batch could start (here).
|
|
17
|
+
const finish = ()=>{
|
|
18
|
+
if (swept === 0 && !shouldContinue()) warnNoProgress();
|
|
19
|
+
return swept;
|
|
20
|
+
};
|
|
14
21
|
// Each statement still uses the caller's budget/race-aware executor.
|
|
15
22
|
async function batches(predicate, order, params = []) {
|
|
16
23
|
while(shouldContinue()){
|
|
@@ -38,7 +45,7 @@ import { ResultsBudgetError } from './pgExecutor.js';
|
|
|
38
45
|
type
|
|
39
46
|
]);
|
|
40
47
|
}
|
|
41
|
-
if (!shouldContinue()) return
|
|
48
|
+
if (!shouldContinue()) return finish();
|
|
42
49
|
// Final-endedAt retention protects every phase while any phase is open.
|
|
43
50
|
const ended = await exec.query(`SELECT ${q(p.experimentId)} AS "experimentId" FROM ${phases}
|
|
44
51
|
WHERE ${q(p.experimentId)} IS NOT NULL GROUP BY ${q(p.experimentId)}
|
|
@@ -54,7 +61,7 @@ import { ResultsBudgetError } from './pgExecutor.js';
|
|
|
54
61
|
cutoff
|
|
55
62
|
]);
|
|
56
63
|
}
|
|
57
|
-
if (!shouldContinue()) return
|
|
64
|
+
if (!shouldContinue()) return finish();
|
|
58
65
|
// Every protected window ends at/after cutoff (or is open). For old
|
|
59
66
|
// conversions their union therefore starts at the earliest lookback start;
|
|
60
67
|
// everything before that single bound is safe. Compute it once, in the same
|
|
@@ -80,11 +87,9 @@ import { ResultsBudgetError } from './pgExecutor.js';
|
|
|
80
87
|
before,
|
|
81
88
|
frozen.revision
|
|
82
89
|
]);
|
|
83
|
-
return
|
|
90
|
+
return finish();
|
|
84
91
|
} catch (error) {
|
|
85
|
-
if (swept === 0 && error instanceof ResultsBudgetError)
|
|
86
|
-
logger?.warn('Experiment retention sweep made no progress before its budget expired; eligible rows may remain.');
|
|
87
|
-
}
|
|
92
|
+
if (swept === 0 && error instanceof ResultsBudgetError) warnNoProgress();
|
|
88
93
|
throw error;
|
|
89
94
|
}
|
|
90
95
|
}
|
package/dist/endpoints/events.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeError } from '../log.js';
|
|
1
2
|
import { NextRequest } from 'next/server.js';
|
|
2
3
|
import { DEFAULT_MAX_BODY_BYTES } from '../constants.js';
|
|
3
4
|
import { pooledExecutor } from '../db/executor.js';
|
|
@@ -89,9 +90,8 @@ export async function handleEvents(req, { options }) {
|
|
|
89
90
|
} catch (error) {
|
|
90
91
|
try {
|
|
91
92
|
req.payload.logger.error({
|
|
92
|
-
err: error
|
|
93
|
-
|
|
94
|
-
});
|
|
93
|
+
err: safeError(error)
|
|
94
|
+
}, 'Experiments: events endpoint failed');
|
|
95
95
|
} catch {}
|
|
96
96
|
}
|
|
97
97
|
return accepted();
|
package/dist/endpoints/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { XpNotImplemented, XpRefusal } from '../errors.js';
|
|
|
4
4
|
import { readUnitCookie } from '../runtime/cookie.js';
|
|
5
5
|
import { handleManifest } from './manifest.js';
|
|
6
6
|
import { handleEvents } from './events.js';
|
|
7
|
-
import { handleStart, handleAllocate, handleAddMetric, handleStop, handleDecide, handleReleaseHoldout, handleMakeContent, handleArchive } from './transitions.js';
|
|
7
|
+
import { handleStart, handleAllocate, handleAddMetric, handleStop, handleDecide, handleReleaseHoldout, handleReleasePages, handleMakeContent, handleDismissContentEntry, handleRestoreContentEntry, handleArchive } from './transitions.js';
|
|
8
8
|
import { handlePreflight } from './preflight.js';
|
|
9
9
|
import { handlePreviewToken } from './previewToken.js';
|
|
10
10
|
import { handleResults } from './results.js';
|
|
@@ -59,12 +59,30 @@ export const ROUTES = [
|
|
|
59
59
|
access: 'manage',
|
|
60
60
|
handler: handleReleaseHoldout
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
method: 'post',
|
|
64
|
+
path: '/xp/experiments/:id/release-pages',
|
|
65
|
+
access: 'manage',
|
|
66
|
+
handler: handleReleasePages
|
|
67
|
+
},
|
|
62
68
|
{
|
|
63
69
|
method: 'post',
|
|
64
70
|
path: '/xp/experiments/:id/make-content',
|
|
65
71
|
access: 'manage',
|
|
66
72
|
handler: handleMakeContent
|
|
67
73
|
},
|
|
74
|
+
{
|
|
75
|
+
method: 'post',
|
|
76
|
+
path: '/xp/experiments/:id/content-report/dismiss',
|
|
77
|
+
access: 'manage',
|
|
78
|
+
handler: handleDismissContentEntry
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
method: 'post',
|
|
82
|
+
path: '/xp/experiments/:id/content-report/restore',
|
|
83
|
+
access: 'manage',
|
|
84
|
+
handler: handleRestoreContentEntry
|
|
85
|
+
},
|
|
68
86
|
{
|
|
69
87
|
method: 'post',
|
|
70
88
|
path: '/xp/experiments/:id/archive',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { safeError } from '../log.js';
|
|
1
2
|
import { runResults } from '../server/runResults.js';
|
|
2
3
|
import { readSnapshots, resultsError } from './results.js';
|
|
3
4
|
import { routeId } from './routeId.js';
|
|
@@ -39,7 +40,7 @@ export async function handleRecompute(req, _ctx) {
|
|
|
39
40
|
});
|
|
40
41
|
} catch (error) {
|
|
41
42
|
req.payload.logger.error({
|
|
42
|
-
err: error,
|
|
43
|
+
err: safeError(error),
|
|
43
44
|
experimentId: docs[0].id
|
|
44
45
|
}, 'Experiment recompute failed');
|
|
45
46
|
return resultsError(503, 'recompute-failed', 'Results could not be recomputed. Try again.');
|
|
@@ -111,6 +111,11 @@ export async function handleResults(req, _ctx) {
|
|
|
111
111
|
if (selected !== null && !records.some((record)=>record.phase === phase)) return resultsError(404, 'not-found', 'Round not found');
|
|
112
112
|
const snapshot = snapshots.find((snapshot)=>snapshot.phase === phase)?.snapshot ?? null;
|
|
113
113
|
const response = {
|
|
114
|
+
experiment: {
|
|
115
|
+
name: metadata.name ?? '',
|
|
116
|
+
kind: metadata.kind,
|
|
117
|
+
status: metadata.status
|
|
118
|
+
},
|
|
114
119
|
phases,
|
|
115
120
|
snapshot,
|
|
116
121
|
...decision ? {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { sanitizeReadinessDetail } from '../sanitizeReadinessDetail.js';
|
|
@@ -2,6 +2,7 @@ import { checkStart } from '../transitions/checkStart.js';
|
|
|
2
2
|
import { loadStartInputsBatch } from '../transitions/preflightData.js';
|
|
3
3
|
import { readSnapshots, resultsError } from './results.js';
|
|
4
4
|
import { experimentId } from './routeId.js';
|
|
5
|
+
import { sanitizeReadinessDetail } from './sanitizeReadinessDetail.js';
|
|
5
6
|
export async function handleSummary(req, { options }) {
|
|
6
7
|
let ids;
|
|
7
8
|
try {
|
|
@@ -40,7 +41,13 @@ export async function handleSummary(req, { options }) {
|
|
|
40
41
|
verdict: {
|
|
41
42
|
code: snapshot.verdict.code,
|
|
42
43
|
headline: snapshot.verdict.headline
|
|
43
|
-
}
|
|
44
|
+
},
|
|
45
|
+
...snapshot.health ? {
|
|
46
|
+
health: snapshot.health.slice(0, 100).map(({ code, path })=>({
|
|
47
|
+
code,
|
|
48
|
+
path: path.slice(0, 128)
|
|
49
|
+
}))
|
|
50
|
+
} : {}
|
|
44
51
|
} : {
|
|
45
52
|
exposures: 0,
|
|
46
53
|
days: 0,
|
|
@@ -72,7 +79,18 @@ export async function handleSummary(req, { options }) {
|
|
|
72
79
|
if (!experiment) continue;
|
|
73
80
|
const input = inputs.get(String(experiment.id));
|
|
74
81
|
try {
|
|
75
|
-
|
|
82
|
+
const checks = input ? checkStart(input) : [];
|
|
83
|
+
response[key].ready = !!input && checks.every((check)=>check.ok);
|
|
84
|
+
const next = checks.find((check)=>!check.ok && !check.warning);
|
|
85
|
+
if (next) {
|
|
86
|
+
const detail = sanitizeReadinessDetail(next.code, next.detail);
|
|
87
|
+
response[key].next = {
|
|
88
|
+
code: next.code,
|
|
89
|
+
...detail === undefined ? {} : {
|
|
90
|
+
detail
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
76
94
|
} catch {
|
|
77
95
|
response[key].ready = false;
|
|
78
96
|
}
|
|
@@ -35,6 +35,21 @@ export declare function handleMakeContent<TUser extends {
|
|
|
35
35
|
} = {
|
|
36
36
|
id: string | number;
|
|
37
37
|
}>(req: PayloadRequest, ctx: EndpointContext<TUser>): Promise<Response>;
|
|
38
|
+
export declare function handleReleasePages<TUser extends {
|
|
39
|
+
id: string | number;
|
|
40
|
+
} = {
|
|
41
|
+
id: string | number;
|
|
42
|
+
}>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
|
|
43
|
+
export declare function handleDismissContentEntry<TUser extends {
|
|
44
|
+
id: string | number;
|
|
45
|
+
} = {
|
|
46
|
+
id: string | number;
|
|
47
|
+
}>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
|
|
48
|
+
export declare function handleRestoreContentEntry<TUser extends {
|
|
49
|
+
id: string | number;
|
|
50
|
+
} = {
|
|
51
|
+
id: string | number;
|
|
52
|
+
}>(req: PayloadRequest, _ctx: EndpointContext<TUser>): Promise<Response>;
|
|
38
53
|
export declare function handleArchive<TUser extends {
|
|
39
54
|
id: string | number;
|
|
40
55
|
} = {
|
|
@@ -5,6 +5,9 @@ import { addMetric } from '../transitions/addMetric.js';
|
|
|
5
5
|
import { stop } from '../transitions/stop.js';
|
|
6
6
|
import { decide } from '../transitions/decide.js';
|
|
7
7
|
import { releaseHoldout } from '../transitions/releaseHoldout.js';
|
|
8
|
+
import { releasePages } from '../transitions/releasePages.js';
|
|
9
|
+
import { dismissContentEntry } from '../transitions/dismissContentEntry.js';
|
|
10
|
+
import { restoreContentEntry } from '../transitions/restoreContentEntry.js';
|
|
8
11
|
import { makeContent } from '../transitions/makeContent.js';
|
|
9
12
|
import { archive } from '../transitions/archive.js';
|
|
10
13
|
import { routeId } from './routeId.js';
|
|
@@ -135,6 +138,39 @@ export async function handleMakeContent(req, ctx) {
|
|
|
135
138
|
options: ctx.options
|
|
136
139
|
}));
|
|
137
140
|
}
|
|
141
|
+
export async function handleReleasePages(req, _ctx) {
|
|
142
|
+
const value = await body(req);
|
|
143
|
+
if (!value || !only(value, [])) return invalidBody();
|
|
144
|
+
return respond(req, (id)=>releasePages(req, {
|
|
145
|
+
experimentId: id
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
export async function handleDismissContentEntry(req, _ctx) {
|
|
149
|
+
const value = await body(req);
|
|
150
|
+
if (!value || !only(value, [
|
|
151
|
+
'collection',
|
|
152
|
+
'id'
|
|
153
|
+
]) || typeof value.collection !== 'string' || !value.collection.trim() || !(typeof value.id === 'string' && value.id.trim() || typeof value.id === 'number' && Number.isSafeInteger(value.id))) return invalidBody();
|
|
154
|
+
const collection = value.collection, id = value.id;
|
|
155
|
+
return respond(req, (experimentId)=>dismissContentEntry(req, {
|
|
156
|
+
experimentId,
|
|
157
|
+
collection,
|
|
158
|
+
id
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
export async function handleRestoreContentEntry(req, _ctx) {
|
|
162
|
+
const value = await body(req);
|
|
163
|
+
if (!value || !only(value, [
|
|
164
|
+
'collection',
|
|
165
|
+
'id'
|
|
166
|
+
]) || typeof value.collection !== 'string' || !value.collection.trim() || !(typeof value.id === 'string' && value.id.trim() || typeof value.id === 'number' && Number.isSafeInteger(value.id))) return invalidBody();
|
|
167
|
+
const collection = value.collection, id = value.id;
|
|
168
|
+
return respond(req, (experimentId)=>restoreContentEntry(req, {
|
|
169
|
+
experimentId,
|
|
170
|
+
collection,
|
|
171
|
+
id
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
138
174
|
export async function handleArchive(req, _ctx) {
|
|
139
175
|
const value = await body(req);
|
|
140
176
|
if (!value || !only(value, [
|
package/dist/errors.d.ts
CHANGED
|
@@ -25,3 +25,11 @@ export declare class XpRefusal extends Error {
|
|
|
25
25
|
readonly isPublic = true;
|
|
26
26
|
constructor(code: RefusalCode, message: string, detail: unknown | undefined, status: 409 | 422 | 500 | 404);
|
|
27
27
|
}
|
|
28
|
+
/** Fixed analysis diagnostics: callers can choose a code, never supply log text. */
|
|
29
|
+
export type AnalysisErrorCode = 'no-baseline' | 'primary-metric-count' | 'duplicate-metric' | 'invalid-timestamp' | 'window-hours' | 'window-before-start' | 'unknown-arm' | 'metric-not-supplied' | 'non-finite-values' | 'non-finite-covariates' | 'source-unavailable' | 'unit-type-unavailable' | 'srm-weights' | 'cuped-non-finite' | 'cuped-binomial' | 'seed-format' | 'snapshot-shape' | 'snapshot-invariants';
|
|
30
|
+
export declare const ANALYSIS_ERROR_MESSAGES: Readonly<Record<AnalysisErrorCode, string>>;
|
|
31
|
+
export declare class XpAnalysisError extends Error {
|
|
32
|
+
name: string;
|
|
33
|
+
readonly code: AnalysisErrorCode;
|
|
34
|
+
constructor(code: AnalysisErrorCode);
|
|
35
|
+
}
|
package/dist/errors.js
CHANGED
|
@@ -31,3 +31,31 @@ export class XpRefusal extends Error {
|
|
|
31
31
|
super(message), this.code = code, this.detail = detail, this.status = status;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
export const ANALYSIS_ERROR_MESSAGES = Object.freeze({
|
|
35
|
+
'no-baseline': 'Phase comparison baseline or shipped arm is missing',
|
|
36
|
+
'primary-metric-count': 'Phase must have exactly one primary metric',
|
|
37
|
+
'duplicate-metric': 'Duplicate metric id',
|
|
38
|
+
'invalid-timestamp': 'Event or exposure timestamp is invalid',
|
|
39
|
+
'window-hours': 'Attribution window must be a finite number of hours greater than 0',
|
|
40
|
+
'window-before-start': 'Analysis window ends before the phase starts',
|
|
41
|
+
'unknown-arm': 'Exposed unit has an unknown arm',
|
|
42
|
+
'metric-not-supplied': 'Metric data was not supplied',
|
|
43
|
+
'non-finite-values': 'Metric values must be finite',
|
|
44
|
+
'non-finite-covariates': 'Covariates must be finite',
|
|
45
|
+
'source-unavailable': 'Metric source unavailable',
|
|
46
|
+
'unit-type-unavailable': 'Metric unit type unavailable',
|
|
47
|
+
'srm-weights': 'SRM requires positive weights',
|
|
48
|
+
'cuped-non-finite': 'Metric values and covariates must be finite',
|
|
49
|
+
'cuped-binomial': 'Binomial values and covariates must be 0 or 1',
|
|
50
|
+
'seed-format': 'Assignment seed must end in |',
|
|
51
|
+
'snapshot-shape': 'Invalid Snapshot shape (all numbers must be finite)',
|
|
52
|
+
'snapshot-invariants': 'Invalid Snapshot invariants'
|
|
53
|
+
});
|
|
54
|
+
export class XpAnalysisError extends Error {
|
|
55
|
+
name = 'XpAnalysisError';
|
|
56
|
+
code;
|
|
57
|
+
constructor(code){
|
|
58
|
+
super(ANALYSIS_ERROR_MESSAGES[code]);
|
|
59
|
+
this.code = code;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
export { XP_IF_UPDATED_AT_HEADER } from '../constants.js';
|
|
2
3
|
export { LayerUsage } from '../admin/LayerUsage.js';
|
|
3
4
|
export { MetricSourceSelect } from '../admin/MetricSourceSelect.js';
|
|
4
5
|
export { ArmSelectField } from '../admin/armSelect/ArmSelectField.js';
|
package/dist/exports/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createExperimentsPlugin } from '../plugin.js';
|
|
2
2
|
export { defineFlagPoint, defineMetricSource } from '../define.js';
|
|
3
|
+
export { contentDone } from '../contentDone.js';
|
|
3
4
|
export { createArmSelectField } from '../fields/armSelect.js';
|
|
4
|
-
export type { SerializedFlagPoint, SerializedMetricSource, SerializedAttributeKeys, EditableField, Assignment, Assignments, FlagPointResult, ClientAssignments, RenderedExposure, DocId, Attributes, ExposurePolicy, ExperimentKind, Status, UnitType, ArmKey, ArmKind, TargetingRule, Arm, ParamField, MetricSource, FlagPoint, RateLimiter, ExperimentsAccess, ExperimentsPluginOptions, ArmIssueCode, RefusalCode, ReadinessCode, ErrorEnvelope, TransitionSuccess, AllocateBody, AddMetricBody, DecideBody, ArchiveBody, MakeContentResult, PreflightResult, PreviewTokenResult, PhaseSummary, ResultsResponse, SummaryResponse, EventBody, FlagDefault, Manifest, ManifestExperiment, ArmSlot, ExperimentRef, ExperimentSlotProps, ExperimentsProxyRuntimeOptions, ExperimentsProxyRuntime, ExperimentsServerRuntimeOptions, ExperimentsServerRuntime, VerdictCode, Snapshot, } from '../types/index.js';
|
|
5
|
+
export type { SerializedFlagPoint, SerializedMetricSource, SerializedAttributeKeys, EditableField, Assignment, Assignments, FlagPointResult, ClientAssignments, RenderedExposure, DocId, Attributes, ExposurePolicy, ExperimentKind, Status, UnitType, ArmKey, ArmKind, TargetingRule, Arm, ParamField, MetricSource, FlagPoint, RateLimiter, ExperimentsAccess, ExperimentsPluginOptions, ArmIssueCode, RefusalCode, ReadinessCode, ErrorEnvelope, TransitionSuccess, AllocateBody, AddMetricBody, DecideBody, ArchiveBody, MakeContentResult, DismissContentEntryBody, DismissContentEntryResult, RestoreContentEntryBody, RestoreContentEntryResult, ContentReport, PreflightResult, PreviewTokenResult, PhaseSummary, ResultsResponse, SummaryResponse, EventBody, FlagDefault, Manifest, ManifestExperiment, ArmSlot, ExperimentRef, ExperimentSlotProps, ExperimentsProxyRuntimeOptions, ExperimentsProxyRuntime, ExperimentsServerRuntimeOptions, ExperimentsServerRuntime, VerdictCode, Snapshot, MetricUnavailableReason, ContentReportReason, } from '../types/index.js';
|
package/dist/exports/index.js
CHANGED
package/dist/exports/rsc.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ export { getAssignments } from '../server/getAssignments.js';
|
|
|
2
2
|
export { resolveExperimentData } from '../server/resolveExperimentData.js';
|
|
3
3
|
export { recordExposures } from '../server/recordExposures.js';
|
|
4
4
|
export { getFlagPoint } from '../server/getFlagPoint.js';
|
|
5
|
-
export { linkIdentity } from '../server/linkIdentity.js';
|
|
5
|
+
export { linkIdentity, type LinkIdentityOutcome } from '../server/linkIdentity.js';
|
|
6
6
|
export { trackConversion } from '../server/trackConversion.js';
|
|
7
7
|
export { eraseUser } from '../server/eraseUser.js';
|
|
8
|
+
export type { MetricUnavailableReason, ContentReportReason } from '../types/reasons.js';
|
|
8
9
|
export { runResults } from '../server/runResults.js';
|
|
9
10
|
export { recordContentChange } from '../server/recordContentChange.js';
|
|
10
11
|
export { experimentSitemapFilter } from '../server/sitemap.js';
|
|
11
12
|
export { experimentMetadata } from '../server/metadata.js';
|
|
12
13
|
export { toClientAssignments } from '../server/toClientAssignments.js';
|
|
14
|
+
export { compileManifest } from '../manifest/compile.js';
|
|
15
|
+
export type { ManifestInputs, ManifestInputExperiment, ManifestPhase, ManifestCompileOptions } from '../manifest/compile.js';
|
|
16
|
+
export type { Manifest, ManifestExperiment } from '../types/manifest.js';
|
|
13
17
|
export { contentChangesBeforeHook as captureContentBaseline } from '../hooks/contentChanges.js';
|
package/dist/exports/rsc.js
CHANGED
|
@@ -10,4 +10,6 @@ export { recordContentChange } from '../server/recordContentChange.js';
|
|
|
10
10
|
export { experimentSitemapFilter } from '../server/sitemap.js';
|
|
11
11
|
export { experimentMetadata } from '../server/metadata.js';
|
|
12
12
|
export { toClientAssignments } from '../server/toClientAssignments.js';
|
|
13
|
+
// Pure manifest builder for host tests (CORE-43): the same compile transitions run, with no database access.
|
|
14
|
+
export { compileManifest } from '../manifest/compile.js';
|
|
13
15
|
export { contentChangesBeforeHook as captureContentBaseline } from '../hooks/contentChanges.js';
|
package/dist/hooks/hostHook.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { safeError } from '../log.js';
|
|
1
2
|
/** Host saves must survive synchronous and asynchronous plugin hook failures. */ export function neverThrow(name, hook) {
|
|
2
3
|
return async (args)=>{
|
|
3
4
|
try {
|
|
4
5
|
return await hook(args);
|
|
5
6
|
} catch (error) {
|
|
6
|
-
console.error(`[payload-experiments] ${name} failed; the document was saved without it`, error);
|
|
7
|
+
console.error(`[payload-experiments] ${name} failed; the document was saved without it`, safeError(error));
|
|
7
8
|
return args.doc;
|
|
8
9
|
}
|
|
9
10
|
};
|
|
@@ -13,7 +14,7 @@
|
|
|
13
14
|
try {
|
|
14
15
|
return await hook(args);
|
|
15
16
|
} catch (error) {
|
|
16
|
-
console.error(`[payload-experiments] ${name} failed; the document was saved without it`, error);
|
|
17
|
+
console.error(`[payload-experiments] ${name} failed; the document was saved without it`, safeError(error));
|
|
17
18
|
return args.data;
|
|
18
19
|
}
|
|
19
20
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CollectionAfterOperationHook, CollectionBeforeChangeHook, CollectionBeforeDeleteHook, CollectionBeforeOperationHook } from 'payload';
|
|
2
|
+
export declare const pageGuardBeforeOperation: CollectionBeforeOperationHook;
|
|
3
|
+
export declare const pageGuardAfterOperation: CollectionAfterOperationHook;
|
|
4
|
+
export declare const pageGuardBeforeDelete: CollectionBeforeDeleteHook;
|
|
5
|
+
export declare const pageGuardBeforeChange: CollectionBeforeChangeHook;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { previousPublishedContent } from '../db/appends.js';
|
|
2
|
+
import { XpRefusal } from '../errors.js';
|
|
3
|
+
import { pageGuardCopy, READINESS_COPY } from '../admin/copy/readiness.js';
|
|
4
|
+
// createLocalReq replaces context with a shallow copy on nested calls. Keep
|
|
5
|
+
// the shared stack in that context so both the outer req and its proxies see it.
|
|
6
|
+
const operations = (req)=>req.context.xpPageGuardOperations;
|
|
7
|
+
export const pageGuardBeforeOperation = ({ args, operation })=>{
|
|
8
|
+
if (operation !== 'update' && operation !== 'restoreVersion') return;
|
|
9
|
+
const call = args;
|
|
10
|
+
let collections = operations(call.req);
|
|
11
|
+
if (!collections) call.req.context.xpPageGuardOperations = collections = new Map();
|
|
12
|
+
const slug = call.collection.config.slug;
|
|
13
|
+
const stack = collections.get(slug) ?? [];
|
|
14
|
+
stack.push({
|
|
15
|
+
draft: call.draft === true,
|
|
16
|
+
restore: operation === 'restoreVersion'
|
|
17
|
+
});
|
|
18
|
+
collections.set(slug, stack);
|
|
19
|
+
};
|
|
20
|
+
export const pageGuardAfterOperation = ({ req, collection, operation, result })=>{
|
|
21
|
+
// beforeOperation reports by-id updates as 'update'; afterOperation reports 'updateByID'.
|
|
22
|
+
if (operation === 'update' || operation === 'updateByID' || operation === 'restoreVersion') {
|
|
23
|
+
const collections = operations(req);
|
|
24
|
+
const stack = collections?.get(collection.slug);
|
|
25
|
+
stack?.pop();
|
|
26
|
+
if (!stack?.length) collections?.delete(collection.slug);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
async function protect(req, path) {
|
|
31
|
+
// page guard: the hook must see test placements regardless of the editor's access
|
|
32
|
+
const tests = await req.payload.find({
|
|
33
|
+
collection: 'xp-experiments',
|
|
34
|
+
req,
|
|
35
|
+
overrideAccess: true,
|
|
36
|
+
depth: 0,
|
|
37
|
+
limit: 0,
|
|
38
|
+
where: {
|
|
39
|
+
and: [
|
|
40
|
+
{
|
|
41
|
+
kind: {
|
|
42
|
+
equals: 'page'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
status: {
|
|
47
|
+
in: [
|
|
48
|
+
'running',
|
|
49
|
+
'decided',
|
|
50
|
+
'archived'
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
for (const test of tests.docs){
|
|
58
|
+
const decision = test.decision;
|
|
59
|
+
const archived = test.status === 'archived';
|
|
60
|
+
if (archived && (!decision?.winnerArm || decision.discardedAt || decision.pagesReleasedAt)) continue;
|
|
61
|
+
// page guard: the hook must see test placements regardless of the editor's access
|
|
62
|
+
const phases = await req.payload.find({
|
|
63
|
+
collection: 'xp-phases',
|
|
64
|
+
req,
|
|
65
|
+
overrideAccess: true,
|
|
66
|
+
depth: 0,
|
|
67
|
+
limit: 1,
|
|
68
|
+
sort: '-phase',
|
|
69
|
+
where: {
|
|
70
|
+
experiment: {
|
|
71
|
+
equals: test.id
|
|
72
|
+
},
|
|
73
|
+
phaseKind: {
|
|
74
|
+
equals: 'test'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const phase = phases.docs[0];
|
|
79
|
+
if (!phase) throw new XpRefusal('invalid-phase', READINESS_COPY.missingVersions, {
|
|
80
|
+
experimentId: test.id
|
|
81
|
+
}, 409);
|
|
82
|
+
const arms = phase.arms;
|
|
83
|
+
const protectedArms = archived ? arms.filter((arm)=>arm.kind === 'control' || arm.key === decision.winnerArm) : arms;
|
|
84
|
+
if (protectedArms.some((arm)=>arm.pageTarget === path)) {
|
|
85
|
+
throw new XpRefusal('locked-placement', pageGuardCopy(String(test.name), test.status), {
|
|
86
|
+
experimentId: test.id
|
|
87
|
+
}, 409);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function registration(req, slug) {
|
|
92
|
+
return (req.payload.config.custom?.xp).pages?.collections.find((entry)=>entry.slug === slug);
|
|
93
|
+
}
|
|
94
|
+
export const pageGuardBeforeDelete = async ({ req, id, collection })=>{
|
|
95
|
+
const page = registration(req, collection.slug);
|
|
96
|
+
if (!page) return;
|
|
97
|
+
const published = await previousPublishedContent(req, collection.slug, id);
|
|
98
|
+
if (!published || published._status === 'draft') return;
|
|
99
|
+
const path = page.pathOf(published);
|
|
100
|
+
if (path !== null) await protect(req, path);
|
|
101
|
+
};
|
|
102
|
+
export const pageGuardBeforeChange = async ({ req, collection, originalDoc, operation, data })=>{
|
|
103
|
+
if (operation !== 'update') return data;
|
|
104
|
+
const page = registration(req, collection.slug);
|
|
105
|
+
if (!page) return data;
|
|
106
|
+
const drafts = Boolean(collection.versions && collection.versions.drafts);
|
|
107
|
+
// updateDocument computes isSavingDraft from draftArg and _status before
|
|
108
|
+
// beforeChange, and writes the main row only when !isSavingDraft.
|
|
109
|
+
const call = operations(req)?.get(collection.slug)?.at(-1);
|
|
110
|
+
if (drafts && call?.draft && (call.restore || data._status !== 'published')) return data;
|
|
111
|
+
const published = await previousPublishedContent(req, collection.slug, originalDoc.id);
|
|
112
|
+
if (!published || published._status === 'draft') return data;
|
|
113
|
+
const path = page.pathOf(published);
|
|
114
|
+
const unpublishing = drafts && data._status === 'draft';
|
|
115
|
+
const trashing = collection.trash && data.deletedAt != null && !originalDoc.deletedAt;
|
|
116
|
+
if (path !== null && (trashing || unpublishing || page.pathOf({
|
|
117
|
+
...originalDoc,
|
|
118
|
+
...data
|
|
119
|
+
}) !== path)) await protect(req, path);
|
|
120
|
+
return data;
|
|
121
|
+
};
|
package/dist/log.d.ts
ADDED
package/dist/log.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ANALYSIS_ERROR_MESSAGES, XpAnalysisError, XpNoTransactionError, XpNotImplemented, XpRefusal } from './errors.js';
|
|
2
|
+
import { ResultsBudgetError } from './db/pgExecutor.js';
|
|
3
|
+
/** Deliberately construct a plain object: host serializers must never see the error. */ export function safeError(err) {
|
|
4
|
+
try {
|
|
5
|
+
if (!(err instanceof Error)) return {
|
|
6
|
+
name: typeof err
|
|
7
|
+
};
|
|
8
|
+
const name = err.name;
|
|
9
|
+
const result = {
|
|
10
|
+
name: typeof name === 'string' ? name : 'Error'
|
|
11
|
+
};
|
|
12
|
+
const code = 'code' in err ? err.code : undefined;
|
|
13
|
+
const analysisCode = err instanceof XpAnalysisError && typeof code === 'string' && Object.hasOwn(ANALYSIS_ERROR_MESSAGES, code) ? code : undefined;
|
|
14
|
+
if (typeof code === 'string' && (/^[0-9A-Z]{5}$/.test(code) || err instanceof XpRefusal || analysisCode)) result.code = code;
|
|
15
|
+
const message = err.message;
|
|
16
|
+
const stack = err.stack;
|
|
17
|
+
// Remove the entire V8 header first: message lines can impersonate frames.
|
|
18
|
+
// Unknown stack formats fail closed. Never traverse cause or database fields.
|
|
19
|
+
if (typeof name === 'string' && typeof message === 'string' && typeof stack === 'string') {
|
|
20
|
+
const header = message ? `${name}: ${message}` : name;
|
|
21
|
+
const frames = stack.startsWith(`${header}\n`) ? stack.slice(header.length + 1).split('\n').filter((line)=>/^\s+at /.test(line)) : [];
|
|
22
|
+
if (frames.length) result.stack = frames.join('\n');
|
|
23
|
+
}
|
|
24
|
+
// XpRefusal includes stored slugs; XpLockedFieldError includes request keys
|
|
25
|
+
// and stored status. Neither class's message is safe to send to host logs.
|
|
26
|
+
// An analysis error's logged text always comes from the fixed table by code, never from err.message.
|
|
27
|
+
if (analysisCode) result.message = ANALYSIS_ERROR_MESSAGES[analysisCode];
|
|
28
|
+
else if (err instanceof XpNoTransactionError || err instanceof XpNotImplemented || err instanceof ResultsBudgetError) {
|
|
29
|
+
if (typeof message === 'string') result.message = message;
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
} catch {
|
|
33
|
+
// Even instanceof, property existence checks and getters can throw on hosts' errors.
|
|
34
|
+
return {
|
|
35
|
+
name: 'Error'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -35,6 +35,7 @@ export interface ManifestInputExperiment {
|
|
|
35
35
|
winnerArm?: string | null;
|
|
36
36
|
discardedAt?: string | null;
|
|
37
37
|
holdoutReleasedAt?: string | null;
|
|
38
|
+
pagesReleasedAt?: string | null;
|
|
38
39
|
} | null;
|
|
39
40
|
currentPhase: ManifestPhase | null;
|
|
40
41
|
lastTestPhase?: ManifestPhase | null;
|
|
@@ -50,6 +51,10 @@ export declare function loadManifestInputs(req: PayloadRequest, { excludeIds, pr
|
|
|
50
51
|
pruneFlagDefaultIds?: DocId[];
|
|
51
52
|
}): Promise<ManifestInputs>;
|
|
52
53
|
/** Pure compiler. Assignment config comes exclusively from the frozen phase. */
|
|
54
|
+
/**
|
|
55
|
+
* Pure manifest builder (exported from `/rsc` for host tests). Pass the same `attributeKeys` as the plugin options,
|
|
56
|
+
* or legacy untyped targeting rules are dropped. Its input shape may change in a pre-1.0 minor.
|
|
57
|
+
*/
|
|
53
58
|
export declare function compileManifest(inputs: ManifestInputs, revision: number, { attributeKeys, onInvalid }?: ManifestCompileOptions): Manifest;
|
|
54
59
|
/** Take the state lock before reading inputs. Under READ COMMITTED, a builder
|
|
55
60
|
* waiting for another transition then sees that transition's committed changes,
|
package/dist/manifest/compile.js
CHANGED
|
@@ -107,7 +107,10 @@ export async function loadManifestInputs(req, { excludeIds = [], pruneFlagDefaul
|
|
|
107
107
|
});
|
|
108
108
|
inputs.flagDefaults = defaults;
|
|
109
109
|
}
|
|
110
|
-
/** Pure compiler. Assignment config comes exclusively from the frozen phase. */
|
|
110
|
+
/** Pure compiler. Assignment config comes exclusively from the frozen phase. */ /**
|
|
111
|
+
* Pure manifest builder (exported from `/rsc` for host tests). Pass the same `attributeKeys` as the plugin options,
|
|
112
|
+
* or legacy untyped targeting rules are dropped. Its input shape may change in a pre-1.0 minor.
|
|
113
|
+
*/ export function compileManifest(inputs, revision, { attributeKeys, onInvalid } = {}) {
|
|
111
114
|
const excluded = new Set(inputs.excludeIds?.map(String));
|
|
112
115
|
const prunedDefaults = new Set(inputs.pruneFlagDefaultIds?.map(String));
|
|
113
116
|
const manifest = {
|
|
@@ -126,6 +129,7 @@ export async function loadManifestInputs(req, { excludeIds = [], pruneFlagDefaul
|
|
|
126
129
|
const { decision, currentPhase: phase } = experiment;
|
|
127
130
|
if (experiment.status === 'archived') {
|
|
128
131
|
if (!decision?.winnerArm || decision.discardedAt) continue;
|
|
132
|
+
if (experiment.kind === 'page' && decision.pagesReleasedAt) continue;
|
|
129
133
|
const testPhase = experiment.lastTestPhase ?? (phase?.phaseKind === 'test' ? phase : null);
|
|
130
134
|
if (!testPhase) throw new XpRefusal('invalid-status', `Missing last test phase for experiment ${experiment.slug}`, undefined, 409);
|
|
131
135
|
const winner = testPhase.arms.find((arm)=>arm.key === decision.winnerArm);
|