make-mp-data 2.0.21 → 2.0.23
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/dungeons/student-teacher.js +38 -87
- package/entry.js +7 -1
- package/index.js +90 -8
- package/lib/cli/cli.js +15 -1
- package/lib/core/config-validator.js +230 -219
- package/lib/core/context.js +13 -1
- package/lib/core/storage.js +88 -23
- package/lib/generators/events.js +17 -16
- package/lib/generators/funnels.js +8 -6
- package/lib/orchestrators/mixpanel-sender.js +5 -2
- package/lib/orchestrators/user-loop.js +212 -181
- package/lib/templates/abbreviated.d.ts +4 -3
- package/lib/templates/instructions.txt +1 -0
- package/lib/templates/{dungeon-template.js → scratch-dungeon-template.js} +9 -3
- package/lib/templates/verbose-schema.js +31 -4
- package/lib/utils/utils.js +178 -14
- package/package.json +5 -4
- package/types.d.ts +9 -4
|
@@ -21,185 +21,216 @@ import { makeSCD } from "../generators/scd.js";
|
|
|
21
21
|
* @returns {Promise<void>}
|
|
22
22
|
*/
|
|
23
23
|
export async function userLoop(context) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
24
|
+
const { config, storage, defaults } = context;
|
|
25
|
+
const chance = u.getChance();
|
|
26
|
+
const concurrency = config?.concurrency || Math.min(os.cpus().length * 2, 16);
|
|
27
|
+
const USER_CONN = pLimit(concurrency);
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
verbose,
|
|
31
|
+
numUsers,
|
|
32
|
+
numEvents,
|
|
33
|
+
isAnonymous,
|
|
34
|
+
hasAvatar,
|
|
35
|
+
hasAnonIds,
|
|
36
|
+
hasSessionIds,
|
|
37
|
+
hasLocation,
|
|
38
|
+
funnels,
|
|
39
|
+
userProps,
|
|
40
|
+
scdProps,
|
|
41
|
+
numDays,
|
|
42
|
+
percentUsersBornInDataset = 5,
|
|
43
|
+
} = config;
|
|
44
|
+
|
|
45
|
+
const { eventData, userProfilesData, scdTableData } = storage;
|
|
46
|
+
const avgEvPerUser = numEvents / numUsers;
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
|
|
49
|
+
// Create batches for parallel processing
|
|
50
|
+
const batchSize = Math.max(1, Math.ceil(numUsers / concurrency));
|
|
51
|
+
const userPromises = [];
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < numUsers; i++) {
|
|
54
|
+
const userPromise = USER_CONN(async () => {
|
|
55
|
+
context.incrementUserCount();
|
|
56
|
+
const eps = Math.floor(context.getEventCount() / ((Date.now() - startTime) / 1000));
|
|
57
|
+
|
|
58
|
+
if (verbose) {
|
|
59
|
+
u.progress([
|
|
60
|
+
["users", context.getUserCount()],
|
|
61
|
+
["events", context.getEventCount()],
|
|
62
|
+
["eps", eps]
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const userId = chance.guid();
|
|
67
|
+
const user = u.generateUser(userId, { numDays, isAnonymous, hasAvatar, hasAnonIds, hasSessionIds });
|
|
68
|
+
const { distinct_id, created } = user;
|
|
69
|
+
const userIsBornInDataset = chance.bool({ likelihood: percentUsersBornInDataset });
|
|
70
|
+
let numEventsPreformed = 0;
|
|
71
|
+
|
|
72
|
+
if (!userIsBornInDataset) delete user.created;
|
|
73
|
+
|
|
74
|
+
// Calculate time adjustments
|
|
75
|
+
const daysShift = context.getDaysShift();
|
|
76
|
+
const adjustedCreated = userIsBornInDataset
|
|
77
|
+
? dayjs(created).subtract(daysShift, 'd')
|
|
78
|
+
: dayjs.unix(global.FIXED_BEGIN);
|
|
79
|
+
|
|
80
|
+
if (hasLocation) {
|
|
81
|
+
const location = u.pickRandom(u.choose(defaults.locationsUsers));
|
|
82
|
+
for (const key in location) {
|
|
83
|
+
user[key] = location[key];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Profile creation
|
|
88
|
+
const profile = await makeUserProfile(context, userProps, user);
|
|
89
|
+
|
|
90
|
+
// Call user hook after profile creation
|
|
91
|
+
if (config.hook) {
|
|
92
|
+
await config.hook(profile, "user", {
|
|
93
|
+
user,
|
|
94
|
+
config,
|
|
95
|
+
userIsBornInDataset
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// SCD creation
|
|
100
|
+
// @ts-ignore
|
|
101
|
+
const scdUserTables = t.objFilter(scdProps, (scd) => scd.type === 'user' || !scd.type);
|
|
102
|
+
const scdTableKeys = Object.keys(scdUserTables);
|
|
103
|
+
|
|
104
|
+
const userSCD = {};
|
|
105
|
+
for (const [index, key] of scdTableKeys.entries()) {
|
|
106
|
+
const { max = 100 } = scdProps[key];
|
|
107
|
+
const mutations = chance.integer({ min: 1, max });
|
|
108
|
+
const changes = await makeSCD(context, scdProps[key], key, distinct_id, mutations, created);
|
|
109
|
+
userSCD[key] = changes;
|
|
110
|
+
|
|
111
|
+
await config.hook(changes, "scd-pre", {
|
|
112
|
+
profile,
|
|
113
|
+
type: 'user',
|
|
114
|
+
scd: { [key]: scdProps[key] },
|
|
115
|
+
config,
|
|
116
|
+
allSCDs: userSCD
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let numEventsThisUserWillPreform = Math.floor(chance.normal({
|
|
121
|
+
mean: avgEvPerUser,
|
|
122
|
+
dev: avgEvPerUser / u.integer(u.integer(2, 5), u.integer(2, 7))
|
|
123
|
+
}) * 0.714159265359);
|
|
124
|
+
|
|
125
|
+
// Power users and low-activity users logic
|
|
126
|
+
chance.bool({ likelihood: 20 }) ? numEventsThisUserWillPreform *= 5 : null;
|
|
127
|
+
chance.bool({ likelihood: 15 }) ? numEventsThisUserWillPreform *= 0.333 : null;
|
|
128
|
+
numEventsThisUserWillPreform = Math.round(numEventsThisUserWillPreform);
|
|
129
|
+
|
|
130
|
+
let userFirstEventTime;
|
|
131
|
+
|
|
132
|
+
const firstFunnels = funnels.filter((f) => f.isFirstFunnel)
|
|
133
|
+
.filter((f) => !f.conditions || matchConditions(profile, f.conditions))
|
|
134
|
+
.reduce(weighFunnels, []);
|
|
135
|
+
const usageFunnels = funnels.filter((f) => !f.isFirstFunnel)
|
|
136
|
+
.filter((f) => !f.conditions || matchConditions(profile, f.conditions))
|
|
137
|
+
.reduce(weighFunnels, []);
|
|
138
|
+
|
|
139
|
+
const secondsInDay = 86400;
|
|
140
|
+
const noise = () => chance.integer({ min: 0, max: secondsInDay });
|
|
141
|
+
let usersEvents = [];
|
|
142
|
+
|
|
143
|
+
// PATH FOR USERS BORN IN DATASET AND PERFORMING FIRST FUNNEL
|
|
144
|
+
if (firstFunnels.length && userIsBornInDataset) {
|
|
145
|
+
const firstFunnel = chance.pickone(firstFunnels, user);
|
|
146
|
+
const firstTime = adjustedCreated.subtract(noise(), 'seconds').unix();
|
|
147
|
+
const [data, userConverted] = await makeFunnel(context, firstFunnel, user, firstTime, profile, userSCD);
|
|
148
|
+
|
|
149
|
+
const timeShift = context.getTimeShift();
|
|
150
|
+
userFirstEventTime = dayjs(data[0].time).subtract(timeShift, 'seconds').unix();
|
|
151
|
+
numEventsPreformed += data.length;
|
|
152
|
+
usersEvents = usersEvents.concat(data);
|
|
153
|
+
|
|
154
|
+
if (!userConverted) {
|
|
155
|
+
// if (verbose) {
|
|
156
|
+
// u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
|
|
157
|
+
// }
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
userFirstEventTime = adjustedCreated.subtract(noise(), 'seconds').unix();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ALL SUBSEQUENT FUNNELS
|
|
165
|
+
while (numEventsPreformed < numEventsThisUserWillPreform) {
|
|
166
|
+
if (usageFunnels.length) {
|
|
167
|
+
const currentFunnel = chance.pickone(usageFunnels);
|
|
168
|
+
const [data, userConverted] = await makeFunnel(context, currentFunnel, user, userFirstEventTime, profile, userSCD);
|
|
169
|
+
numEventsPreformed += data.length;
|
|
170
|
+
usersEvents = usersEvents.concat(data);
|
|
171
|
+
} else {
|
|
172
|
+
const data = await makeEvent(context, distinct_id, userFirstEventTime, u.pick(config.events), user.anonymousIds, user.sessionIds, {}, config.groupKeys, true);
|
|
173
|
+
numEventsPreformed++;
|
|
174
|
+
usersEvents = usersEvents.concat(data);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Hook for processing all user events
|
|
179
|
+
if (config.hook) {
|
|
180
|
+
const newEvents = await config.hook(usersEvents, "everything", {
|
|
181
|
+
profile,
|
|
182
|
+
scd: userSCD,
|
|
183
|
+
config,
|
|
184
|
+
userIsBornInDataset
|
|
185
|
+
});
|
|
186
|
+
if (Array.isArray(newEvents)) usersEvents = newEvents;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Store all user data
|
|
190
|
+
await userProfilesData.hookPush(profile);
|
|
191
|
+
|
|
192
|
+
if (Object.keys(userSCD).length) {
|
|
193
|
+
for (const [key, changesArray] of Object.entries(userSCD)) {
|
|
194
|
+
for (const changes of changesArray) {
|
|
195
|
+
try {
|
|
196
|
+
const target = scdTableData.filter(arr => arr.scdKey === key).pop();
|
|
197
|
+
await target.hookPush(changes, { profile, type: 'user' });
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
// This is probably a test
|
|
201
|
+
const target = scdTableData[0];
|
|
202
|
+
await target.hookPush(changes, { profile, type: 'user' });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await eventData.hookPush(usersEvents, { profile });
|
|
209
|
+
|
|
210
|
+
if (verbose) {
|
|
211
|
+
// u.progress([["users", context.getUserCount()], ["events", context.getEventCount()]]);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
userPromises.push(userPromise);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Wait for all users to complete
|
|
219
|
+
await Promise.all(userPromises);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
export function weighFunnels(acc, funnel) {
|
|
224
|
+
const weight = funnel?.weight || 1;
|
|
225
|
+
for (let i = 0; i < weight; i++) {
|
|
226
|
+
acc.push(funnel);
|
|
227
|
+
}
|
|
228
|
+
return acc;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function matchConditions(profile, conditions) {
|
|
232
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
233
|
+
if (profile[key] !== value) return false;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
205
236
|
}
|
|
@@ -94,6 +94,9 @@ interface Funnel {
|
|
|
94
94
|
|
|
95
95
|
/** Properties that will be attached to every event generated within this funnel journey. */
|
|
96
96
|
props?: Record<string, ValueValid>;
|
|
97
|
+
|
|
98
|
+
/** User property conditions that determine which users are eligible for this funnel. Only users whose properties match these conditions will be considered for this funnel. */
|
|
99
|
+
conditions?: Record<string, ValueValid>;
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
|
|
@@ -153,6 +156,4 @@ interface LookupTableSchema {
|
|
|
153
156
|
|
|
154
157
|
/** A dictionary of attributes (columns) for the table and their possible values. */
|
|
155
158
|
attributes: Record<string, ValueValid>;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
+
}
|
|
@@ -56,6 +56,7 @@ Core Requirements:
|
|
|
56
56
|
- Use groups if the prompt involves B2B, teams, companies, or organizations.
|
|
57
57
|
- Use SCD if user or group traits change over time (e.g., subscription tier).
|
|
58
58
|
- Use lookup tables if events reference external entities with their own attributes (e.g., product_id, video_id).
|
|
59
|
+
- Use funnel conditions when different user segments or cohorts should have different behavioral patterns (e.g., premium vs free users, students vs teachers, rider vs driver, doctor vs patient).
|
|
59
60
|
|
|
60
61
|
4. Available Functions: You have access to these built-in functions: date, weighNumRange, range, and the chance library.
|
|
61
62
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* this file is NOT currently used
|
|
3
|
+
*/
|
|
4
|
+
|
|
1
5
|
import dayjs from "dayjs";
|
|
2
6
|
import utc from "dayjs/plugin/utc.js";
|
|
7
|
+
dayjs.extend(utc);
|
|
3
8
|
import "dotenv/config";
|
|
4
|
-
import { weighNumRange, range, date, initChance, exhaust, choose, integer, decimal } from "
|
|
9
|
+
import { weighNumRange, range, date, initChance, exhaust, choose, integer, decimal, odds } from "../utils/utils.js";
|
|
10
|
+
const { NODE_ENV = "unknown" } = process.env;
|
|
11
|
+
|
|
5
12
|
|
|
6
|
-
dayjs.extend(utc);
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* Dungeon template for AI-generated configurations
|
|
@@ -29,7 +35,7 @@ export function createDungeonTemplate({
|
|
|
29
35
|
const chance = u.initChance(seed);
|
|
30
36
|
const numEvents = numUsers * 100;
|
|
31
37
|
|
|
32
|
-
/** @typedef {import("../../types.
|
|
38
|
+
/** @typedef {import("../../types.js").Dungeon} Config */
|
|
33
39
|
|
|
34
40
|
/** @type {Config} */
|
|
35
41
|
const config = {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview this is a highly verbose schema for a dungeon that shows all the options available
|
|
3
|
+
* and how they might be implemented with extensive comments so an AI can understand it
|
|
4
|
+
* it is not meant to be used as a template, but rather as a reference for how to create a dungeon
|
|
5
|
+
* it is also used as a test for the AI to see if it can generate a dungeon with the same structure
|
|
6
|
+
*/
|
|
1
7
|
|
|
2
8
|
|
|
3
9
|
|
|
@@ -6,8 +12,9 @@ const chance = new Chance();
|
|
|
6
12
|
import dayjs from "dayjs";
|
|
7
13
|
import utc from "dayjs/plugin/utc.js";
|
|
8
14
|
dayjs.extend(utc);
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
15
|
+
import "dotenv/config";
|
|
16
|
+
import { weighNumRange, range, date, initChance, exhaust, choose, integer, decimal, odds } from "../../lib/utils/utils.js";
|
|
17
|
+
const { NODE_ENV = "unknown" } = process.env;
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
/** @type {import("../../types.js").Dungeon} */
|
|
@@ -119,8 +126,9 @@ const DUNGEON = {
|
|
|
119
126
|
*/
|
|
120
127
|
userProps: {
|
|
121
128
|
title: ["Mr.", "Ms.", "Mrs.", "Dr.", "Prof.", "Sir", "Madam", "Lord", "Lady", "Dame", "Baron", "Baroness", "Count", "Countess", "Viscount", "Viscountess", "Marquis", "Marchioness"],
|
|
129
|
+
role: ["basic", "basic", "basic", "premium", "admin"], // role property that can be used in funnel conditions
|
|
122
130
|
luckyNumber: weighNumRange(42, 420, .3),
|
|
123
|
-
spiritAnimal: ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "
|
|
131
|
+
spiritAnimal: ["duck", "dog", "otter", "penguin", "cat", "elephant", "lion", "cheetah", "giraffe", "zebra", "rhino", "hippo", "whale", "dolphin", "shark", "octopus", "squid", "jellyfish", "starfish", "seahorse", "crab", "lobster", "shrimp", "clam", "snail", "slug", "butterfly", "moth", "bee", "wasp", "ant", "beetle", "ladybug", "caterpillar", "centipede", "millipede", "scorpion", "spider", "tarantula", "tick", "mite", "mosquito", "fly", "dragonfly", "damselfly", "grasshopper", "cricket", "locust", "mantis", "cockroach", "termite", "praying mantis", "walking stick", "stick bug", "leaf insect", "lacewing", "aphid", "cicada", "thrips", "psyllid", "scale insect", "whitefly", "mealybug", "planthopper", "leafhopper", "treehopper", "flea", "louse", "bedbug", "flea beetle", "weevil", "longhorn beetle", "leaf beetle", "tiger beetle", "ground beetle", "lady beetle", "firefly", "click beetle", "rove beetle", "scarab beetle", "dung beetle", "stag beetle", "rhinoceus beetle", "hercules beetle", "goliath beetle", "jewel beetle", "tortoise beetle"]
|
|
124
132
|
},
|
|
125
133
|
/**
|
|
126
134
|
* Funnels represent intentional user journeys (e.g., sign-up, checkout),
|
|
@@ -130,10 +138,14 @@ const DUNGEON = {
|
|
|
130
138
|
* unless otherwise specified in the prompt.
|
|
131
139
|
* Funnels are the primary mechanism used to generate the example data, and it's critical sequences match events in the events array.
|
|
132
140
|
* there are many different options for the funnels like:
|
|
133
|
-
* isFirstFunnel, conversionRate, isChurnFunnel, order, props, requireRepeats, timeToConvert, weight
|
|
141
|
+
* isFirstFunnel, conversionRate, isChurnFunnel, order, props, requireRepeats, timeToConvert, weight, conditions
|
|
134
142
|
*
|
|
135
143
|
* isFirstFunnel are funnels a user will only go through once (like a sign up)
|
|
136
144
|
* non isFirstFunnel are funnels a user will go through multiple times (like a purchase)
|
|
145
|
+
*
|
|
146
|
+
* conditions are used to filter which users are eligible for a specific funnel based on their user properties
|
|
147
|
+
* this is useful when different user segments should have different behavioral patterns
|
|
148
|
+
* for example: premium users might have access to advanced features, students vs teachers have different workflows
|
|
137
149
|
*
|
|
138
150
|
*/
|
|
139
151
|
funnels: [
|
|
@@ -170,6 +182,21 @@ const DUNGEON = {
|
|
|
170
182
|
props: {
|
|
171
183
|
"browsing type": ["casual", "intentional", "exploratory"] // you can add properties to the funnel
|
|
172
184
|
}
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "premium user workflow",
|
|
188
|
+
description: "advanced features only available to premium users",
|
|
189
|
+
sequence: ["page view", "view item", "save item", "checkout"],
|
|
190
|
+
isFirstFunnel: false,
|
|
191
|
+
conversionRate: 85,
|
|
192
|
+
timeToConvert: 3,
|
|
193
|
+
conditions: {
|
|
194
|
+
role: "premium" // only users with role "premium" are eligible for this funnel
|
|
195
|
+
},
|
|
196
|
+
order: "sequential",
|
|
197
|
+
props: {
|
|
198
|
+
"feature_tier": "premium"
|
|
199
|
+
}
|
|
173
200
|
}
|
|
174
201
|
],
|
|
175
202
|
/**
|