@delmaredigital/payload-experiments 0.2.0-rc.7 → 0.3.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -26
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +50 -21
- package/dist/admin/TestsNavLink.d.ts +3 -1
- package/dist/admin/TestsNavLink.js +2 -2
- 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/armSelect/ArmSelectField.js +14 -22
- package/dist/admin/arms/arms.model.d.ts +1 -21
- package/dist/admin/arms/arms.model.js +1 -153
- 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 -6
- package/dist/admin/copy/arms.js +1 -16
- 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 +22 -0
- package/dist/admin/copy/readiness.js +21 -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 +43 -2
- package/dist/admin/copy/results.js +44 -3
- package/dist/admin/copy/review.d.ts +2 -2
- package/dist/admin/copy/review.js +2 -2
- 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 -5
- package/dist/admin/copy/surface.js +9 -5
- package/dist/admin/copy/testPage.d.ts +10 -0
- package/dist/admin/copy/testPage.js +10 -0
- package/dist/admin/format/date.d.ts +2 -0
- package/dist/admin/format/date.js +10 -0
- package/dist/admin/paths.d.ts +2 -1
- package/dist/admin/paths.js +2 -1
- package/dist/admin/previewLinks.model.d.ts +4 -2
- package/dist/admin/previewLinks.model.js +8 -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/legacy.d.ts +16 -0
- package/dist/admin/results/legacy.js +27 -0
- 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 +11 -7
- package/dist/admin/results/results.model.js +24 -7
- package/dist/admin/results/snapshot.d.ts +14 -0
- package/dist/admin/results/snapshot.js +3 -0
- package/dist/admin/surface/ResultsRoute.d.ts +1 -1
- package/dist/admin/surface/ResultsRoute.js +59 -34
- 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 +182 -114
- package/dist/admin/surface/TestsSurface.js +56 -16
- 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 +24 -16
- package/dist/admin/surface/goals/goals.css +10 -13
- package/dist/admin/surface/home.model.d.ts +8 -8
- package/dist/admin/surface/home.model.js +23 -12
- package/dist/admin/surface/newTest/NewTest.js +9 -9
- package/dist/admin/surface/pageBlock/parts.js +31 -26
- package/dist/admin/surface/readiness/ActionDialogs.js +47 -19
- package/dist/admin/surface/readiness/Banner.d.ts +3 -0
- package/dist/admin/surface/readiness/Banner.js +58 -33
- 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 +25 -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 +11 -1
- package/dist/admin/surface/results/ResultsBody.js +277 -105
- 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.d.ts +3 -2
- package/dist/admin/surface/results/recommendation.model.js +12 -9
- 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 +61 -47
- package/dist/admin/surface/test/AdvancedSection.js +55 -48
- package/dist/admin/surface/test/GoalSection.js +46 -47
- 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 -68
- 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/test/testPage.model.d.ts +2 -3
- package/dist/admin/surface/test/testPage.model.js +1 -21
- package/dist/admin/surface/useTestDocument.js +12 -5
- 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/useLayerUsage.js +14 -23
- package/dist/admin/views/ExperimentsRootView.js +12 -2
- package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
- package/dist/admin/views/TestsNavLinkServer.js +43 -0
- package/dist/arms/copy.d.ts +6 -0
- package/dist/arms/copy.js +16 -0
- package/dist/arms/model.d.ts +23 -0
- package/dist/arms/model.js +173 -0
- package/dist/collections/experiments/config.js +25 -0
- package/dist/collections/experiments/lockHook.js +3 -1
- 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/contentDone.d.ts +4 -0
- package/dist/contentDone.js +5 -0
- package/dist/db/appends.d.ts +6 -2
- package/dist/db/appends.js +10 -4
- package/dist/db/identities.d.ts +8 -7
- package/dist/db/identities.js +9 -6
- package/dist/db/locks.d.ts +6 -0
- package/dist/db/locks.js +13 -1
- 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/pageOccupancy.d.ts +12 -0
- package/dist/db/pageOccupancy.js +45 -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 +25 -8
- package/dist/db/sweep.js +11 -6
- package/dist/endpoints/index.js +19 -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/adminRsc.d.ts +1 -0
- package/dist/exports/adminRsc.js +1 -0
- package/dist/exports/index.d.ts +2 -1
- package/dist/exports/index.js +1 -0
- package/dist/exports/puck.d.ts +2 -1
- package/dist/exports/puck.js +1 -1
- package/dist/exports/puckEditor.d.ts +4 -1
- package/dist/exports/puckEditor.js +3 -1
- package/dist/exports/rsc.d.ts +6 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/hooks/pageGuard.d.ts +5 -0
- package/dist/hooks/pageGuard.js +139 -0
- package/dist/lib/fetchAllDocs.d.ts +16 -0
- package/dist/lib/fetchAllDocs.js +56 -0
- package/dist/log.js +6 -3
- package/dist/manifest/compile.d.ts +5 -0
- package/dist/manifest/compile.js +5 -1
- package/dist/options.js +16 -0
- package/dist/plugin.js +30 -2
- package/dist/puck/ExperimentPicker.js +47 -16
- package/dist/puck/ExperimentSlot.d.ts +51 -1
- package/dist/puck/ExperimentSlot.js +23 -12
- package/dist/puck/copy.d.ts +6 -5
- package/dist/puck/copy.js +6 -5
- package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
- package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
- package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
- package/dist/puck/editor/ResultsReadout.d.ts +6 -0
- package/dist/puck/editor/ResultsReadout.js +99 -0
- package/dist/puck/editor/VersionBuilder.d.ts +34 -0
- package/dist/puck/editor/VersionBuilder.js +317 -0
- package/dist/puck/editor/armWrites.d.ts +7 -0
- package/dist/puck/editor/armWrites.js +71 -0
- package/dist/puck/editor/armsCache.d.ts +14 -0
- package/dist/puck/editor/armsCache.js +78 -0
- package/dist/puck/editor/copy.d.ts +33 -1
- package/dist/puck/editor/copy.js +45 -8
- package/dist/puck/editor/createTest.d.ts +1 -1
- package/dist/puck/editor/createTest.js +25 -14
- package/dist/puck/editor/editor.chrome.css +121 -0
- package/dist/puck/editor/editor.css +288 -204
- package/dist/puck/editor/editor.model.d.ts +4 -1
- package/dist/puck/editor/editor.model.js +10 -7
- package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
- package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
- package/dist/puck/experimentSlotField.d.ts +2 -0
- package/dist/puck/experimentSlotField.js +11 -3
- package/dist/puck/hostContext.d.ts +10 -0
- package/dist/puck/hostContext.js +12 -0
- package/dist/puck/routes.d.ts +12 -0
- package/dist/puck/routes.js +11 -0
- package/dist/puck/slotConfig.d.ts +1 -1
- package/dist/puck/slotConfig.js +2 -2
- 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/sanitizeReadinessDetail.d.ts +7 -0
- package/dist/sanitizeReadinessDetail.js +63 -0
- package/dist/server/linkIdentity.d.ts +10 -1
- package/dist/server/linkIdentity.js +17 -15
- package/dist/server/metadata.d.ts +5 -0
- package/dist/server/metadata.js +18 -3
- package/dist/server/normaliseContentRelationships.d.ts +7 -0
- package/dist/server/normaliseContentRelationships.js +62 -0
- package/dist/server/recordContentChange.d.ts +1 -1
- package/dist/server/recordContentChange.js +17 -8
- package/dist/server/runResults.d.ts +3 -2
- package/dist/server/runResults.js +157 -35
- 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 +10 -3
- package/dist/transitions/checkStart.js +25 -0
- package/dist/transitions/contentFields.d.ts +7 -0
- package/dist/transitions/contentFields.js +20 -0
- package/dist/transitions/decide.js +22 -2
- 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 +3 -6
- package/dist/transitions/placements.js +12 -1
- package/dist/transitions/preflightData.d.ts +9 -1
- package/dist/transitions/preflightData.js +138 -20
- 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 +62 -7
- package/dist/types/contracts.d.ts +24 -4
- 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 +16 -0
- package/dist/ui/Tabs.js +61 -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 +40 -0
- package/dist/ui/accent.js +139 -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 +5 -2
- 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 -70
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { createUsePuck, walkTree } from '@puckeditor/core';
|
|
4
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { EXPERIMENT_SLOT_TYPE } from '../../types/puck.js';
|
|
6
|
+
import { useArms } from './armsCache.js';
|
|
7
|
+
const usePuckSelector = createUsePuck();
|
|
8
|
+
const slots = [
|
|
9
|
+
'control',
|
|
10
|
+
'variantA',
|
|
11
|
+
'variantB',
|
|
12
|
+
'variantC'
|
|
13
|
+
];
|
|
14
|
+
// Puck 0.24.0 dist/chunk-DQXMVPVM.mjs:8040 (item attribute), 8126–8135
|
|
15
|
+
// (direct zone containers). Zones have no stable attribute; avoid the CSS-module hash.
|
|
16
|
+
const zone = 'div[class*="Layer-zones"]';
|
|
17
|
+
const chain = (count)=>Array(count).fill(zone).join(' ~ ');
|
|
18
|
+
function SlotRules({ item: [id, testId, emptySlots] }) {
|
|
19
|
+
const known = useArms(testId ?? undefined);
|
|
20
|
+
const [mounted, setMounted] = useState(false);
|
|
21
|
+
useEffect(()=>{
|
|
22
|
+
setMounted(true);
|
|
23
|
+
}, []);
|
|
24
|
+
// CSS.escape is a browser API: SSR and the first hydration render emit no rules.
|
|
25
|
+
if (!mounted) return null;
|
|
26
|
+
const rules = slots.flatMap((slot, index)=>{
|
|
27
|
+
// Original always remains available. Unknown bound tests must reveal every zone.
|
|
28
|
+
if (slot === 'control' || testId !== null && (!known || known.arms.includes(slot))) return [];
|
|
29
|
+
if (!emptySlots[index]) return [];
|
|
30
|
+
// Exactly the nth zone sibling: the positive chain matches n onward and the
|
|
31
|
+
// negated chain excludes n+1 onward. Unlike nth-child, DropLine siblings do not count.
|
|
32
|
+
const selector = `li[data-puck-layer-tree-id="${CSS.escape(id)}"] > ${chain(index + 1)}:not(${chain(index + 2)})`;
|
|
33
|
+
return [
|
|
34
|
+
`${selector} { display: none; }`
|
|
35
|
+
];
|
|
36
|
+
}).join('\n');
|
|
37
|
+
return /*#__PURE__*/ _jsx("style", {
|
|
38
|
+
"data-xp-outline": id,
|
|
39
|
+
children: rules
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function ExperimentOutline({ children }) {
|
|
43
|
+
// Public selector API in Puck 0.24, rather than the deprecated usePuck hook.
|
|
44
|
+
const key = usePuckSelector((state)=>{
|
|
45
|
+
const { data } = state.appState;
|
|
46
|
+
const items = new Map();
|
|
47
|
+
const collect = (content)=>{
|
|
48
|
+
for (const item of content)if (item.type === EXPERIMENT_SLOT_TYPE) {
|
|
49
|
+
const testId = item.props.experiment?.id;
|
|
50
|
+
items.set(item.props.id, [
|
|
51
|
+
item.props.id,
|
|
52
|
+
testId === undefined ? null : String(testId),
|
|
53
|
+
slots.map((slot)=>{
|
|
54
|
+
const content = item.props[slot];
|
|
55
|
+
return !Array.isArray(content) || content.length === 0;
|
|
56
|
+
})
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walkTree(data, state.config, (content)=>{
|
|
61
|
+
collect(content);
|
|
62
|
+
});
|
|
63
|
+
// walkTree visits slots inside legacy zones, but does not call back on the zone itself.
|
|
64
|
+
for (const content of Object.values(data.zones ?? {}))collect(content);
|
|
65
|
+
return JSON.stringify([
|
|
66
|
+
...items.values()
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
// Only binding/emptiness changes rebuild the rules, not edits to other page fields.
|
|
70
|
+
const rules = useMemo(()=>JSON.parse(key).map((item)=>/*#__PURE__*/ _jsx(SlotRules, {
|
|
71
|
+
item: item
|
|
72
|
+
}, item[0])), [
|
|
73
|
+
key
|
|
74
|
+
]);
|
|
75
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
76
|
+
children: [
|
|
77
|
+
children,
|
|
78
|
+
rules
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Add to Puck's plugins so its outline wrapper composes with the host's overrides. */ export function experimentOutlinePlugin() {
|
|
83
|
+
return {
|
|
84
|
+
name: 'experiment-outline',
|
|
85
|
+
overrides: {
|
|
86
|
+
outline: ExperimentOutline
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CustomField } from '@puckeditor/core';
|
|
2
2
|
import type { ExperimentRef } from '../types/puck.js';
|
|
3
|
+
import { type PuckRoutes } from './routes.js';
|
|
3
4
|
export declare function createExperimentSlotField(opts?: {
|
|
4
5
|
label?: string;
|
|
6
|
+
routes?: PuckRoutes;
|
|
5
7
|
}): CustomField<ExperimentRef | null>;
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { createElement } from 'react';
|
|
2
2
|
import { ExperimentPicker } from './ExperimentPicker.js';
|
|
3
|
+
import { resolvePuckRoutes } from './routes.js';
|
|
4
|
+
import { PuckHostProvider } from './hostContext.js';
|
|
3
5
|
import { pickerCopy } from './copy.js';
|
|
4
6
|
export function createExperimentSlotField(opts) {
|
|
7
|
+
const routes = resolvePuckRoutes(opts?.routes);
|
|
5
8
|
return {
|
|
6
9
|
type: 'custom',
|
|
7
10
|
label: opts?.label ?? pickerCopy.label,
|
|
8
|
-
render: ({ value, onChange })=>createElement(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
render: ({ value, onChange })=>createElement(PuckHostProvider, {
|
|
12
|
+
routes,
|
|
13
|
+
children: createElement('div', {
|
|
14
|
+
className: 'xp-puck-field'
|
|
15
|
+
}, createElement(ExperimentPicker, {
|
|
16
|
+
value,
|
|
17
|
+
onChange
|
|
18
|
+
}))
|
|
11
19
|
})
|
|
12
20
|
};
|
|
13
21
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolvePuckRoutes } from './routes.js';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
export declare const usePuckRoutes: () => {
|
|
4
|
+
admin: string;
|
|
5
|
+
api: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function PuckHostProvider({ routes, children }: {
|
|
8
|
+
routes: ReturnType<typeof resolvePuckRoutes>;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
import { resolvePuckRoutes } from './routes.js';
|
|
5
|
+
const PuckHostContext = /*#__PURE__*/ createContext(resolvePuckRoutes());
|
|
6
|
+
export const usePuckRoutes = ()=>useContext(PuckHostContext);
|
|
7
|
+
export function PuckHostProvider({ routes, children }) {
|
|
8
|
+
return /*#__PURE__*/ _jsx(PuckHostContext.Provider, {
|
|
9
|
+
value: routes,
|
|
10
|
+
children: children
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PuckRoutes {
|
|
2
|
+
admin?: string;
|
|
3
|
+
api?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const DEFAULT_PUCK_ROUTES: {
|
|
6
|
+
readonly admin: "/admin";
|
|
7
|
+
readonly api: "/api";
|
|
8
|
+
};
|
|
9
|
+
export declare function resolvePuckRoutes(routes?: PuckRoutes): {
|
|
10
|
+
admin: string;
|
|
11
|
+
api: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Payload's documented route defaults; hosts pass config.routes when customized.
|
|
2
|
+
export const DEFAULT_PUCK_ROUTES = {
|
|
3
|
+
admin: '/admin',
|
|
4
|
+
api: '/api'
|
|
5
|
+
};
|
|
6
|
+
export function resolvePuckRoutes(routes) {
|
|
7
|
+
return {
|
|
8
|
+
admin: (routes?.admin ?? DEFAULT_PUCK_ROUTES.admin).replace(/\/$/, ''),
|
|
9
|
+
api: (routes?.api ?? DEFAULT_PUCK_ROUTES.api).replace(/\/$/, '')
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentConfig } from '@puckeditor/core';
|
|
2
2
|
import type { ExperimentSlotProps } from '../types/puck.js';
|
|
3
|
-
export declare const experimentSlotLabel
|
|
3
|
+
export declare const experimentSlotLabel = "Experiment block";
|
|
4
4
|
export declare const experimentSlotFields: NonNullable<ComponentConfig<ExperimentSlotProps>['fields']>;
|
|
5
5
|
export declare const experimentSlotDefaultProps: NonNullable<ComponentConfig<ExperimentSlotProps>['defaultProps']>;
|
package/dist/puck/slotConfig.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createExperimentSlotField } from './experimentSlotField.js';
|
|
2
|
-
import {
|
|
3
|
-
export const experimentSlotLabel =
|
|
2
|
+
import { slotLabels } from './copy.js';
|
|
3
|
+
export const experimentSlotLabel = 'Experiment block';
|
|
4
4
|
export const experimentSlotFields = {
|
|
5
5
|
experiment: createExperimentSlotField(),
|
|
6
6
|
// Puck 0.24's defaultSlots prepends each slot, reversing declaration order in the outline.
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { XpAnalysisError } from '../errors.js';
|
|
1
2
|
/** Accept Date, timestamp text, or epoch milliseconds; reject invalid dates. */ export function timestampMs(ts) {
|
|
2
3
|
if (!(ts instanceof Date) && typeof ts !== 'string' && typeof ts !== 'number') {
|
|
3
|
-
throw new
|
|
4
|
+
throw new XpAnalysisError('invalid-timestamp');
|
|
4
5
|
}
|
|
5
6
|
const ms = ts instanceof Date ? ts.getTime() : new Date(ts).getTime();
|
|
6
|
-
if (!Number.isFinite(ms)) throw new
|
|
7
|
+
if (!Number.isFinite(ms)) throw new XpAnalysisError('invalid-timestamp');
|
|
7
8
|
return ms;
|
|
8
9
|
}
|
|
9
10
|
/** Attribution windows must have a finite, positive length in hours. */ export function windowMs(windowHours) {
|
|
10
11
|
if (!Number.isFinite(windowHours) || windowHours <= 0) {
|
|
11
|
-
throw new
|
|
12
|
+
throw new XpAnalysisError('window-hours');
|
|
12
13
|
}
|
|
13
14
|
return windowHours * 3_600_000;
|
|
14
15
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GaussianPrior } from '@delmaredigital/experiments-stats';
|
|
2
|
+
import type { MetricUnavailableReason } from '../types/reasons.js';
|
|
2
3
|
import type { Arm, DocId, MetricSource } from '../types/contracts.js';
|
|
3
4
|
import type { Snapshot } from '../types/snapshot.js';
|
|
4
5
|
import type { ExposedUnit, Timestamp } from './aggregate.js';
|
|
@@ -55,10 +56,12 @@ export interface MetricUnitData {
|
|
|
55
56
|
values: ReadonlyMap<string, number>;
|
|
56
57
|
/** Host pre-period outcomes; absent entries in a supplied map mean zero. */
|
|
57
58
|
covariates?: ReadonlyMap<string, number>;
|
|
58
|
-
unavailable?:
|
|
59
|
+
unavailable?: MetricUnavailableReason;
|
|
59
60
|
}
|
|
60
61
|
export type AnalysePhaseInput = {
|
|
61
62
|
phase: AnalysisPhase;
|
|
63
|
+
/** The runner logs failures; injecting this keeps analysis free of I/O imports. */
|
|
64
|
+
onMetricError?: (error: unknown, metricId: DocId) => void;
|
|
62
65
|
status: 'running' | 'stopped' | 'decided';
|
|
63
66
|
now: Timestamp;
|
|
64
67
|
/** CORE-14a firstExposures output; same-unit multi-arm units already excluded. */
|
package/dist/results/analyse.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { XpAnalysisError } from '../errors.js';
|
|
1
2
|
import { DEFAULT_GAUSSIAN_PRIOR, bonferroniAlpha, capValues, createThetaAdjustedStatistics, effectBayesianABTest, sequentialOneSidedTreatmentGreaterTTest, sequentialOneSidedTreatmentLesserTTest, sequentialTwoSidedTTest, statisticMean, statisticVariance } from '@delmaredigital/experiments-stats';
|
|
2
3
|
import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
|
|
3
4
|
import { timestampMs } from './aggregate.js';
|
|
@@ -18,8 +19,8 @@ const severity = {
|
|
|
18
19
|
*/ export function analysePhase(input) {
|
|
19
20
|
const { phase, units } = input;
|
|
20
21
|
const { analysis } = phase;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
22
|
+
// Compatibility for phases created before Start froze the effective tuning
|
|
23
|
+
// value. Record that effective value in every snapshot, including n = 0.
|
|
23
24
|
const sequentialTuningParameter = analysis.sequentialTuningParameter ?? DEFAULT_SEQUENTIAL_TUNING_PARAMETER;
|
|
24
25
|
const alpha = analysis.alpha ?? 0.05;
|
|
25
26
|
const prior = analysis.prior ?? DEFAULT_GAUSSIAN_PRIOR;
|
|
@@ -30,7 +31,7 @@ const severity = {
|
|
|
30
31
|
const rollout = phase.phaseKind === 'rollout';
|
|
31
32
|
const baselineArm = phase.arms.find((arm)=>arm.kind === (rollout ? 'holdout' : 'control'));
|
|
32
33
|
const winner = rollout ? phase.arms.find((arm)=>arm.key === input.winnerArm && arm.kind !== 'holdout') : undefined;
|
|
33
|
-
if (!baselineArm || rollout && (!winner || phase.arms.length !== 2)) throw new
|
|
34
|
+
if (!baselineArm || rollout && (!winner || phase.arms.length !== 2)) throw new XpAnalysisError('no-baseline');
|
|
34
35
|
const compared = phase.arms.filter((arm)=>rollout ? arm.key === winner.key : arm.kind === 'variant');
|
|
35
36
|
const k = compared.length;
|
|
36
37
|
const alphaAdjusted = analysis.method === 'sequential' ? bonferroniAlpha(alpha, k) : alpha;
|
|
@@ -42,22 +43,22 @@ const severity = {
|
|
|
42
43
|
}));
|
|
43
44
|
const primaryDefinition = definitions.find((metric)=>metric.role === 'primary');
|
|
44
45
|
if (!primaryDefinition || phase.metricDefinitions.filter((metric)=>metric.role === 'primary').length !== 1) {
|
|
45
|
-
throw new
|
|
46
|
+
throw new XpAnalysisError('primary-metric-count');
|
|
46
47
|
}
|
|
47
48
|
if (new Set(phase.metricDefinitions.map((metric)=>String(metric.metricId))).size !== phase.metricDefinitions.length || new Set(input.metricData.map((metric)=>String(metric.metricId))).size !== input.metricData.length) {
|
|
48
|
-
throw new
|
|
49
|
+
throw new XpAnalysisError('duplicate-metric');
|
|
49
50
|
}
|
|
50
51
|
const from = timestampMs(phase.startedAt);
|
|
51
52
|
const now = timestampMs(input.now);
|
|
52
53
|
const to = Math.min(now, phase.endedAt == null ? now : timestampMs(phase.endedAt));
|
|
53
|
-
if (to < from) throw new
|
|
54
|
+
if (to < from) throw new XpAnalysisError('window-before-start');
|
|
54
55
|
const byArm = new Map(phase.arms.map((arm)=>[
|
|
55
56
|
arm.key,
|
|
56
57
|
[]
|
|
57
58
|
]));
|
|
58
59
|
for (const [id, unit] of units){
|
|
59
60
|
const ids = byArm.get(unit.arm);
|
|
60
|
-
if (!ids) throw new
|
|
61
|
+
if (!ids) throw new XpAnalysisError('unknown-arm');
|
|
61
62
|
ids.push(id);
|
|
62
63
|
}
|
|
63
64
|
const perArm = Object.fromEntries([
|
|
@@ -120,17 +121,19 @@ const severity = {
|
|
|
120
121
|
let worstArmLabel = compared[0]?.label ?? '';
|
|
121
122
|
let worstArm = compared[0]?.key ?? '';
|
|
122
123
|
try {
|
|
123
|
-
if (!data || data.unavailable !== undefined)
|
|
124
|
+
if (!data || data.unavailable !== undefined) {
|
|
125
|
+
throw new XpAnalysisError('metric-not-supplied');
|
|
126
|
+
}
|
|
124
127
|
const stats = new Map();
|
|
125
128
|
const cap = kind === 'revenue' ? definition.resolvedCap ?? null : null;
|
|
126
129
|
const useCovariates = analysis.cuped === 'auto' && data.covariates !== undefined && data.covariates.size > 0;
|
|
127
130
|
for (const arm of phase.arms){
|
|
128
131
|
const ids = byArm.get(arm.key);
|
|
129
132
|
const values = ids.map((id)=>data.values.get(id) ?? 0);
|
|
130
|
-
if (!values.every(Number.isFinite)) throw new
|
|
133
|
+
if (!values.every(Number.isFinite)) throw new XpAnalysisError('non-finite-values');
|
|
131
134
|
const capped = capValues(values, cap);
|
|
132
135
|
const rawPre = useCovariates ? ids.map((id)=>data.covariates.get(id) ?? 0) : undefined;
|
|
133
|
-
if (rawPre && !rawPre.every(Number.isFinite)) throw new
|
|
136
|
+
if (rawPre && !rawPre.every(Number.isFinite)) throw new XpAnalysisError('non-finite-covariates');
|
|
134
137
|
const pre = rawPre && capValues(rawPre, cap).values;
|
|
135
138
|
const built = outcomeStatistic(kind, capped.values, pre);
|
|
136
139
|
stats.set(arm.key, built.statistic);
|
|
@@ -257,7 +260,20 @@ const severity = {
|
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
262
|
} catch (error) {
|
|
260
|
-
|
|
263
|
+
input.onMetricError?.(error, metricId);
|
|
264
|
+
let reason = 'analysis-failed';
|
|
265
|
+
if (error instanceof XpAnalysisError) {
|
|
266
|
+
switch(error.code){
|
|
267
|
+
case 'metric-not-supplied':
|
|
268
|
+
reason = data?.unavailable ?? 'not-supplied';
|
|
269
|
+
break;
|
|
270
|
+
case 'non-finite-values':
|
|
271
|
+
case 'non-finite-covariates':
|
|
272
|
+
reason = error.code;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
metric.unavailable = reason;
|
|
261
277
|
metric.perArm = [];
|
|
262
278
|
metric.comparisons = [];
|
|
263
279
|
metric.cupedApplied = false;
|
package/dist/results/cuped.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { XpAnalysisError } from '../errors.js';
|
|
1
2
|
import { buildRegressionAdjustedStatistic, buildStatistic, summarizeUnits, summarizeUnitsWithCovariate } from '@delmaredigital/experiments-stats';
|
|
2
3
|
/** Build candidate CUPED statistics; the stats engine pools each pair, infers theta,
|
|
3
4
|
* and falls back to the post statistic when pre-period variance/covariance is zero.
|
|
4
5
|
* Missing units in a supplied pre-period map are zero-filled by the caller.
|
|
5
6
|
*/ export function outcomeStatistic(kind, values, covariates) {
|
|
6
7
|
if (!values.every(Number.isFinite) || covariates && !covariates.every(Number.isFinite)) {
|
|
7
|
-
throw new
|
|
8
|
+
throw new XpAnalysisError('cuped-non-finite');
|
|
8
9
|
}
|
|
9
10
|
if (kind === 'binomial' && (values.some((value)=>value !== 0 && value !== 1) || covariates?.some((value)=>value !== 0 && value !== 1))) {
|
|
10
|
-
throw new
|
|
11
|
+
throw new XpAnalysisError('cuped-binomial');
|
|
11
12
|
}
|
|
12
13
|
const moments = summarizeUnits(values);
|
|
13
14
|
if (covariates === undefined) return {
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { ExposedUnit, MetricEvent, Timestamp } from './aggregate.js';
|
|
2
|
-
|
|
3
|
-
* Spec §8.3: credit once to the earliest exposed linked visitor; conflicts are diagnostic only.
|
|
4
|
-
* Callers must pass user ids through String() for both identities and user-keyed source events.
|
|
5
|
-
*/
|
|
6
|
-
export declare function creditUserEvents(units: ReadonlyMap<string, ExposedUnit>, identities: readonly {
|
|
2
|
+
export interface IdentityLink {
|
|
7
3
|
anonymousId: string;
|
|
8
4
|
userId: string;
|
|
9
|
-
|
|
5
|
+
verifiedAt: Timestamp | null;
|
|
6
|
+
/** Earliest verification across all the user's browsers, exposed or not. */
|
|
7
|
+
ownedAt: Timestamp | null;
|
|
8
|
+
}
|
|
9
|
+
/** Unverified links credit only events before anyone proves account ownership.
|
|
10
|
+
* Never-verified accounts keep crediting without a time limit.
|
|
11
|
+
*/
|
|
12
|
+
export declare function eligibleIdentityLinks<T extends IdentityLink>(links: readonly T[], ts: Timestamp): T[];
|
|
13
|
+
/** Credit each event to the earliest exposed eligible visitor, with a stable ID
|
|
14
|
+
* tie-break. A user counts once if eligible links span arms for a credited event.
|
|
15
|
+
* Callers must normalize user IDs with String().
|
|
16
|
+
*/
|
|
17
|
+
export declare function creditUserEvents(units: ReadonlyMap<string, ExposedUnit>, identities: readonly IdentityLink[], userEvents: readonly {
|
|
10
18
|
userId: string;
|
|
11
19
|
value: number;
|
|
12
20
|
ts: Timestamp;
|
|
13
|
-
}[]): {
|
|
21
|
+
}[], onConflict?: (userId: string) => void): {
|
|
14
22
|
events: MetricEvent[];
|
|
15
23
|
conflicts: number;
|
|
16
24
|
};
|
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
import { timestampMs } from './aggregate.js';
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/** Unverified links credit only events before anyone proves account ownership.
|
|
3
|
+
* Never-verified accounts keep crediting without a time limit.
|
|
4
|
+
*/ export function eligibleIdentityLinks(links, ts) {
|
|
5
|
+
const t = timestampMs(ts);
|
|
6
|
+
return links.filter((link)=>link.verifiedAt !== null || link.ownedAt === null || t < timestampMs(link.ownedAt));
|
|
7
|
+
}
|
|
8
|
+
/** Credit each event to the earliest exposed eligible visitor, with a stable ID
|
|
9
|
+
* tie-break. A user counts once if eligible links span arms for a credited event.
|
|
10
|
+
* Callers must normalize user IDs with String().
|
|
11
|
+
*/ export function creditUserEvents(units, identities, userEvents, onConflict) {
|
|
12
|
+
const starts = new Map([
|
|
13
|
+
...units
|
|
14
|
+
].map(([id, unit])=>[
|
|
15
|
+
id,
|
|
16
|
+
timestampMs(unit.firstTs)
|
|
17
|
+
]));
|
|
6
18
|
const users = new Map();
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const firstTs = starts.get(anonymousId);
|
|
13
|
-
const user = users.get(userId);
|
|
14
|
-
if (!user) users.set(userId, {
|
|
15
|
-
unitId: anonymousId,
|
|
16
|
-
firstTs,
|
|
17
|
-
arms: new Set([
|
|
18
|
-
unit.arm
|
|
19
|
-
])
|
|
20
|
-
});
|
|
21
|
-
else {
|
|
22
|
-
user.arms.add(unit.arm);
|
|
23
|
-
// Stable tie-break for simultaneous exposures, independent of identity row order.
|
|
24
|
-
if (firstTs < user.firstTs || firstTs === user.firstTs && anonymousId < user.unitId) {
|
|
25
|
-
user.unitId = anonymousId;
|
|
26
|
-
user.firstTs = firstTs;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
19
|
+
for (const link of identities){
|
|
20
|
+
if (!units.has(link.anonymousId)) continue;
|
|
21
|
+
const links = users.get(link.userId) ?? [];
|
|
22
|
+
links.push(link);
|
|
23
|
+
users.set(link.userId, links);
|
|
29
24
|
}
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
for (const links of users.values())links.sort((a, b)=>starts.get(a.anonymousId) - starts.get(b.anonymousId) || (a.anonymousId < b.anonymousId ? -1 : a.anonymousId > b.anonymousId ? 1 : 0));
|
|
26
|
+
const conflicts = new Set();
|
|
32
27
|
const events = [];
|
|
33
28
|
for (const event of userEvents){
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
unitId:
|
|
29
|
+
const eligible = eligibleIdentityLinks(users.get(event.userId) ?? [], event.ts);
|
|
30
|
+
if (!eligible.length) continue;
|
|
31
|
+
events.push({
|
|
32
|
+
unitId: eligible[0].anonymousId,
|
|
38
33
|
value: event.value,
|
|
39
34
|
ts: event.ts
|
|
40
35
|
});
|
|
36
|
+
if (new Set(eligible.map((link)=>units.get(link.anonymousId).arm)).size > 1) {
|
|
37
|
+
conflicts.add(event.userId);
|
|
38
|
+
onConflict?.(event.userId);
|
|
39
|
+
}
|
|
41
40
|
}
|
|
42
41
|
return {
|
|
43
42
|
events,
|
|
44
|
-
conflicts
|
|
43
|
+
conflicts: conflicts.size
|
|
45
44
|
};
|
|
46
45
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { XpAnalysisError } from '../errors.js';
|
|
1
2
|
import { hash, layerSlice, rolloutBucketRanges } from '@delmaredigital/experiments-stats';
|
|
2
3
|
import { timestampMs } from './aggregate.js';
|
|
3
4
|
/** Spec §7.6/§8.1.5 (X1): first-ever rollout exposures inside assignment's exact ranges. */ export function rolloutAnalysisUnits({ exposuresAllPhases, rolloutPhase, seed, testWeights, coverage, layer }) {
|
|
4
|
-
if (!seed.endsWith('|')) throw new
|
|
5
|
+
if (!seed.endsWith('|')) throw new XpAnalysisError('seed-format');
|
|
5
6
|
const first = new Map();
|
|
6
7
|
for (const row of exposuresAllPhases){
|
|
7
8
|
const ts = timestampMs(row.ts);
|
package/dist/results/snapshot.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { XpAnalysisError } from '../errors.js';
|
|
1
2
|
/** JSON cannot represent unbounded confidence limits. */ export function jsonInterval(ci) {
|
|
2
3
|
return ci.map((bound)=>bound !== null && Number.isFinite(bound) ? bound : null);
|
|
3
4
|
}
|
|
@@ -138,14 +139,19 @@ const shape = object({
|
|
|
138
139
|
}),
|
|
139
140
|
by: nullable(id)
|
|
140
141
|
}))
|
|
142
|
+
}, {
|
|
143
|
+
health: list(object({
|
|
144
|
+
code: oneOf('version-page-unavailable', 'version-page-unchecked'),
|
|
145
|
+
path: string
|
|
146
|
+
}))
|
|
141
147
|
});
|
|
142
148
|
/** Validate the frozen JSON shape and headline/rollout invariants before storage.
|
|
143
149
|
* This also accepts the shared CORE-13/admin fixtures without any I/O.
|
|
144
150
|
*/ export function validateSnapshot(value) {
|
|
145
|
-
if (!shape(value)) throw new
|
|
151
|
+
if (!shape(value)) throw new XpAnalysisError('snapshot-shape');
|
|
146
152
|
const snapshot = value;
|
|
147
153
|
if (snapshot.phaseKind === 'test' && snapshot.winnerArm !== null || snapshot.phaseKind === 'rollout' && (snapshot.winnerArm === null || !(snapshot.winnerArm in snapshot.exposures.perArm)) || snapshot.verdict.headline !== snapshot.verdict.finding + (snapshot.verdict.action ? ` ${snapshot.verdict.action}` : '') || new Date(snapshot.window.to).getTime() < new Date(snapshot.window.from).getTime()) {
|
|
148
|
-
throw new
|
|
154
|
+
throw new XpAnalysisError('snapshot-invariants');
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
/** Assemble the already-computed snapshot at the pure/I/O boundary. */ export function assembleSnapshot(snapshot) {
|
package/dist/results/srm.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { XpAnalysisError } from '../errors.js';
|
|
1
2
|
import { checkSrm } from '@delmaredigital/experiments-stats';
|
|
2
3
|
/** SRM uses exposed units and renormalised frozen weights, never allocation or
|
|
3
4
|
* assignment counts. Rollout callers pass only shipped/holdout weights (100-h,h).
|
|
@@ -5,7 +6,7 @@ import { checkSrm } from '@delmaredigital/experiments-stats';
|
|
|
5
6
|
*/ export function exposureSrm(arms, perArm) {
|
|
6
7
|
const active = arms.filter((arm)=>arm.weight > 0);
|
|
7
8
|
const weight = active.reduce((sum, arm)=>sum + arm.weight, 0);
|
|
8
|
-
if (weight <= 0) throw new
|
|
9
|
+
if (weight <= 0) throw new XpAnalysisError('srm-weights');
|
|
9
10
|
const expected = Object.fromEntries(arms.map((arm)=>[
|
|
10
11
|
arm.key,
|
|
11
12
|
arm.weight / weight
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReadinessCode } from './types/endpoints.js';
|
|
2
|
+
/** Summary details contain identifiers only. Project known shapes rather than
|
|
3
|
+
* recursively copying host objects: titles, messages and placement detail are text.
|
|
4
|
+
* Other checks use authored client fallbacks and need no detail (including slot
|
|
5
|
+
* counts and layer capacity, which are not identifiers).
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeReadinessDetail(code: ReadinessCode, detail: unknown): unknown;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const object = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
2
|
+
// The runtime's slug/key alphabet, bounded even for host-provided parameter keys.
|
|
3
|
+
const key = (value)=>typeof value === 'string' && value.length <= 128 && /^[A-Za-z0-9_-]+$/.test(value);
|
|
4
|
+
const arm = (value)=>typeof value === 'string' && [
|
|
5
|
+
'control',
|
|
6
|
+
'variantA',
|
|
7
|
+
'variantB',
|
|
8
|
+
'variantC',
|
|
9
|
+
'holdout'
|
|
10
|
+
].includes(value);
|
|
11
|
+
const path = (value)=>typeof value === 'string' && value.length <= 128 && value.startsWith('/') && !value.includes('//') && !/[^A-Za-z0-9_/-]/.test(value);
|
|
12
|
+
const failure = (value)=>value === 'placements-failed' || value === 'forbidden' || value === 'not-found';
|
|
13
|
+
/** Summary details contain identifiers only. Project known shapes rather than
|
|
14
|
+
* recursively copying host objects: titles, messages and placement detail are text.
|
|
15
|
+
* Other checks use authored client fallbacks and need no detail (including slot
|
|
16
|
+
* counts and layer capacity, which are not identifiers).
|
|
17
|
+
*/ export function sanitizeReadinessDetail(code, detail) {
|
|
18
|
+
switch(code){
|
|
19
|
+
case 'block-path-rule':
|
|
20
|
+
return object(detail) && Array.isArray(detail.rules) && detail.rules.length && detail.rules.every((index)=>Number.isSafeInteger(index) && index >= 0) ? {
|
|
21
|
+
rules: detail.rules
|
|
22
|
+
} : undefined;
|
|
23
|
+
case 'no-placements':
|
|
24
|
+
return failure(detail) ? detail : undefined;
|
|
25
|
+
case 'page-in-use':
|
|
26
|
+
{
|
|
27
|
+
if (!object(detail) || !Array.isArray(detail.paths)) return undefined;
|
|
28
|
+
const paths = detail.paths.filter(path);
|
|
29
|
+
return paths.length ? {
|
|
30
|
+
paths
|
|
31
|
+
} : undefined;
|
|
32
|
+
}
|
|
33
|
+
case 'page-target-unpublished':
|
|
34
|
+
// Paths only: no query, fragment, whitespace, URL authority or encoded text.
|
|
35
|
+
return path(detail) ? detail : undefined;
|
|
36
|
+
case 'flag-params-invalid':
|
|
37
|
+
{
|
|
38
|
+
if (!Array.isArray(detail) || !detail.length || !detail.every((item)=>object(item) && arm(item.arm) && key(item.field))) return undefined;
|
|
39
|
+
return detail.map((item)=>({
|
|
40
|
+
arm: item.arm,
|
|
41
|
+
field: item.field
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
case 'flag-placement-missing':
|
|
45
|
+
{
|
|
46
|
+
if (failure(detail)) return detail;
|
|
47
|
+
if (!object(detail) || !Array.isArray(detail.missing) || !detail.missing.every(arm) || !Array.isArray(detail.placements) || !detail.placements.every((item)=>object(item) && arm(item.arm))) return undefined;
|
|
48
|
+
return {
|
|
49
|
+
missing: detail.missing,
|
|
50
|
+
placements: detail.placements.map((item)=>({
|
|
51
|
+
arm: item.arm
|
|
52
|
+
}))
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
case 'undeclared-targeting-key':
|
|
56
|
+
case 'invalid-targeting-rule':
|
|
57
|
+
return object(detail) && Array.isArray(detail.attributes) && detail.attributes.length && detail.attributes.every(key) ? {
|
|
58
|
+
attributes: detail.attributes
|
|
59
|
+
} : undefined;
|
|
60
|
+
default:
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ExperimentsServerRuntime } from '../types/runtime.js';
|
|
2
|
+
import { type IdentityWriteOutcome } from '../db/identities.js';
|
|
3
|
+
export type LinkIdentityOutcome = IdentityWriteOutcome | 'failed';
|
|
4
|
+
/**
|
|
5
|
+
* verified: true means this request proves, in this browser, that the person owns
|
|
6
|
+
* the account: a magic-link or one-time-code callback, password sign-in, or OAuth
|
|
7
|
+
* callback. It does not mean "a session exists" or "the email is verified".
|
|
8
|
+
* Never throws into registration or sign-in.
|
|
9
|
+
*/
|
|
2
10
|
export declare function linkIdentity<TUser extends {
|
|
3
11
|
id: string | number;
|
|
4
12
|
} = {
|
|
@@ -6,4 +14,5 @@ export declare function linkIdentity<TUser extends {
|
|
|
6
14
|
}>(serverRuntime: ExperimentsServerRuntime<TUser>, opts: {
|
|
7
15
|
headers: Headers;
|
|
8
16
|
userId: string;
|
|
9
|
-
|
|
17
|
+
verified: boolean;
|
|
18
|
+
}): Promise<LinkIdentityOutcome>;
|