@delmaredigital/payload-experiments 0.2.0-rc.5 → 0.2.0-rc.6
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/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/endpoints/recompute.js +6 -1
- package/dist/puck/copy.d.ts +6 -0
- package/dist/puck/copy.js +7 -0
- package/dist/puck/editor/ExperimentSlotEditor.js +2 -0
- package/dist/puck/slotConfig.js +14 -9
- package/dist/results/analyse.d.ts +1 -1
- package/dist/results/analyse.js +7 -3
- package/dist/server/runResults.d.ts +1 -0
- package/dist/server/runResults.js +4 -0
- package/dist/types/endpoints.d.ts +1 -1
- package/package.json +1 -1
package/dist/constants.d.ts
CHANGED
package/dist/constants.js
CHANGED
|
@@ -22,6 +22,7 @@ export async function handleRecompute(req, _ctx) {
|
|
|
22
22
|
experimentId: docs[0].id,
|
|
23
23
|
sweep: false
|
|
24
24
|
});
|
|
25
|
+
if (result.failed) return resultsError(500, 'analysis-failed', 'Results could not be computed; this has been logged.');
|
|
25
26
|
if (result.skipped || result.budgetExhausted) return resultsError(503, 'recompute-failed', 'Results could not be recomputed. Try again.');
|
|
26
27
|
const latest = (await readSnapshots(req, [
|
|
27
28
|
docs[0].id
|
|
@@ -36,7 +37,11 @@ export async function handleRecompute(req, _ctx) {
|
|
|
36
37
|
'Cache-Control': 'no-store'
|
|
37
38
|
}
|
|
38
39
|
});
|
|
39
|
-
} catch
|
|
40
|
+
} catch (error) {
|
|
41
|
+
req.payload.logger.error({
|
|
42
|
+
err: error,
|
|
43
|
+
experimentId: docs[0].id
|
|
44
|
+
}, 'Experiment recompute failed');
|
|
40
45
|
return resultsError(503, 'recompute-failed', 'Results could not be recomputed. Try again.');
|
|
41
46
|
}
|
|
42
47
|
}
|
package/dist/puck/copy.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export declare const slotLabels: {
|
|
2
|
+
readonly control: "Original";
|
|
3
|
+
readonly variantA: "Variant A";
|
|
4
|
+
readonly variantB: "Variant B";
|
|
5
|
+
readonly variantC: "Variant C";
|
|
6
|
+
};
|
|
1
7
|
export declare const pickerCopy: {
|
|
2
8
|
readonly label: "Experiment";
|
|
3
9
|
readonly none: "No experiment";
|
package/dist/puck/copy.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Kept separate from admin copy because the Puck entries cannot import admin modules.
|
|
2
|
+
export const slotLabels = {
|
|
3
|
+
control: 'Original',
|
|
4
|
+
variantA: 'Variant A',
|
|
5
|
+
variantB: 'Variant B',
|
|
6
|
+
variantC: 'Variant C'
|
|
7
|
+
};
|
|
1
8
|
export const pickerCopy = {
|
|
2
9
|
label: 'Experiment',
|
|
3
10
|
none: 'No experiment',
|
|
@@ -156,6 +156,8 @@ function EditorRender(props) {
|
|
|
156
156
|
experiment ? /*#__PURE__*/ _jsx("a", {
|
|
157
157
|
className: "xp-slot-button xp-slot-button-primary",
|
|
158
158
|
href: `/admin/experiments/${encodeURIComponent(String(experimentId))}${experiment.status === 'draft' ? '' : '/results'}`,
|
|
159
|
+
target: "_blank",
|
|
160
|
+
rel: "noopener noreferrer",
|
|
159
161
|
children: experiment.status === 'draft' ? editorCopy.setup : editorCopy.viewResults
|
|
160
162
|
}) : null
|
|
161
163
|
]
|
package/dist/puck/slotConfig.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { createExperimentSlotField } from './experimentSlotField.js';
|
|
2
|
-
import { pickerCopy } from './copy.js';
|
|
2
|
+
import { pickerCopy, slotLabels } from './copy.js';
|
|
3
3
|
export const experimentSlotLabel = pickerCopy.label;
|
|
4
4
|
export const experimentSlotFields = {
|
|
5
5
|
experiment: createExperimentSlotField(),
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
type: 'slot'
|
|
6
|
+
// Puck 0.24's defaultSlots prepends each slot, reversing declaration order in the outline.
|
|
7
|
+
variantC: {
|
|
8
|
+
type: 'slot',
|
|
9
|
+
label: slotLabels.variantC
|
|
11
10
|
},
|
|
12
11
|
variantB: {
|
|
13
|
-
type: 'slot'
|
|
12
|
+
type: 'slot',
|
|
13
|
+
label: slotLabels.variantB
|
|
14
14
|
},
|
|
15
|
-
|
|
16
|
-
type: 'slot'
|
|
15
|
+
variantA: {
|
|
16
|
+
type: 'slot',
|
|
17
|
+
label: slotLabels.variantA
|
|
18
|
+
},
|
|
19
|
+
control: {
|
|
20
|
+
type: 'slot',
|
|
21
|
+
label: slotLabels.control
|
|
17
22
|
}
|
|
18
23
|
};
|
|
19
24
|
export const experimentSlotDefaultProps = {
|
|
@@ -33,7 +33,7 @@ export interface AnalysisPhase {
|
|
|
33
33
|
analysis: {
|
|
34
34
|
method: 'sequential' | 'bayesian';
|
|
35
35
|
alpha?: number;
|
|
36
|
-
sequentialTuningParameter
|
|
36
|
+
sequentialTuningParameter?: number | null;
|
|
37
37
|
cuped: 'auto' | 'off';
|
|
38
38
|
cupedLookbackDays?: number;
|
|
39
39
|
minExposuresPerArm: number;
|
package/dist/results/analyse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_GAUSSIAN_PRIOR, bonferroniAlpha, capValues, createThetaAdjustedStatistics, effectBayesianABTest, sequentialOneSidedTreatmentGreaterTTest, sequentialOneSidedTreatmentLesserTTest, sequentialTwoSidedTTest, statisticMean, statisticVariance } from '@delmaredigital/experiments-stats';
|
|
2
|
+
import { DEFAULT_SEQUENTIAL_TUNING_PARAMETER } from '../constants.js';
|
|
2
3
|
import { timestampMs } from './aggregate.js';
|
|
3
4
|
import { outcomeStatistic } from './cuped.js';
|
|
4
5
|
import { guardrailStatus } from './guardrails.js';
|
|
@@ -17,6 +18,9 @@ const severity = {
|
|
|
17
18
|
*/ export function analysePhase(input) {
|
|
18
19
|
const { phase, units } = input;
|
|
19
20
|
const { analysis } = phase;
|
|
21
|
+
// Start freezes this optional collection field as-is. Match the stats engine's
|
|
22
|
+
// default and record the effective value in every snapshot, including n = 0.
|
|
23
|
+
const sequentialTuningParameter = analysis.sequentialTuningParameter ?? DEFAULT_SEQUENTIAL_TUNING_PARAMETER;
|
|
20
24
|
const alpha = analysis.alpha ?? 0.05;
|
|
21
25
|
const prior = analysis.prior ?? DEFAULT_GAUSSIAN_PRIOR;
|
|
22
26
|
const bayesian = analysis.bayesian ?? {
|
|
@@ -73,7 +77,7 @@ const severity = {
|
|
|
73
77
|
capped: {}
|
|
74
78
|
};
|
|
75
79
|
const params = {
|
|
76
|
-
sequentialTuningParameter
|
|
80
|
+
sequentialTuningParameter,
|
|
77
81
|
prior: {
|
|
78
82
|
...prior
|
|
79
83
|
},
|
|
@@ -158,7 +162,7 @@ const severity = {
|
|
|
158
162
|
metric.cupedApplied ||= a.type === 'regressionAdjusted' && b.type === 'regressionAdjusted';
|
|
159
163
|
const config = {
|
|
160
164
|
alpha,
|
|
161
|
-
sequentialTuningParameter
|
|
165
|
+
sequentialTuningParameter
|
|
162
166
|
};
|
|
163
167
|
const inverse = definition.direction === 'decrease';
|
|
164
168
|
let comparison;
|
|
@@ -342,7 +346,7 @@ const severity = {
|
|
|
342
346
|
alpha: alphaAdjusted,
|
|
343
347
|
method: analysis.method,
|
|
344
348
|
prior,
|
|
345
|
-
sequentialTuningParameter
|
|
349
|
+
sequentialTuningParameter
|
|
346
350
|
});
|
|
347
351
|
verdict = computeVerdict({
|
|
348
352
|
...verdictInput,
|
|
@@ -219,6 +219,7 @@ export async function runResults(payload, { budgetMs = 20_000, experimentId, swe
|
|
|
219
219
|
const summary = {
|
|
220
220
|
ran: 0,
|
|
221
221
|
skipped: 0,
|
|
222
|
+
failed: 0,
|
|
222
223
|
swept: 0,
|
|
223
224
|
processedIds: [],
|
|
224
225
|
budgetExhausted: false,
|
|
@@ -243,6 +244,7 @@ export async function runResults(payload, { budgetMs = 20_000, experimentId, swe
|
|
|
243
244
|
});
|
|
244
245
|
} catch (error) {
|
|
245
246
|
summary.skipped++;
|
|
247
|
+
if (!(error instanceof ResultsBudgetError)) summary.failed++;
|
|
246
248
|
summary.budgetExhausted = error instanceof ResultsBudgetError;
|
|
247
249
|
payload.logger.error({
|
|
248
250
|
err: error,
|
|
@@ -277,6 +279,7 @@ export async function runResults(payload, { budgetMs = 20_000, experimentId, swe
|
|
|
277
279
|
if (summary.processedIds.length < 100) summary.processedIds.push(id);
|
|
278
280
|
} catch (error) {
|
|
279
281
|
summary.skipped++;
|
|
282
|
+
if (!(error instanceof ResultsBudgetError)) summary.failed++;
|
|
280
283
|
summary.budgetExhausted ||= error instanceof ResultsBudgetError;
|
|
281
284
|
payload.logger.error({
|
|
282
285
|
err: error,
|
|
@@ -296,6 +299,7 @@ export async function runResults(payload, { budgetMs = 20_000, experimentId, swe
|
|
|
296
299
|
logger: payload.logger
|
|
297
300
|
});
|
|
298
301
|
} catch (error) {
|
|
302
|
+
if (!(error instanceof ResultsBudgetError)) summary.failed++;
|
|
299
303
|
summary.budgetExhausted ||= error instanceof ResultsBudgetError;
|
|
300
304
|
payload.logger.error({
|
|
301
305
|
err: error
|
|
@@ -4,7 +4,7 @@ import type { ReadinessCode } from '../transitions/checkStart.js';
|
|
|
4
4
|
export type { ReadinessCode } from '../transitions/checkStart.js';
|
|
5
5
|
import type { Snapshot, VerdictCode } from './snapshot.js';
|
|
6
6
|
export type ArmIssueCode = 'weights-total' | 'control-count' | 'control-first' | 'holdout-last' | 'holdout-count' | 'variant-count' | 'duplicate-key' | 'duplicate-slot' | 'slot-required' | 'invalid-key';
|
|
7
|
-
export type RefusalCode = ArmIssueCode | 'not-found' | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'empty-slot' | 'page-no-targets' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'layer-full' | 'layer-unit-mismatch' | 'unknown-metric-source' | 'unit-source-mismatch' | 'no-reach-per-day' | 'invalid-status' | 'stale-draft' | 'commit-unverified' | 'allocation-lower' | 'confirmation-required' | 'invalid-winner' | 'slices-taken' | 'locked-placement' | 'not-implemented' | 'forbidden' | 'unauthenticated' | 'body-too-large' | 'invalid-body' | 'too-many-ids' | 'invalid-phase' | 'invalid-ids' | 'recompute-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'duplicate-metric';
|
|
7
|
+
export type RefusalCode = ArmIssueCode | 'not-found' | 'no-planned-duration' | 'no-primary-metric' | 'no-placements' | 'empty-slot' | 'page-no-targets' | 'page-user-unit' | 'flag-no-point' | 'flag-params-invalid' | 'flag-point-busy' | 'flag-placement-missing' | 'layer-full' | 'layer-unit-mismatch' | 'unknown-metric-source' | 'unit-source-mismatch' | 'no-reach-per-day' | 'invalid-status' | 'stale-draft' | 'commit-unverified' | 'allocation-lower' | 'confirmation-required' | 'invalid-winner' | 'slices-taken' | 'locked-placement' | 'not-implemented' | 'forbidden' | 'unauthenticated' | 'body-too-large' | 'invalid-body' | 'too-many-ids' | 'invalid-phase' | 'invalid-ids' | 'recompute-failed' | 'analysis-failed' | 'undeclared-targeting-key' | 'invalid-targeting-rule' | 'duplicate-metric';
|
|
8
8
|
export type ErrorEnvelope = {
|
|
9
9
|
errors: Array<{
|
|
10
10
|
code: RefusalCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delmaredigital/payload-experiments",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.6",
|
|
4
4
|
"description": "No-code A/B testing for Payload CMS and Next.js: block, page and flag experiments with holdouts, layers, exposure logging and honest statistics inside the admin — no vendor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|