@delmaredigital/payload-experiments 0.2.0-rc.7 → 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/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 +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/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 +5 -1
- package/dist/admin/results/results.model.js +10 -2
- 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 +183 -112
- 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 +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 +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 +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 +11 -1
- package/dist/admin/surface/results/ResultsBody.js +276 -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.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 -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/useTestDocument.js +10 -4
- 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 +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/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/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/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/pageGuard.d.ts +5 -0
- package/dist/hooks/pageGuard.js +121 -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 +23 -0
- package/dist/plugin.js +26 -2
- package/dist/puck/copy.d.ts +1 -1
- package/dist/puck/copy.js +1 -1
- 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/sanitizeReadinessDetail.d.ts +7 -0
- package/dist/sanitizeReadinessDetail.js +54 -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 +9 -1
- package/dist/server/runResults.d.ts +3 -2
- package/dist/server/runResults.js +121 -31
- 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/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 -70
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { XpAnalysisError } from '../errors.js';
|
|
1
2
|
import { safeError } from '../log.js';
|
|
3
|
+
import { createLocalReq } from 'payload';
|
|
2
4
|
import { resultsCandidates, resultsExecutor, writeSnapshots } from '../db/snapshots.js';
|
|
3
5
|
import { sweepEvents } from '../db/sweep.js';
|
|
4
6
|
import { armMismatchCount, identityRows, metricEventRows, phaseExposureRows, rolloutExposureRows } from '../db/results.js';
|
|
@@ -6,8 +8,8 @@ import { raceDeadline, ResultsBudgetError, RESULTS_TIMEOUT_MARGIN_MS } from '../
|
|
|
6
8
|
import { analysePhase } from '../results/analyse.js';
|
|
7
9
|
import { firstExposures, perUnitValues, timestampMs } from '../results/aggregate.js';
|
|
8
10
|
import { rolloutAnalysisUnits } from '../results/rolloutUnits.js';
|
|
9
|
-
import { creditUserEvents } from '../results/identityCredit.js';
|
|
10
|
-
async function analyseCandidate(exec, candidate, options, now, deadline, unavailableMetrics) {
|
|
11
|
+
import { creditUserEvents, eligibleIdentityLinks } from '../results/identityCredit.js';
|
|
12
|
+
async function analyseCandidate(exec, candidate, options, now, deadline, unavailableMetrics, logger) {
|
|
11
13
|
const { phase } = candidate;
|
|
12
14
|
// Rollout assignment preserves the original test ranges, but its SRM compares
|
|
13
15
|
// only the shipped group against holdout with weights {100-h, h}.
|
|
@@ -59,6 +61,7 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
|
|
|
59
61
|
...new Set(identities.map((identity)=>identity.userId))
|
|
60
62
|
];
|
|
61
63
|
const linked = new Set(userIds);
|
|
64
|
+
const identityConflicts = new Set();
|
|
62
65
|
const credit = (events, userKeyed)=>{
|
|
63
66
|
if (candidate.unitType === 'user') return events.map((event)=>({
|
|
64
67
|
...event,
|
|
@@ -69,13 +72,12 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
|
|
|
69
72
|
value: event.value,
|
|
70
73
|
ts: event.ts
|
|
71
74
|
}));
|
|
72
|
-
const credited = creditUserEvents(units, identities, userEvents).events;
|
|
75
|
+
const credited = creditUserEvents(units, identities, userEvents, (userId)=>identityConflicts.add(userId)).events;
|
|
73
76
|
return userKeyed ? credited : [
|
|
74
77
|
...events.filter((event)=>event.userId == null || !linked.has(event.userId)),
|
|
75
78
|
...credited
|
|
76
79
|
];
|
|
77
80
|
};
|
|
78
|
-
const diagnostics = creditUserEvents(units, identities, []);
|
|
79
81
|
const metricData = [];
|
|
80
82
|
for (const definition of phase.metricDefinitions){
|
|
81
83
|
try {
|
|
@@ -89,7 +91,7 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
|
|
|
89
91
|
let events, covariates;
|
|
90
92
|
if (definition.source.startsWith('events:')) {
|
|
91
93
|
const eventName = definition.source.slice(7);
|
|
92
|
-
if (!options.eventSources?.some((source)=>source.name === eventName)) throw new
|
|
94
|
+
if (!options.eventSources?.some((source)=>source.name === eventName)) throw new XpAnalysisError('source-unavailable');
|
|
93
95
|
const read = async (start)=>{
|
|
94
96
|
if (candidate.unitType === 'user') return metricEventRows(exec, {
|
|
95
97
|
eventName,
|
|
@@ -132,9 +134,9 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
|
|
|
132
134
|
}
|
|
133
135
|
} else {
|
|
134
136
|
const source = options.metricSources?.find((source)=>source.id === definition.source);
|
|
135
|
-
if (!source || source.kind !== kind || definition.keyedBy && source.keyedBy !== definition.keyedBy) throw new
|
|
137
|
+
if (!source || source.kind !== kind || definition.keyedBy && source.keyedBy !== definition.keyedBy) throw new XpAnalysisError('source-unavailable');
|
|
136
138
|
const userKeyed = source.keyedBy === 'user' && candidate.unitType === 'visitor';
|
|
137
|
-
if (candidate.unitType === 'user' && source.keyedBy !== 'user') throw new
|
|
139
|
+
if (candidate.unitType === 'user' && source.keyedBy !== 'user') throw new XpAnalysisError('unit-type-unavailable');
|
|
138
140
|
const sourceIds = userKeyed ? userIds : ids;
|
|
139
141
|
events = credit(await raceDeadline(deadline, ()=>source.query({
|
|
140
142
|
from,
|
|
@@ -142,35 +144,51 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
|
|
|
142
144
|
unitIds: sourceIds
|
|
143
145
|
})), userKeyed);
|
|
144
146
|
if (phase.analysis.cuped === 'auto' && source.covariate) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
const userUnits = new Map();
|
|
148
|
+
if (userKeyed) for (const link of identities){
|
|
149
|
+
const unit = units.get(link.anonymousId);
|
|
150
|
+
if (!unit) continue;
|
|
151
|
+
// Hosts aggregate pre-period values in SQL; per-event rows over 14 days would break the one-row-per-unit contract and multiply host traffic.
|
|
152
|
+
// CUPED only reduces variance: mis-credited pre-exposure values add noise, but cannot bias the effect because they are independent of assignment.
|
|
153
|
+
if (!eligibleIdentityLinks([
|
|
154
|
+
link
|
|
155
|
+
], unit.firstTs).length) continue;
|
|
156
|
+
const previous = userUnits.get(link.userId);
|
|
157
|
+
if (!previous || +unit.firstTs < +previous.firstExposureTs || +unit.firstTs === +previous.firstExposureTs && link.anonymousId < previous.unitId) {
|
|
158
|
+
userUnits.set(link.userId, {
|
|
159
|
+
unitId: link.anonymousId,
|
|
160
|
+
firstExposureTs: unit.firstTs
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const sourceUnits = userKeyed ? [
|
|
165
|
+
...userUnits
|
|
166
|
+
].map(([unitId, unit])=>({
|
|
167
|
+
unitId,
|
|
168
|
+
firstExposureTs: unit.firstExposureTs
|
|
152
169
|
})) : [
|
|
153
170
|
...units
|
|
154
171
|
].map(([unitId, unit])=>({
|
|
155
172
|
unitId,
|
|
156
|
-
firstExposureTs: unit.firstTs
|
|
173
|
+
firstExposureTs: new Date(timestampMs(unit.firstTs))
|
|
157
174
|
}));
|
|
158
175
|
const pre = await raceDeadline(deadline, ()=>source.covariate({
|
|
159
176
|
units: sourceUnits,
|
|
160
177
|
lookbackDays: 14
|
|
161
178
|
}));
|
|
162
|
-
if (pre.some((row)=>!Number.isFinite(row.value))) throw new
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
]
|
|
179
|
+
if (pre.some((row)=>!Number.isFinite(row.value))) throw new XpAnalysisError('non-finite-covariates');
|
|
180
|
+
covariates = new Map(pre.flatMap((row)=>{
|
|
181
|
+
const unitId = userKeyed ? userUnits.get(row.unitId)?.unitId : row.unitId;
|
|
182
|
+
return unitId === undefined ? [] : [
|
|
183
|
+
[
|
|
184
|
+
unitId,
|
|
185
|
+
row.value
|
|
186
|
+
]
|
|
187
|
+
];
|
|
188
|
+
}));
|
|
171
189
|
}
|
|
172
190
|
}
|
|
173
|
-
if (events.some((event)=>!Number.isFinite(event.value))) throw new
|
|
191
|
+
if (events.some((event)=>!Number.isFinite(event.value))) throw new XpAnalysisError('non-finite-values');
|
|
174
192
|
metricData.push({
|
|
175
193
|
metricId: definition.metricId,
|
|
176
194
|
values: perUnitValues(units, events, {
|
|
@@ -183,7 +201,22 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
|
|
|
183
201
|
} : {}
|
|
184
202
|
});
|
|
185
203
|
} catch (error) {
|
|
186
|
-
|
|
204
|
+
let reason = error instanceof ResultsBudgetError ? 'budget' : 'source-failed';
|
|
205
|
+
if (error instanceof XpAnalysisError) {
|
|
206
|
+
switch(error.code){
|
|
207
|
+
case 'source-unavailable':
|
|
208
|
+
case 'unit-type-unavailable':
|
|
209
|
+
case 'non-finite-values':
|
|
210
|
+
case 'non-finite-covariates':
|
|
211
|
+
reason = error.code;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
logger.error({
|
|
216
|
+
err: safeError(error),
|
|
217
|
+
experimentId: phase.experiment,
|
|
218
|
+
metricId: definition.metricId
|
|
219
|
+
}, 'Experiment metric source failed');
|
|
187
220
|
metricData.push({
|
|
188
221
|
metricId: definition.metricId,
|
|
189
222
|
values: new Map(),
|
|
@@ -205,9 +238,14 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
|
|
|
205
238
|
units,
|
|
206
239
|
metricData,
|
|
207
240
|
status,
|
|
241
|
+
onMetricError: (error, metricId)=>logger.error({
|
|
242
|
+
err: safeError(error),
|
|
243
|
+
experimentId: phase.experiment,
|
|
244
|
+
metricId
|
|
245
|
+
}, 'Experiment metric analysis failed'),
|
|
208
246
|
excludedMultiArm: exposed.excludedMultiArm,
|
|
209
247
|
armMismatch: await armMismatchCount(exec, query),
|
|
210
|
-
identityConflictShare: userIds.length ?
|
|
248
|
+
identityConflictShare: userIds.length ? identityConflicts.size / userIds.length : 0,
|
|
211
249
|
...phase.phaseKind === 'rollout' ? {
|
|
212
250
|
winnerArm: phase.arms.find((arm)=>arm.kind !== 'holdout').key
|
|
213
251
|
} : {}
|
|
@@ -252,8 +290,8 @@ export async function runResults(payload, { budgetMs = 20_000, experimentId, swe
|
|
|
252
290
|
experimentId
|
|
253
291
|
}, 'Experiment scheduling failed');
|
|
254
292
|
}
|
|
255
|
-
//
|
|
256
|
-
// phases together
|
|
293
|
+
// Analysis precedes health-only refreshes, then oldest phase snapshots first;
|
|
294
|
+
// grouping keeps each experiment's phases together with that priority.
|
|
257
295
|
const experiments = new Map();
|
|
258
296
|
for (const candidate of candidates){
|
|
259
297
|
const id = candidate.phase.experiment;
|
|
@@ -272,10 +310,62 @@ export async function runResults(payload, { budgetMs = 20_000, experimentId, swe
|
|
|
272
310
|
const snapshots = [];
|
|
273
311
|
for (const phase of phases){
|
|
274
312
|
if (Date.now() >= analysisDeadline) throw new ResultsBudgetError();
|
|
275
|
-
|
|
313
|
+
// Health refreshes must not reopen inference after the late-beacon grace.
|
|
314
|
+
snapshots.push(phase.frozenSnapshot ? {
|
|
315
|
+
...phase.frozenSnapshot,
|
|
316
|
+
computedAt: now.toISOString()
|
|
317
|
+
} : await analyseCandidate(exec, phase, options, now, analysisDeadline, summary.unavailableMetrics, payload.logger));
|
|
276
318
|
}
|
|
277
319
|
if (Date.now() >= analysisDeadline) throw new ResultsBudgetError();
|
|
278
|
-
|
|
320
|
+
const current = phases.find((candidate)=>candidate.kind === 'page' && candidate.phase.phase === candidate.currentPhase && (candidate.status === 'running' || candidate.status === 'decided'));
|
|
321
|
+
if (current) {
|
|
322
|
+
const req = await raceDeadline(analysisDeadline, ()=>createLocalReq({}, payload));
|
|
323
|
+
const health = [];
|
|
324
|
+
// Decided tests serve only the winner and Original, including rollouts.
|
|
325
|
+
const arms = current.phase.phaseKind === 'rollout' ? [
|
|
326
|
+
...current.phase.arms.filter((arm)=>arm.kind !== 'holdout'),
|
|
327
|
+
...current.testArms.filter((arm)=>arm.kind === 'control')
|
|
328
|
+
] : current.testArms.filter((arm)=>arm.kind !== 'holdout' && (current.status !== 'decided' || arm.kind === 'control' || arm.key === current.winnerArm));
|
|
329
|
+
const targets = new Set(arms.flatMap((arm)=>arm.pageTarget ? [
|
|
330
|
+
arm.pageTarget
|
|
331
|
+
] : []));
|
|
332
|
+
for (const path of targets){
|
|
333
|
+
let published = false, found = false, unchecked = false;
|
|
334
|
+
for (const collection of options.pages?.collections ?? []){
|
|
335
|
+
try {
|
|
336
|
+
const page = await raceDeadline(analysisDeadline, ()=>collection.findByPath(path, req));
|
|
337
|
+
found ||= page != null;
|
|
338
|
+
published ||= page?.published === true;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (error instanceof ResultsBudgetError) throw error;
|
|
341
|
+
payload.logger.error({
|
|
342
|
+
err: safeError(error),
|
|
343
|
+
experimentId: id
|
|
344
|
+
}, 'Experiment version page lookup failed');
|
|
345
|
+
unchecked = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!published) health.push({
|
|
349
|
+
code: !found && (unchecked || !options.pages?.collections.length) ? 'version-page-unchecked' : 'version-page-unavailable',
|
|
350
|
+
path
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
for (const snapshot of snapshots)snapshot.health = health;
|
|
354
|
+
}
|
|
355
|
+
if (Date.now() >= analysisDeadline) throw new ResultsBudgetError();
|
|
356
|
+
// Order and optional empty health are semantically identical. Frozen
|
|
357
|
+
// inference gets a new row only when its page availability changes.
|
|
358
|
+
const canonicalHealth = (snapshot)=>JSON.stringify((snapshot.health ?? []).map(({ code, path })=>[
|
|
359
|
+
code,
|
|
360
|
+
path
|
|
361
|
+
]).map((entry)=>JSON.stringify(entry)).sort((a, b)=>a < b ? -1 : a > b ? 1 : 0));
|
|
362
|
+
const changed = snapshots.filter((snapshot, index)=>!phases[index].frozenSnapshot || canonicalHealth(snapshot) !== canonicalHealth(phases[index].frozenSnapshot));
|
|
363
|
+
// Unchanged health is a successful refresh with nothing to write.
|
|
364
|
+
if (!changed.length) {
|
|
365
|
+
summary.ran++;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
await writeSnapshots(exec, changed);
|
|
279
369
|
summary.ran++;
|
|
280
370
|
if (summary.processedIds.length < 100) summary.processedIds.push(id);
|
|
281
371
|
} catch (error) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Trim and deduplicate list inputs while preserving commas inside array items. */
|
|
2
|
+
export declare function coerceListValue(raw: unknown): string[];
|
|
3
|
+
/** Only the read boundary accepts legacy comma-separated document values. */
|
|
4
|
+
export declare function readListValue(raw: unknown): string[];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Trim and deduplicate list inputs while preserving commas inside array items. */ export function coerceListValue(raw) {
|
|
2
|
+
return [
|
|
3
|
+
...new Set((Array.isArray(raw) ? raw.map(String) : [
|
|
4
|
+
String(raw ?? '')
|
|
5
|
+
]).map((value)=>value.trim()).filter(Boolean))
|
|
6
|
+
];
|
|
7
|
+
}
|
|
8
|
+
/** Only the read boundary accepts legacy comma-separated document values. */ export function readListValue(raw) {
|
|
9
|
+
return coerceListValue(typeof raw === 'string' ? raw.split(',') : raw);
|
|
10
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lockMetricsForFreeze } from '../db/metricUsage.js';
|
|
1
2
|
import { readState } from '../db/state.js';
|
|
2
3
|
import { XpRefusal } from '../errors.js';
|
|
3
4
|
import { relationshipId } from './checkStart.js';
|
|
@@ -44,7 +45,14 @@ export async function addMetric(req, { experimentId, metric, role, options }) {
|
|
|
44
45
|
if (ids.some((id)=>id !== undefined && String(id) === String(metric))) refuse('duplicate-metric', {
|
|
45
46
|
message: 'This metric is already part of the experiment.'
|
|
46
47
|
});
|
|
48
|
+
const lockedMetrics = await lockMetricsForFreeze(exec, caps.definitions.map((metric)=>metric.id));
|
|
49
|
+
if (!lockedMetrics) {
|
|
50
|
+
throw new XpRefusal('stale-draft', 'This goal was deleted. Choose another goal.', undefined, 409);
|
|
51
|
+
}
|
|
47
52
|
const definition = caps.definitions[0];
|
|
53
|
+
if (lockedMetrics[0].slug !== definition.slug || lockedMetrics[0].source !== definition.source) {
|
|
54
|
+
throw new XpRefusal('stale-draft', 'This goal changed. Review it again before adding it.', undefined, 409);
|
|
55
|
+
}
|
|
48
56
|
if (current.unitType === 'user' && definition.keyedBy === 'visitor' && !definition.source.startsWith('events:')) refuse('unit-source-mismatch');
|
|
49
57
|
// §7.8: phase lookup serves the access-authorised transition write.
|
|
50
58
|
const phases = await req.payload.find({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentDone } from '../contentDone.js';
|
|
1
2
|
import { relationshipId } from './checkStart.js';
|
|
2
3
|
import { expectedConfirm, matchesConfirm, storedWinnerLabel } from './confirmPhrases.js';
|
|
3
4
|
import { withTransition } from './kernel.js';
|
|
@@ -14,9 +15,7 @@ export async function archive(req, { experimentId, mode, confirmation }) {
|
|
|
14
15
|
if (mode === 'stopped' ? current.status !== 'stopped' : current.status !== 'decided') refuse('invalid-status');
|
|
15
16
|
if (mode === 'hard-coded' && current.kind !== 'code') refuse('invalid-status');
|
|
16
17
|
if (mode === 'after-content') {
|
|
17
|
-
|
|
18
|
-
const docs = Array.isArray(report) ? report : report?.docs;
|
|
19
|
-
if (!Array.isArray(docs) || !docs.every((doc)=>doc?.ok === true)) refuse('invalid-status');
|
|
18
|
+
if (!contentDone(decision?.contentReport)) refuse('invalid-status');
|
|
20
19
|
}
|
|
21
20
|
if (mode === 'discard' && !matchesConfirm(expectedConfirm('discard', ''), confirmation)) refuse('confirmation-required');
|
|
22
21
|
if (mode === 'hard-coded') {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Arm, DocId, ExperimentKind, ExposurePolicy, FlagPoint, MetricSource, ResolvedOptions, Status, TargetingRule, UnitType } from '../types/contracts.js';
|
|
2
2
|
import type { ArmIssueCode, PreflightResult } from '../types/endpoints.js';
|
|
3
3
|
import type { BlockPlacement, FlagPlacement } from './placements.js';
|
|
4
|
-
export type ReadinessCode = ArmIssueCode | 'invalid-status' | 'no-planned-duration' | 'no-primary-metric' | 'no-reach-per-day' | 'page-no-targets' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'no-placements' | 'empty-slot' | 'layer-unit-mismatch' | 'layer-full' | 'unknown-metric-source' | 'unit-source-mismatch' | 'undeclared-targeting-key' | 'invalid-targeting-rule';
|
|
4
|
+
export type ReadinessCode = ArmIssueCode | 'invalid-status' | 'no-planned-duration' | 'no-primary-metric' | 'no-reach-per-day' | 'page-no-targets' | 'page-target-unpublished' | 'page-targets-unchecked' | 'version-page-unavailable' | 'version-page-unchecked' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'no-placements' | 'empty-slot' | 'make-content-unsupported' | 'layer-unit-mismatch' | 'layer-full' | 'unknown-metric-source' | 'unit-source-mismatch' | 'block-path-rule' | 'undeclared-targeting-key' | 'invalid-targeting-rule';
|
|
5
5
|
export interface StartExperiment {
|
|
6
6
|
id: DocId;
|
|
7
7
|
slug: string;
|
|
@@ -38,6 +38,7 @@ export interface StartMetric {
|
|
|
38
38
|
id: DocId;
|
|
39
39
|
source: string;
|
|
40
40
|
}
|
|
41
|
+
export type PlacementFailure = 'placements-failed' | 'forbidden' | 'not-found';
|
|
41
42
|
export interface CheckStartInput {
|
|
42
43
|
experiment: StartExperiment;
|
|
43
44
|
metrics: StartMetric[];
|
|
@@ -45,9 +46,11 @@ export interface CheckStartInput {
|
|
|
45
46
|
flagPoints?: FlagPoint[];
|
|
46
47
|
placements?: BlockPlacement[];
|
|
47
48
|
flagPlacements?: FlagPlacement[];
|
|
48
|
-
placementError?:
|
|
49
|
-
flagPlacementError?:
|
|
49
|
+
placementError?: PlacementFailure;
|
|
50
|
+
flagPlacementError?: PlacementFailure;
|
|
50
51
|
flagPointBusy?: boolean;
|
|
52
|
+
unpublishedPageTargets?: string[];
|
|
53
|
+
pageTargetsUnchecked?: boolean;
|
|
51
54
|
layer?: {
|
|
52
55
|
unitType: UnitType;
|
|
53
56
|
usedSlices: number[];
|
|
@@ -56,6 +56,9 @@ function invalidParams(arms, point) {
|
|
|
56
56
|
'viewport'
|
|
57
57
|
].includes(e.exposurePolicy) || positive(e.analysis?.reachPerDay));
|
|
58
58
|
add('page-no-targets', e.kind !== 'page' || e.arms.filter((arm)=>arm.kind !== 'holdout').every((arm)=>typeof arm.pageTarget === 'string' && arm.pageTarget.trim().length > 0));
|
|
59
|
+
add('page-target-unpublished', !input.unpublishedPageTargets?.length, input.unpublishedPageTargets?.[0]);
|
|
60
|
+
add('page-targets-unchecked', true);
|
|
61
|
+
if (input.pageTargetsUnchecked) checks[checks.length - 1].warning = true;
|
|
59
62
|
add('page-user-unit', e.kind !== 'page' || e.unitType === 'visitor');
|
|
60
63
|
const point = input.flagPoints?.find((point)=>point.id === e.flagPoint);
|
|
61
64
|
add('flag-no-point', e.kind !== 'flag' || !!point);
|
|
@@ -72,6 +75,12 @@ function invalidParams(arms, point) {
|
|
|
72
75
|
add('no-placements', e.kind !== 'block' || !input.placementError && placements.length > 0, input.placementError);
|
|
73
76
|
const empty = placements.filter((placement)=>e.arms.some((arm)=>arm.kind !== 'holdout' && (!arm.slot || !(placement.slots[arm.slot] > 0))));
|
|
74
77
|
add('empty-slot', e.kind !== 'block' || empty.length === 0, empty);
|
|
78
|
+
add('make-content-unsupported', true);
|
|
79
|
+
const localized = placements.find((placement)=>placement.localized);
|
|
80
|
+
if (localized) Object.assign(checks[checks.length - 1], {
|
|
81
|
+
warning: true,
|
|
82
|
+
detail: localized.title
|
|
83
|
+
});
|
|
75
84
|
add('layer-unit-mismatch', e.layer == null || input.layer?.unitType === e.unitType);
|
|
76
85
|
const free = 100 - new Set(input.layer?.usedSlices).size;
|
|
77
86
|
add('layer-full', e.layer == null || !!input.layer && free >= e.allocation, {
|
|
@@ -82,6 +91,12 @@ function invalidParams(arms, point) {
|
|
|
82
91
|
add('unknown-metric-source', unknown.length === 0, unknown);
|
|
83
92
|
const mismatches = input.metrics.filter((metric)=>!metric.source.startsWith('events:') && e.unitType === 'user' && input.metricSources?.some((source)=>source.id === metric.source && source.keyedBy === 'visitor'));
|
|
84
93
|
add('unit-source-mismatch', mismatches.length === 0, mismatches);
|
|
94
|
+
const pathRules = (e.targeting ?? []).flatMap((rule, index)=>rule.attribute === 'path' ? [
|
|
95
|
+
index
|
|
96
|
+
] : []);
|
|
97
|
+
add('block-path-rule', e.kind !== 'block' || pathRules.length === 0, {
|
|
98
|
+
rules: pathRules
|
|
99
|
+
});
|
|
85
100
|
const undeclared = (e.targeting ?? []).filter((rule)=>!Object.hasOwn(input.attributeKeys ?? {}, rule.attribute));
|
|
86
101
|
add('undeclared-targeting-key', undeclared.length === 0, {
|
|
87
102
|
message: 'Targeting uses an undeclared attribute.',
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Block, Field, SanitizedConfig } from 'payload';
|
|
2
|
+
/** Payload keeps both inline definitions and slug references in blockReferences. */
|
|
3
|
+
export declare function contentBlocks(field: Extract<Field, {
|
|
4
|
+
type: 'blocks';
|
|
5
|
+
}>, blocks?: Block[]): Block[];
|
|
6
|
+
/** Any localized data makes a full-document publish unsafe in Payload 3.90. */
|
|
7
|
+
export declare function hasLocalizedFields(fields: Field[], config: Pick<SanitizedConfig, 'localization' | 'blocks'>, seen?: Set<Field>): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Payload keeps both inline definitions and slug references in blockReferences. */ export function contentBlocks(field, blocks = []) {
|
|
2
|
+
return [
|
|
3
|
+
...field.blocks,
|
|
4
|
+
...(field.blockReferences ?? []).flatMap((reference)=>typeof reference === 'string' ? blocks.filter((block)=>block.slug === reference) : [
|
|
5
|
+
reference
|
|
6
|
+
])
|
|
7
|
+
];
|
|
8
|
+
}
|
|
9
|
+
/** Any localized data makes a full-document publish unsafe in Payload 3.90. */ export function hasLocalizedFields(fields, config, seen = new Set()) {
|
|
10
|
+
if (!config.localization) return false;
|
|
11
|
+
const blocks = config.blocks ?? [];
|
|
12
|
+
return fields.some((field)=>{
|
|
13
|
+
if (seen.has(field)) return false;
|
|
14
|
+
const branch = new Set(seen).add(field);
|
|
15
|
+
if ('localized' in field && field.localized) return true;
|
|
16
|
+
if ('tabs' in field && field.tabs.some((tab)=>tab.localized || hasLocalizedFields(tab.fields, config, branch))) return true;
|
|
17
|
+
if ('fields' in field && hasLocalizedFields(field.fields, config, branch)) return true;
|
|
18
|
+
return field.type === 'blocks' && contentBlocks(field, blocks).some((block)=>hasLocalizedFields(block.fields, config, branch));
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
|
|
1
2
|
import { reclaimSlices } from '../db/slices.js';
|
|
2
3
|
import { decideGuard } from '../results/decideGuard.js';
|
|
3
4
|
import { relationshipId } from './checkStart.js';
|
|
@@ -112,7 +113,11 @@ export async function decide(req, { experimentId, winnerArm, note, confirmation
|
|
|
112
113
|
allocationChanges: [],
|
|
113
114
|
targeting: test.targeting,
|
|
114
115
|
metricDefinitions: test.metricDefinitions,
|
|
115
|
-
analysis:
|
|
116
|
+
analysis: {
|
|
117
|
+
...test.analysis,
|
|
118
|
+
// Older test phases may predate freezing the effective tuning value.
|
|
119
|
+
sequentialTuningParameter: test.analysis?.sequentialTuningParameter ?? DEFAULT_SEQUENTIAL_TUNING_PARAMETER
|
|
120
|
+
},
|
|
116
121
|
contentChanges: []
|
|
117
122
|
});
|
|
118
123
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
import type { DocId } from '../types/contracts.js';
|
|
3
|
+
import type { DismissContentEntryBody } from '../types/endpoints.js';
|
|
4
|
+
export declare function dismissContentEntry(req: PayloadRequest, { experimentId, collection, id }: DismissContentEntryBody & {
|
|
5
|
+
experimentId: DocId;
|
|
6
|
+
}): Promise<{
|
|
7
|
+
doc: import("payload").JsonObject & import("payload").TypeWithID;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { relationshipId } from './checkStart.js';
|
|
2
|
+
import { withTransition } from './kernel.js';
|
|
3
|
+
import { readExperiment, refuse, updateExperiment } from './start.js';
|
|
4
|
+
export async function dismissContentEntry(req, { experimentId, collection, id }) {
|
|
5
|
+
const before = await readExperiment(req, experimentId);
|
|
6
|
+
// Only the audit report changes; assignment and the manifest stay the same.
|
|
7
|
+
const { result: doc } = await withTransition(req, {
|
|
8
|
+
experimentId,
|
|
9
|
+
layerId: relationshipId(before.layer),
|
|
10
|
+
flagPointId: before.kind === 'flag' ? before.flagPoint : undefined,
|
|
11
|
+
bump: false
|
|
12
|
+
}, async ({ req })=>{
|
|
13
|
+
const current = await readExperiment(req, experimentId);
|
|
14
|
+
const decision = current.decision;
|
|
15
|
+
const report = decision?.contentReport;
|
|
16
|
+
const index = Array.isArray(report?.docs) ? report.docs.findIndex((entry)=>entry.collection === collection && String(entry.id) === String(id)) : -1;
|
|
17
|
+
const entry = report?.docs?.[index];
|
|
18
|
+
if (current.status !== 'decided' || !decision?.winnerArm || decision.discardedAt) refuse('not-decided');
|
|
19
|
+
if (!entry) refuse('entry-not-found');
|
|
20
|
+
if (entry.ok !== false) refuse('entry-ok');
|
|
21
|
+
if (entry.dismissedAt) refuse('entry-dismissed');
|
|
22
|
+
const docs = report.docs.map((entry, i)=>i === index ? {
|
|
23
|
+
...entry,
|
|
24
|
+
dismissedAt: new Date().toISOString(),
|
|
25
|
+
dismissedBy: req.user?.id == null ? null : String(req.user.id)
|
|
26
|
+
} : entry);
|
|
27
|
+
return updateExperiment(req, experimentId, {
|
|
28
|
+
decision: {
|
|
29
|
+
...decision,
|
|
30
|
+
contentReport: {
|
|
31
|
+
...report,
|
|
32
|
+
docs
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
req.payload.logger.info({
|
|
38
|
+
experimentId,
|
|
39
|
+
collection,
|
|
40
|
+
id
|
|
41
|
+
}, 'Experiment content entry dismissed');
|
|
42
|
+
return {
|
|
43
|
+
doc
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { type PayloadRequest } from 'payload';
|
|
2
|
+
import type { ContentReportReason } from '../types/reasons.js';
|
|
2
3
|
import type { DocId, ResolvedOptions } from '../types/contracts.js';
|
|
3
4
|
type ContentDoc = {
|
|
4
5
|
collection: string;
|
|
5
6
|
id: DocId;
|
|
6
7
|
ok: boolean;
|
|
7
|
-
detail?:
|
|
8
|
-
error?:
|
|
8
|
+
detail?: ContentReportReason;
|
|
9
|
+
error?: ContentReportReason;
|
|
10
|
+
dismissedAt?: string;
|
|
11
|
+
dismissedBy?: string | null;
|
|
9
12
|
};
|
|
10
13
|
export declare function makeContent<TUser extends {
|
|
11
14
|
id: DocId;
|