@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.
@@ -1,3 +1,4 @@
1
+ export declare const DEFAULT_SEQUENTIAL_TUNING_PARAMETER = 5000;
1
2
  export declare const MAX_EVENTS_PER_BODY = 20;
2
3
  export declare const DEFAULT_MAX_BODY_BYTES = 8192;
3
4
  export declare const XP_COOKIE = "xp_id";
package/dist/constants.js CHANGED
@@ -1,3 +1,4 @@
1
+ export const DEFAULT_SEQUENTIAL_TUNING_PARAMETER = 5000;
1
2
  export const MAX_EVENTS_PER_BODY = 20;
2
3
  export const DEFAULT_MAX_BODY_BYTES = 8192;
3
4
  export const XP_COOKIE = 'xp_id';
@@ -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
  }
@@ -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
  ]
@@ -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
- control: {
7
- type: 'slot'
8
- },
9
- variantA: {
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
- variantC: {
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: number;
36
+ sequentialTuningParameter?: number | null;
37
37
  cuped: 'auto' | 'off';
38
38
  cupedLookbackDays?: number;
39
39
  minExposuresPerArm: number;
@@ -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: analysis.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: analysis.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: analysis.sequentialTuningParameter
349
+ sequentialTuningParameter
346
350
  });
347
351
  verdict = computeVerdict({
348
352
  ...verdictInput,
@@ -13,6 +13,7 @@ export declare function runResults(payload: Payload, { budgetMs, experimentId, s
13
13
  }): Promise<{
14
14
  ran: number;
15
15
  skipped: number;
16
+ failed: number;
16
17
  swept: number;
17
18
  processedIds: DocId[];
18
19
  budgetExhausted: boolean;
@@ -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.5",
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",