make-mp-data 3.0.1 → 3.0.2
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/adspend.js +35 -1
- package/dungeons/anon.js +25 -1
- package/dungeons/{array-of-object-loopup.js → array-of-object-lookup.js} +28 -8
- package/dungeons/benchmark-heavy.js +2 -2
- package/dungeons/benchmark-light.js +2 -2
- package/dungeons/big.js +2 -2
- package/dungeons/business.js +59 -12
- package/dungeons/complex.js +34 -1
- package/dungeons/copilot.js +1 -1
- package/dungeons/{harness/harness-education.js → education.js} +29 -12
- package/dungeons/experiments.js +15 -2
- package/dungeons/{harness/harness-fintech.js → fintech.js} +8 -8
- package/dungeons/foobar.js +33 -1
- package/dungeons/{harness/harness-food.js → food.js} +7 -4
- package/dungeons/funnels.js +38 -1
- package/dungeons/gaming.js +25 -5
- package/dungeons/media.js +861 -271
- package/dungeons/mil.js +29 -2
- package/dungeons/mirror.js +33 -1
- package/dungeons/{kurby.js → retention-cadence.js} +1 -1
- package/dungeons/{harness/harness-gaming.js → rpg.js} +5 -5
- package/dungeons/sanity.js +31 -2
- package/dungeons/{harness/harness-sass.js → sass.js} +2 -2
- package/dungeons/scd.js +46 -1
- package/dungeons/simple.js +1 -1
- package/dungeons/{harness/harness-social.js → social.js} +2 -2
- package/dungeons/streaming.js +373 -0
- package/dungeons/strict-event-test.js +1 -1
- package/dungeons/student-teacher.js +18 -5
- package/dungeons/text-generation.js +38 -1
- package/dungeons/too-big-events.js +38 -1
- package/dungeons/{userAgent.js → user-agent.js} +21 -1
- package/entry.js +5 -4
- package/lib/utils/logger.js +0 -4
- package/package.json +1 -4
- package/dungeons/ai-chat-analytics-ed.js +0 -275
- package/dungeons/clinch-agi.js +0 -632
- package/dungeons/ecommerce-store.js +0 -0
- package/dungeons/harness/harness-media.js +0 -961
- package/dungeons/money2020-ed-also.js +0 -277
- package/dungeons/money2020-ed.js +0 -580
- package/dungeons/uday-schema.json +0 -220
- package/lib/templates/funnels-instructions.txt +0 -272
- package/lib/templates/hook-examples.json +0 -187
- package/lib/templates/hooks-instructions.txt +0 -721
- package/lib/templates/refine-instructions.txt +0 -485
- package/lib/templates/schema-instructions.txt +0 -285
- package/lib/utils/ai.js +0 -896
- package/lib/utils/mixpanel.js +0 -101
- package/lib/utils/project.js +0 -167
package/dungeons/adspend.js
CHANGED
|
@@ -18,7 +18,7 @@ import { pickAWinner, weighNumRange, date, integer } from "../lib/utils/utils.js
|
|
|
18
18
|
|
|
19
19
|
/** @type {import('../types').Dungeon} */
|
|
20
20
|
const config = {
|
|
21
|
-
token:
|
|
21
|
+
token: "",
|
|
22
22
|
seed: "foo bar",
|
|
23
23
|
numDays: 365, //how many days worth of data
|
|
24
24
|
numEvents: 10000000, //how many events
|
|
@@ -87,6 +87,40 @@ const config = {
|
|
|
87
87
|
groupProps: {},
|
|
88
88
|
lookupTables: [],
|
|
89
89
|
hook: function (record, type, meta) {
|
|
90
|
+
// user hook: segment users into tiers based on their lucky number
|
|
91
|
+
if (type === "user") {
|
|
92
|
+
if (record.luckyNumber > 300) {
|
|
93
|
+
record.tier = "gold";
|
|
94
|
+
} else if (record.luckyNumber > 150) {
|
|
95
|
+
record.tier = "silver";
|
|
96
|
+
} else {
|
|
97
|
+
record.tier = "bronze";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// event hook: gold tier users get a "premium" color override 20% of the time
|
|
102
|
+
if (type === "event") {
|
|
103
|
+
if (record.color === "violet" || record.color === "indigo") {
|
|
104
|
+
record.is_rare_color = true;
|
|
105
|
+
record.number = (record.number || 0) * 2;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// everything hook: users with many events get a bonus "milestone" event
|
|
110
|
+
if (type === "everything") {
|
|
111
|
+
if (record.length > 50) {
|
|
112
|
+
const lastEvent = record[record.length - 1];
|
|
113
|
+
record.push({
|
|
114
|
+
event: "milestone reached",
|
|
115
|
+
time: lastEvent.time,
|
|
116
|
+
user_id: lastEvent.user_id,
|
|
117
|
+
milestone: record.length,
|
|
118
|
+
color: "gold",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
123
|
+
|
|
90
124
|
return record;
|
|
91
125
|
}
|
|
92
126
|
};
|
package/dungeons/anon.js
CHANGED
|
@@ -20,7 +20,7 @@ import { pickAWinner, weighNumRange, date, integer } from "../lib/utils/utils.js
|
|
|
20
20
|
|
|
21
21
|
/** @type {import('../types').Dungeon} */
|
|
22
22
|
const config = {
|
|
23
|
-
token:
|
|
23
|
+
token: "",
|
|
24
24
|
seed: "foo bar",
|
|
25
25
|
numDays: 365, //how many days worth of data
|
|
26
26
|
numEvents: 100000, //how many events
|
|
@@ -95,6 +95,30 @@ const config = {
|
|
|
95
95
|
groupProps: {},
|
|
96
96
|
lookupTables: [],
|
|
97
97
|
hook: function (record, type, meta) {
|
|
98
|
+
// user hook: assign engagement tier based on lucky number
|
|
99
|
+
if (type === "user") {
|
|
100
|
+
record.engagement_tier = record.luckyNumber > 250 ? "high" : "low";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// event hook: weight-1 events ("yak") are treated as error/rare signals
|
|
104
|
+
if (type === "event") {
|
|
105
|
+
if (record.event === "yak") {
|
|
106
|
+
record.is_error = true;
|
|
107
|
+
}
|
|
108
|
+
if (record.event === "foo") {
|
|
109
|
+
record.priority = "high";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// everything hook: low-activity anonymous users churn — keep only first 60% of events
|
|
114
|
+
if (type === "everything") {
|
|
115
|
+
if (record.length < 8) {
|
|
116
|
+
const cutoff = Math.ceil(record.length * 0.6);
|
|
117
|
+
return record.slice(0, cutoff);
|
|
118
|
+
}
|
|
119
|
+
return record;
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
return record;
|
|
99
123
|
}
|
|
100
124
|
};
|
|
@@ -19,7 +19,7 @@ const spiritAnimals = ["duck", "dog", "otter", "penguin", "cat", "elephant", "li
|
|
|
19
19
|
|
|
20
20
|
/** @type {import('../types.js').Dungeon} */
|
|
21
21
|
const config = {
|
|
22
|
-
// token: "
|
|
22
|
+
// token: "",
|
|
23
23
|
seed: "test array of objects lookup",
|
|
24
24
|
name: "array-of-object-lookup",
|
|
25
25
|
numDays: 60, //how many days worth1 of data
|
|
@@ -41,7 +41,7 @@ const config = {
|
|
|
41
41
|
alsoInferFunnels: true,
|
|
42
42
|
concurrency: 1,
|
|
43
43
|
batchSize: 250_000,
|
|
44
|
-
writeToDisk:
|
|
44
|
+
writeToDisk: false,
|
|
45
45
|
events: [
|
|
46
46
|
{
|
|
47
47
|
event: "checkout",
|
|
@@ -112,18 +112,38 @@ const config = {
|
|
|
112
112
|
|
|
113
113
|
const NOW = dayjs();
|
|
114
114
|
|
|
115
|
-
|
|
116
115
|
if (type === "event") {
|
|
117
|
-
|
|
116
|
+
// Pattern 1: Checkouts with coupons get a discount_applied flag and adjusted total
|
|
117
|
+
if (record.event === "checkout" && record.coupon && record.coupon !== "none") {
|
|
118
|
+
record.discount_applied = true;
|
|
119
|
+
const pctMatch = record.coupon.match(/(\d+)%/);
|
|
120
|
+
if (pctMatch) {
|
|
121
|
+
record.discount_percent = parseInt(pctMatch[1]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Pattern 2: "save item" events on weekends are tagged as wishlist behavior
|
|
126
|
+
if (record.event === "save item") {
|
|
127
|
+
const dow = dayjs(record.time).day();
|
|
128
|
+
if (dow === 0 || dow === 6) {
|
|
129
|
+
record.save_context = "weekend_browse";
|
|
130
|
+
} else {
|
|
131
|
+
record.save_context = "weekday_intent";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
if (type === "everything") {
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
// Pattern 3: Users who view 5+ items but never checkout are tagged as window shoppers
|
|
138
|
+
const views = record.filter(e => e.event === "view item").length;
|
|
139
|
+
const checkouts = record.filter(e => e.event === "checkout").length;
|
|
140
|
+
if (views >= 5 && checkouts === 0) {
|
|
141
|
+
for (const e of record) {
|
|
142
|
+
e.user_segment = "window_shopper";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
123
145
|
}
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
|
|
127
147
|
return record;
|
|
128
148
|
}
|
|
129
149
|
};
|
|
@@ -107,7 +107,7 @@ const commentGenerator = createTextGenerator({
|
|
|
107
107
|
|
|
108
108
|
/** @type {import('../types.js').Dungeon} */
|
|
109
109
|
const config = {
|
|
110
|
-
// token:
|
|
110
|
+
// token: "",
|
|
111
111
|
name: "300k-Events-Heavy",
|
|
112
112
|
format: 'json', //csv or json
|
|
113
113
|
seed: "one million events",
|
|
@@ -130,7 +130,7 @@ const config = {
|
|
|
130
130
|
isAnonymous: false,
|
|
131
131
|
alsoInferFunnels: true,
|
|
132
132
|
// concurrency automatically set to 1 when strictEventCount is enabled
|
|
133
|
-
writeToDisk:
|
|
133
|
+
writeToDisk: false,
|
|
134
134
|
batchSize: 2_500_000,
|
|
135
135
|
|
|
136
136
|
events: [
|
|
@@ -18,7 +18,7 @@ import { createTextGenerator } from '../lib/generators/text.js';
|
|
|
18
18
|
|
|
19
19
|
/** @type {import('../types.js').Dungeon} */
|
|
20
20
|
const config = {
|
|
21
|
-
// token:
|
|
21
|
+
// token: "",
|
|
22
22
|
name: "5M-Events-Light",
|
|
23
23
|
format: 'json', //csv or json
|
|
24
24
|
seed: "one million events",
|
|
@@ -41,7 +41,7 @@ const config = {
|
|
|
41
41
|
isAnonymous: false,
|
|
42
42
|
alsoInferFunnels: true,
|
|
43
43
|
// concurrency automatically set to 1 when strictEventCount is enabled
|
|
44
|
-
writeToDisk:
|
|
44
|
+
writeToDisk: false,
|
|
45
45
|
batchSize: 12_500_000,
|
|
46
46
|
|
|
47
47
|
events: [
|
package/dungeons/big.js
CHANGED
|
@@ -19,7 +19,7 @@ const totalDays = (numQuarters * 90) + 10;
|
|
|
19
19
|
|
|
20
20
|
/** @type {import('../types').Dungeon} */
|
|
21
21
|
const config = {
|
|
22
|
-
// token:
|
|
22
|
+
// token: "",
|
|
23
23
|
seed: seed,
|
|
24
24
|
numDays: totalDays,
|
|
25
25
|
numEvents: totalEvents,
|
|
@@ -39,7 +39,7 @@ const config = {
|
|
|
39
39
|
hasCampaigns: false,
|
|
40
40
|
hasDesktopDevices: false,
|
|
41
41
|
hasIOSDevices: false,
|
|
42
|
-
writeToDisk:
|
|
42
|
+
writeToDisk: false,
|
|
43
43
|
funnels: [
|
|
44
44
|
{
|
|
45
45
|
"sequence": ["foo", "bar", "baz", "qux", "garply", "durtle", "linny", "fonk", "crumn", "yak"],
|
package/dungeons/business.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* notice how the config object is structured, and see it's type definition in ./types.d.ts
|
|
4
|
-
* feel free to modify this file to customize the data you generate
|
|
5
|
-
* see helper functions in utils.js for more ways to generate data
|
|
2
|
+
* Video platform dungeon (business/complex mode)
|
|
6
3
|
*/
|
|
7
4
|
|
|
5
|
+
import Chance from 'chance';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
7
|
+
import utc from 'dayjs/plugin/utc.js';
|
|
8
|
+
dayjs.extend(utc);
|
|
9
|
+
import { weighNumRange, pickAWinner, exhaust } from '../lib/utils/utils.js';
|
|
10
|
+
import * as v from 'ak-tools';
|
|
8
11
|
|
|
9
|
-
const Chance = require('chance');
|
|
10
12
|
const chance = new Chance();
|
|
11
|
-
const
|
|
12
|
-
const u = require('ak-tools');
|
|
13
|
+
const integer = (min, max) => chance.integer({ min, max });
|
|
13
14
|
|
|
14
15
|
const channel_ids = [...Array(1234).keys()].map(i => i + 1).map(n => `channel_id_${n}`);
|
|
15
|
-
const channel_names = chance.n(
|
|
16
|
+
const channel_names = chance.n(v.makeName, 1234);
|
|
16
17
|
const video_ids = [...Array(50000).keys()].map(i => i + 1).map(n => n.toString());
|
|
17
|
-
const video_names = chance.n(
|
|
18
|
+
const video_names = chance.n(v.makeName, 50000);
|
|
18
19
|
|
|
19
20
|
const EVENTS = 50_000
|
|
20
21
|
const USERS = EVENTS / 100
|
|
@@ -22,7 +23,7 @@ const USERS = EVENTS / 100
|
|
|
22
23
|
|
|
23
24
|
/** @type {import('../types.js').Dungeon} */
|
|
24
25
|
const config = {
|
|
25
|
-
token:
|
|
26
|
+
token: "",
|
|
26
27
|
seed: "it's business time...",
|
|
27
28
|
numDays: 90, //how many days worth of data
|
|
28
29
|
numEvents: EVENTS, //how many events
|
|
@@ -258,6 +259,52 @@ const config = {
|
|
|
258
259
|
],
|
|
259
260
|
|
|
260
261
|
hook: function (record, type, meta) {
|
|
262
|
+
// --- user hook: tag users by their experiment variant ---
|
|
263
|
+
if (type === "user") {
|
|
264
|
+
const exp = record.experiment;
|
|
265
|
+
if (exp && Array.isArray(exp) && exp[0]) {
|
|
266
|
+
record.experimentGroup = exp[0].variant || "unknown";
|
|
267
|
+
}
|
|
268
|
+
return record;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- event hook: weekend watch time boost + premium quality bias ---
|
|
272
|
+
if (type === "event") {
|
|
273
|
+
if (record.event === "watch video" && record.time) {
|
|
274
|
+
const day = dayjs(record.time).day();
|
|
275
|
+
if (day === 0 || day === 6) {
|
|
276
|
+
record["watch time (sec)"] = Math.round((record["watch time (sec)"] || 60) * 1.8);
|
|
277
|
+
record.is_weekend_session = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// app errors on weekends are more severe (skeleton crew)
|
|
281
|
+
if (record.event === "app error" && record.time) {
|
|
282
|
+
const day = dayjs(record.time).day();
|
|
283
|
+
if (day === 0 || day === 6) {
|
|
284
|
+
record.weekend_incident = true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return record;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- everything hook: binge watchers (5+ watch events) get a "binge_session" event ---
|
|
291
|
+
if (type === "everything") {
|
|
292
|
+
const watches = record.filter(e => e.event === "watch video");
|
|
293
|
+
if (watches.length >= 5) {
|
|
294
|
+
const totalWatchTime = watches.reduce((sum, e) => sum + (e["watch time (sec)"] || 0), 0);
|
|
295
|
+
const lastWatch = watches[watches.length - 1];
|
|
296
|
+
record.push({
|
|
297
|
+
event: "binge_session",
|
|
298
|
+
time: dayjs(lastWatch.time).add(1, "minute").toISOString(),
|
|
299
|
+
user_id: lastWatch.user_id,
|
|
300
|
+
videos_watched: watches.length,
|
|
301
|
+
total_watch_time_sec: totalWatchTime,
|
|
302
|
+
avg_watch_time_sec: Math.round(totalWatchTime / watches.length)
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
return record;
|
|
306
|
+
}
|
|
307
|
+
|
|
261
308
|
return record;
|
|
262
309
|
}
|
|
263
310
|
};
|
|
@@ -267,7 +314,7 @@ const config = {
|
|
|
267
314
|
function makeHashTags() {
|
|
268
315
|
const possibleHashtags = [];
|
|
269
316
|
for (let i = 0; i < 20; i++) {
|
|
270
|
-
possibleHashtags.push('#' +
|
|
317
|
+
possibleHashtags.push('#' + v.makeName(2, ''));
|
|
271
318
|
}
|
|
272
319
|
|
|
273
320
|
const numHashtags = integer(integer(1, 5), integer(5, 10));
|
|
@@ -342,4 +389,4 @@ function designExperiment() {
|
|
|
342
389
|
|
|
343
390
|
|
|
344
391
|
|
|
345
|
-
|
|
392
|
+
export default config;
|
package/dungeons/complex.js
CHANGED
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
import Chance from 'chance';
|
|
10
10
|
const chance = new Chance();
|
|
11
|
+
import dayjs from "dayjs";
|
|
12
|
+
import utc from "dayjs/plugin/utc.js";
|
|
13
|
+
dayjs.extend(utc);
|
|
11
14
|
import { weighNumRange, date, integer } from "../lib/utils/utils.js";
|
|
12
15
|
import * as u from 'ak-tools';
|
|
13
16
|
|
|
14
17
|
/** @type {import('../types.js').Dungeon} */
|
|
15
18
|
const config = {
|
|
16
|
-
token:
|
|
19
|
+
token: "",
|
|
17
20
|
seed: "quite complexus",
|
|
18
21
|
numDays: 30, //how many days worth of data
|
|
19
22
|
numEvents: 100_000, //how many events
|
|
@@ -281,6 +284,36 @@ const config = {
|
|
|
281
284
|
],
|
|
282
285
|
|
|
283
286
|
hook: function (record, type, meta) {
|
|
287
|
+
// event hook: weekend watch time boost — videos watched on weekends get 1.5x duration
|
|
288
|
+
if (type === "event") {
|
|
289
|
+
if (record.event === "watch video" && record.time) {
|
|
290
|
+
const day = dayjs(record.time).day();
|
|
291
|
+
if (day === 0 || day === 6) {
|
|
292
|
+
record.watchTimeSec = Math.round((record.watchTimeSec || 60) * 1.5);
|
|
293
|
+
record.is_weekend = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// support tickets on high-severity get escalated flag
|
|
297
|
+
if (record.event === "support ticket" && record.severity === "high") {
|
|
298
|
+
record.escalated = true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// everything hook: simulate cart abandonment — users who "add to cart" but never "checkout" lose their last add-to-cart
|
|
303
|
+
if (type === "everything") {
|
|
304
|
+
const hasCheckout = record.some(e => e.event === "checkout");
|
|
305
|
+
const hasAddToCart = record.some(e => e.event === "add to cart");
|
|
306
|
+
if (hasAddToCart && !hasCheckout) {
|
|
307
|
+
// mark all their add-to-cart events as abandoned
|
|
308
|
+
for (const e of record) {
|
|
309
|
+
if (e.event === "add to cart") {
|
|
310
|
+
e.abandoned = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return record;
|
|
315
|
+
}
|
|
316
|
+
|
|
284
317
|
return record;
|
|
285
318
|
}
|
|
286
319
|
};
|
package/dungeons/copilot.js
CHANGED
|
@@ -66,7 +66,7 @@ function makeProducts(maxItems = 5) {
|
|
|
66
66
|
|
|
67
67
|
/** @type {import('../types').Dungeon} */
|
|
68
68
|
const config = {
|
|
69
|
-
token: "
|
|
69
|
+
token: "",
|
|
70
70
|
seed: "simple is best",
|
|
71
71
|
numDays: 108, //how many days worth1 of data
|
|
72
72
|
numEvents: 2_000_000, //how many events
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dayjs from "dayjs";
|
|
2
2
|
import utc from "dayjs/plugin/utc.js";
|
|
3
3
|
import "dotenv/config";
|
|
4
|
-
import * as u from "
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
5
|
import * as v from "ak-tools";
|
|
6
6
|
|
|
7
7
|
const SEED = "harness-education";
|
|
@@ -95,7 +95,7 @@ const problemIds = v.range(1, 601).map(n => `problem_${v.uid(6)}`);
|
|
|
95
95
|
|
|
96
96
|
/** @type {Config} */
|
|
97
97
|
const config = {
|
|
98
|
-
token: "
|
|
98
|
+
token: "",
|
|
99
99
|
seed: SEED,
|
|
100
100
|
numDays: days,
|
|
101
101
|
numEvents: num_users * 120,
|
|
@@ -457,7 +457,7 @@ const config = {
|
|
|
457
457
|
}
|
|
458
458
|
|
|
459
459
|
// ═══════════════════════════════════════════════════════════════════
|
|
460
|
-
// Hook #6: SEMESTER-END SPIKE
|
|
460
|
+
// Hook #6: SEMESTER-END SPIKE (tag in event hook, duplicate in everything hook)
|
|
461
461
|
// ═══════════════════════════════════════════════════════════════════
|
|
462
462
|
if (type === "event") {
|
|
463
463
|
if (record.time) {
|
|
@@ -468,14 +468,6 @@ const config = {
|
|
|
468
468
|
if (spikableEvents.includes(record.event)) {
|
|
469
469
|
if (dayInDataset >= 75 && dayInDataset <= 85) {
|
|
470
470
|
record.semester_end_rush = true;
|
|
471
|
-
|
|
472
|
-
// 50% chance to duplicate
|
|
473
|
-
if (chance.bool({ likelihood: 50 })) {
|
|
474
|
-
const duplicate = JSON.parse(JSON.stringify(record));
|
|
475
|
-
duplicate.time = eventTime.add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
|
|
476
|
-
duplicate.semester_end_rush = true;
|
|
477
|
-
return [record, duplicate];
|
|
478
|
-
}
|
|
479
471
|
} else {
|
|
480
472
|
record.semester_end_rush = false;
|
|
481
473
|
}
|
|
@@ -628,6 +620,31 @@ const config = {
|
|
|
628
620
|
}
|
|
629
621
|
});
|
|
630
622
|
}
|
|
623
|
+
|
|
624
|
+
// Hook #6: SEMESTER-END SPIKE - duplicate assessment events in the spike window
|
|
625
|
+
const duplicates = [];
|
|
626
|
+
userEvents.forEach((event) => {
|
|
627
|
+
if (event.semester_end_rush === true && chance.bool({ likelihood: 50 })) {
|
|
628
|
+
const dup = JSON.parse(JSON.stringify(event));
|
|
629
|
+
dup.time = dayjs(event.time).add(chance.integer({ min: 5, max: 120 }), 'minutes').toISOString();
|
|
630
|
+
dup.semester_end_rush = true;
|
|
631
|
+
duplicates.push(dup);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
if (duplicates.length > 0) {
|
|
635
|
+
userEvents.push(...duplicates);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Hook #7: FREE VS PAID - reinforce the subscription effect on certificates
|
|
639
|
+
const subStatus = userEvents.length > 0 ? userEvents[0].subscription_status : "free";
|
|
640
|
+
if (subStatus === "free") {
|
|
641
|
+
// Free users lose 40% of their certificates (simulating lower completion)
|
|
642
|
+
for (let i = userEvents.length - 1; i >= 0; i--) {
|
|
643
|
+
if (userEvents[i].event === "certificate earned" && chance.bool({ likelihood: 40 })) {
|
|
644
|
+
userEvents.splice(i, 1);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
631
648
|
}
|
|
632
649
|
|
|
633
650
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -944,7 +961,7 @@ export default config;
|
|
|
944
961
|
* import config from './dungeons/harness-education.js';
|
|
945
962
|
* const results = await generate(config);
|
|
946
963
|
*
|
|
947
|
-
* OUTPUT FILES (with writeToDisk:
|
|
964
|
+
* OUTPUT FILES (with writeToDisk: false, format: "json", gzip: true):
|
|
948
965
|
*
|
|
949
966
|
* - needle-haystack-education__events.json.gz - All event data
|
|
950
967
|
* - needle-haystack-education__user_profiles.json.gz - User profiles
|
package/dungeons/experiments.js
CHANGED
|
@@ -15,7 +15,7 @@ const days = 100;
|
|
|
15
15
|
|
|
16
16
|
/** @type {Config} */
|
|
17
17
|
const config = {
|
|
18
|
-
token:
|
|
18
|
+
token: "",
|
|
19
19
|
seed: SEED,
|
|
20
20
|
numDays: days,
|
|
21
21
|
numEvents: num_users * 100,
|
|
@@ -96,6 +96,12 @@ const config = {
|
|
|
96
96
|
|
|
97
97
|
if (type === "event") {
|
|
98
98
|
const EVENT_TIME = dayjs(record.time);
|
|
99
|
+
// Pattern 1: "variant 1" users spend more money (winning variant)
|
|
100
|
+
if (record.event === "money event" && record["Variant name"] === "variant 1") {
|
|
101
|
+
record.amount = Math.round((record.amount || 50) * 1.5);
|
|
102
|
+
}
|
|
103
|
+
// Pattern 2: "control" users see fewer rare events (simulating lower engagement)
|
|
104
|
+
// handled in "everything" below
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
if (type === "user") {
|
|
@@ -115,7 +121,14 @@ const config = {
|
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
if (type === "everything") {
|
|
118
|
-
|
|
124
|
+
// Pattern 3: Control variant users have 30% of their "rare event" events removed
|
|
125
|
+
// to simulate lower engagement in control group
|
|
126
|
+
return record.filter(e => {
|
|
127
|
+
if (e.event === "rare event" && e["Variant name"] === "control" && Math.random() < 0.3) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
});
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
return record;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dayjs from "dayjs";
|
|
2
2
|
import utc from "dayjs/plugin/utc.js";
|
|
3
3
|
import "dotenv/config";
|
|
4
|
-
import * as u from "
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
5
|
|
|
6
6
|
const SEED = "harness-fintech";
|
|
7
7
|
dayjs.extend(utc);
|
|
@@ -83,7 +83,7 @@ const days = 100;
|
|
|
83
83
|
|
|
84
84
|
/** @type {Config} */
|
|
85
85
|
const config = {
|
|
86
|
-
token: "
|
|
86
|
+
token: "",
|
|
87
87
|
seed: SEED,
|
|
88
88
|
numDays: days,
|
|
89
89
|
numEvents: num_users * 120,
|
|
@@ -192,7 +192,7 @@ const config = {
|
|
|
192
192
|
event: "balance checked",
|
|
193
193
|
weight: 15,
|
|
194
194
|
properties: {
|
|
195
|
-
"account_balance": u.weighNumRange(0, 50000, 0.
|
|
195
|
+
"account_balance": u.weighNumRange(0, 50000, 0.8, 2500),
|
|
196
196
|
"account_type": u.pickAWinner(["checking", "savings", "investment"]),
|
|
197
197
|
}
|
|
198
198
|
},
|
|
@@ -405,7 +405,7 @@ const config = {
|
|
|
405
405
|
// Payday: 1st and 15th
|
|
406
406
|
if (record.event === "transaction completed" && record.transaction_type === "direct_deposit") {
|
|
407
407
|
if (dayOfMonth === 1 || dayOfMonth === 15) {
|
|
408
|
-
record.amount = Math.floor((record.amount || 50) *
|
|
408
|
+
record.amount = Math.floor((record.amount || 50) * 3);
|
|
409
409
|
record.payday = true;
|
|
410
410
|
} else {
|
|
411
411
|
record.payday = false;
|
|
@@ -415,8 +415,8 @@ const config = {
|
|
|
415
415
|
// Post-payday spending: days 1-3 and 15-17
|
|
416
416
|
if (record.event === "transfer sent") {
|
|
417
417
|
const isPaydayWindow = (dayOfMonth >= 1 && dayOfMonth <= 3) || (dayOfMonth >= 15 && dayOfMonth <= 17);
|
|
418
|
-
if (isPaydayWindow && chance.bool({ likelihood:
|
|
419
|
-
record.amount = Math.floor((record.amount || 200) *
|
|
418
|
+
if (isPaydayWindow && chance.bool({ likelihood: 60 })) {
|
|
419
|
+
record.amount = Math.floor((record.amount || 200) * 2.0);
|
|
420
420
|
record.post_payday_spending = true;
|
|
421
421
|
} else {
|
|
422
422
|
record.post_payday_spending = false;
|
|
@@ -579,7 +579,7 @@ const config = {
|
|
|
579
579
|
// -----------------------------------------------------------
|
|
580
580
|
let lowBalanceChecks = 0;
|
|
581
581
|
userEvents.forEach((event) => {
|
|
582
|
-
if (event.event === "balance checked" && (event.account_balance || 0) <
|
|
582
|
+
if (event.event === "balance checked" && (event.account_balance || 0) < 3000) {
|
|
583
583
|
lowBalanceChecks++;
|
|
584
584
|
}
|
|
585
585
|
});
|
|
@@ -934,7 +934,7 @@ export default config;
|
|
|
934
934
|
* import config from './dungeons/harness-fintech.js';
|
|
935
935
|
* const results = await generate(config);
|
|
936
936
|
*
|
|
937
|
-
* OUTPUT FILES (with writeToDisk:
|
|
937
|
+
* OUTPUT FILES (with writeToDisk: false):
|
|
938
938
|
*
|
|
939
939
|
* - needle-haystack-fintech__events.json.gz - All event data
|
|
940
940
|
* - needle-haystack-fintech__user_profiles.json.gz - User profiles
|
package/dungeons/foobar.js
CHANGED
|
@@ -136,7 +136,7 @@ const seed = Math.random().toString()
|
|
|
136
136
|
|
|
137
137
|
/** @type {import('../types').Dungeon} */
|
|
138
138
|
const config = {
|
|
139
|
-
token:
|
|
139
|
+
token: "",
|
|
140
140
|
seed: seed,
|
|
141
141
|
numDays: 30, //how many days worth of data
|
|
142
142
|
numEvents: numEvents, //how many events
|
|
@@ -232,6 +232,38 @@ const config = {
|
|
|
232
232
|
},
|
|
233
233
|
|
|
234
234
|
hook: function (record, type, meta) {
|
|
235
|
+
// event hook: high-weight events get a "hot" flag, low-weight get "cold"
|
|
236
|
+
if (type === "event") {
|
|
237
|
+
const hotEvents = ["foo", "bar", "baz"];
|
|
238
|
+
const coldEvents = ["crumn", "yak"];
|
|
239
|
+
if (hotEvents.includes(record.event)) {
|
|
240
|
+
record.temperature = "hot";
|
|
241
|
+
} else if (coldEvents.includes(record.event)) {
|
|
242
|
+
record.temperature = "cold";
|
|
243
|
+
} else {
|
|
244
|
+
record.temperature = "warm";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// everything hook: hash-based cohort — 10% of users (by distinct_id) get doubled events
|
|
249
|
+
if (type === "everything") {
|
|
250
|
+
if (record.length > 0) {
|
|
251
|
+
const userId = record[0].user_id || record[0].distinct_id || "";
|
|
252
|
+
if (userId && userId.charCodeAt(0) % 10 === 0) {
|
|
253
|
+
// power user: duplicate each event with a slight time offset
|
|
254
|
+
const extras = record.slice(0, 3).map(e => ({
|
|
255
|
+
...e,
|
|
256
|
+
event: e.event,
|
|
257
|
+
user_id: e.user_id,
|
|
258
|
+
time: e.time,
|
|
259
|
+
is_duplicate: true,
|
|
260
|
+
}));
|
|
261
|
+
return record.concat(extras);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return record;
|
|
265
|
+
}
|
|
266
|
+
|
|
235
267
|
return record;
|
|
236
268
|
}
|
|
237
269
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dayjs from "dayjs";
|
|
2
2
|
import utc from "dayjs/plugin/utc.js";
|
|
3
3
|
import "dotenv/config";
|
|
4
|
-
import * as u from "
|
|
4
|
+
import * as u from "../lib/utils/utils.js";
|
|
5
5
|
import * as v from "ak-tools";
|
|
6
6
|
|
|
7
7
|
const SEED = "harness-food";
|
|
@@ -77,7 +77,7 @@ const couponCodes = v.range(1, 51).map(n => `QUICK${v.uid(5).toUpperCase()}`);
|
|
|
77
77
|
|
|
78
78
|
/** @type {Config} */
|
|
79
79
|
const config = {
|
|
80
|
-
token: "
|
|
80
|
+
token: "",
|
|
81
81
|
seed: SEED,
|
|
82
82
|
numDays: days,
|
|
83
83
|
numEvents: num_users * 120,
|
|
@@ -447,7 +447,10 @@ const config = {
|
|
|
447
447
|
record.sequence[0] === "checkout started" &&
|
|
448
448
|
record.sequence[1] === "order placed") {
|
|
449
449
|
record.props = record.props || {};
|
|
450
|
-
|
|
450
|
+
// Use hash to deterministically assign ~50% of users as "new"
|
|
451
|
+
const userId = meta && meta.user && (meta.user.distinct_id || String(meta.user));
|
|
452
|
+
const isNewUser = userId && userId.charCodeAt(0) % 2 === 0;
|
|
453
|
+
if (isNewUser) {
|
|
451
454
|
record.conversionRate = 0.90;
|
|
452
455
|
record.props.first_order_bonus = true;
|
|
453
456
|
} else {
|
|
@@ -941,7 +944,7 @@ export default config;
|
|
|
941
944
|
* import config from './dungeons/harness-food.js';
|
|
942
945
|
* const results = await generate(config);
|
|
943
946
|
*
|
|
944
|
-
* OUTPUT FILES (with writeToDisk:
|
|
947
|
+
* OUTPUT FILES (with writeToDisk: false, format: "json", gzip: true):
|
|
945
948
|
*
|
|
946
949
|
* - needle-haystack-food__events.json.gz - All event data
|
|
947
950
|
* - needle-haystack-food__user_profiles.json.gz - User profiles
|