@delmaredigital/payload-experiments 0.1.0-rc.3 → 0.1.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/README.md CHANGED
@@ -372,12 +372,20 @@ is frozen for the round. Capping affects decision statistics, while results
372
372
  also show uncapped totals. Count and binomial metrics are never capped.
373
373
 
374
374
  For simple yes/no outcomes, list names in `eventSources`, for example
375
- `eventSources: ['signup']`. Names must match `/^[a-z0-9_]+$/`. Editors can then
376
- choose `events:signup` as a binomial source, backed by stored conversions with
377
- `eventName: 'signup'`, from either `trackConversion` or
378
- `trackConversionBeacon`. Only listed event names become `events:<name>`
379
- sources. They support visitor and user units and allow client writes; they do
380
- not represent counts or revenue in v1.
375
+ `eventSources: ['signup']`. A bare string allows client writes; the object form
376
+ sets that permission explicitly:
377
+
378
+ ```ts
379
+ eventSources: ['signup', { name: 'donation_completed', clientWritable: false }]
380
+ ```
381
+
382
+ Names must match `/^[a-z0-9_]+$/` and be unique across both forms. Editors can
383
+ choose `events:signup` or `events:donation_completed` as binomial sources backed
384
+ by stored conversions with the corresponding `eventName`. Writable sources
385
+ accept both `trackConversion` and `trackConversionBeacon`; server-only sources
386
+ are recorded with `trackConversion`. Beacon conversions for a non-writable name
387
+ are dropped silently. Only listed names become `events:<name>` sources. They
388
+ support visitor and user units and do not represent counts or revenue in v1.
381
389
 
382
390
  ## Security
383
391
 
@@ -16,12 +16,12 @@ export function serializeMetricSources(options) {
16
16
  keyedBy,
17
17
  clientWritable: clientWritable === true
18
18
  })),
19
- ...(options.eventSources ?? []).map((name)=>({
19
+ ...(options.eventSources ?? []).map(({ name, clientWritable })=>({
20
20
  id: `events:${name}`,
21
21
  label: name,
22
22
  kind: 'binomial',
23
23
  keyedBy: 'visitor',
24
- clientWritable: true
24
+ clientWritable
25
25
  }))
26
26
  ];
27
27
  }
@@ -42,7 +42,7 @@ export function eventRows(events, manifest, assignments, options, visitorId, use
42
42
  } else if (event.type === 'conversion') {
43
43
  if (typeof event.eventName !== 'string' || typeof event.idempotencyKey !== 'string' || !event.idempotencyKey.trim() || event.value !== undefined && (typeof event.value !== 'number' || !Number.isFinite(event.value))) continue;
44
44
  const source = options.metricSources?.find((source)=>source.id === event.eventName);
45
- if (source ? source.clientWritable !== true || source.kind === 'revenue' : !options.eventSources?.includes(event.eventName)) continue;
45
+ if (source ? source.clientWritable !== true || source.kind === 'revenue' : !options.eventSources?.some((source)=>source.name === event.eventName && source.clientWritable)) continue;
46
46
  row = {
47
47
  type: 'conversion',
48
48
  eventName: event.eventName,
package/dist/options.js CHANGED
@@ -42,9 +42,19 @@ export function resolveOptions(input) {
42
42
  throw new Error(`Experiments attribute values must be strings: ${key}`);
43
43
  }
44
44
  }
45
- for (const name of input.eventSources ?? []){
45
+ const eventNames = new Set();
46
+ const eventSources = (input.eventSources ?? []).map((entry)=>{
47
+ const object = entry !== null && typeof entry === 'object' && !Array.isArray(entry);
48
+ const name = typeof entry === 'string' ? entry : object ? entry.name : undefined;
46
49
  if (typeof name !== 'string' || !/^[a-z0-9_]+$/.test(name)) throw new Error('Experiments event source names must match /^[a-z0-9_]+$/');
47
- }
50
+ if (object && typeof entry.clientWritable !== 'boolean') throw new Error('Experiments event source clientWritable must be a boolean');
51
+ if (eventNames.has(name)) throw new Error('Experiments event source names must be unique');
52
+ eventNames.add(name);
53
+ return {
54
+ name,
55
+ clientWritable: typeof entry === 'string' ? true : entry.clientWritable
56
+ };
57
+ });
48
58
  const retentionDays = input.events?.retentionDays ?? 180;
49
59
  const maxBodyBytes = input.events?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
50
60
  for (const [name, value] of Object.entries({
@@ -68,9 +78,7 @@ export function resolveOptions(input) {
68
78
  metricSources: [
69
79
  ...input.metricSources ?? []
70
80
  ],
71
- eventSources: [
72
- ...input.eventSources ?? []
73
- ],
81
+ eventSources,
74
82
  attributeKeys: Object.fromEntries(Object.entries(input.attributeKeys ?? {}).map(([key, attribute])=>[
75
83
  key,
76
84
  {
@@ -88,7 +88,7 @@ async function analyseCandidate(exec, candidate, options, now, deadline, unavail
88
88
  let events, covariates;
89
89
  if (definition.source.startsWith('events:')) {
90
90
  const eventName = definition.source.slice(7);
91
- if (!options.eventSources?.includes(eventName)) throw new Error('Metric source unavailable');
91
+ if (!options.eventSources?.some((source)=>source.name === eventName)) throw new Error('Metric source unavailable');
92
92
  const read = async (start)=>{
93
93
  if (candidate.unitType === 'user') return metricEventRows(exec, {
94
94
  eventName,
@@ -105,7 +105,10 @@ export interface ExperimentsPluginOptions<TUser extends {
105
105
  readIdentities?: ExperimentsAccess;
106
106
  };
107
107
  metricSources?: MetricSource[];
108
- eventSources?: string[];
108
+ eventSources?: Array<string | {
109
+ name: string;
110
+ clientWritable: boolean;
111
+ }>;
109
112
  flagPoints?: FlagPoint[];
110
113
  attributeKeys?: Record<string, {
111
114
  type: 'string' | 'number' | 'boolean';
@@ -130,4 +133,9 @@ export type ResolvedOptions<TUser extends {
130
133
  id: string | number;
131
134
  } = {
132
135
  id: string | number;
133
- }> = ExperimentsPluginOptions<TUser>;
136
+ }> = Omit<ExperimentsPluginOptions<TUser>, 'eventSources'> & {
137
+ eventSources?: Array<{
138
+ name: string;
139
+ clientWritable: boolean;
140
+ }>;
141
+ };
@@ -1,2 +1 @@
1
- /** Options required by the route table; CORE-02c resolves optional defaults. */ // CORE-02c may narrow this alias (pre-authorised exception to the src/types freeze)
2
- export { };
1
+ /** Options required by the route table; CORE-02c resolves optional defaults. */ export { };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delmaredigital/payload-experiments",
3
- "version": "0.1.0-rc.3",
3
+ "version": "0.1.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",
@@ -60,7 +60,8 @@
60
60
  "types": "./dist/exports/puckEditor.d.ts",
61
61
  "import": "./dist/exports/puckEditor.js",
62
62
  "default": "./dist/exports/puckEditor.js"
63
- }
63
+ },
64
+ "./package.json": "./package.json"
64
65
  },
65
66
  "files": [
66
67
  "dist",