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.
Files changed (50) hide show
  1. package/dungeons/adspend.js +35 -1
  2. package/dungeons/anon.js +25 -1
  3. package/dungeons/{array-of-object-loopup.js → array-of-object-lookup.js} +28 -8
  4. package/dungeons/benchmark-heavy.js +2 -2
  5. package/dungeons/benchmark-light.js +2 -2
  6. package/dungeons/big.js +2 -2
  7. package/dungeons/business.js +59 -12
  8. package/dungeons/complex.js +34 -1
  9. package/dungeons/copilot.js +1 -1
  10. package/dungeons/{harness/harness-education.js → education.js} +29 -12
  11. package/dungeons/experiments.js +15 -2
  12. package/dungeons/{harness/harness-fintech.js → fintech.js} +8 -8
  13. package/dungeons/foobar.js +33 -1
  14. package/dungeons/{harness/harness-food.js → food.js} +7 -4
  15. package/dungeons/funnels.js +38 -1
  16. package/dungeons/gaming.js +25 -5
  17. package/dungeons/media.js +861 -271
  18. package/dungeons/mil.js +29 -2
  19. package/dungeons/mirror.js +33 -1
  20. package/dungeons/{kurby.js → retention-cadence.js} +1 -1
  21. package/dungeons/{harness/harness-gaming.js → rpg.js} +5 -5
  22. package/dungeons/sanity.js +31 -2
  23. package/dungeons/{harness/harness-sass.js → sass.js} +2 -2
  24. package/dungeons/scd.js +46 -1
  25. package/dungeons/simple.js +1 -1
  26. package/dungeons/{harness/harness-social.js → social.js} +2 -2
  27. package/dungeons/streaming.js +373 -0
  28. package/dungeons/strict-event-test.js +1 -1
  29. package/dungeons/student-teacher.js +18 -5
  30. package/dungeons/text-generation.js +38 -1
  31. package/dungeons/too-big-events.js +38 -1
  32. package/dungeons/{userAgent.js → user-agent.js} +21 -1
  33. package/entry.js +5 -4
  34. package/lib/utils/logger.js +0 -4
  35. package/package.json +1 -4
  36. package/dungeons/ai-chat-analytics-ed.js +0 -275
  37. package/dungeons/clinch-agi.js +0 -632
  38. package/dungeons/ecommerce-store.js +0 -0
  39. package/dungeons/harness/harness-media.js +0 -961
  40. package/dungeons/money2020-ed-also.js +0 -277
  41. package/dungeons/money2020-ed.js +0 -580
  42. package/dungeons/uday-schema.json +0 -220
  43. package/lib/templates/funnels-instructions.txt +0 -272
  44. package/lib/templates/hook-examples.json +0 -187
  45. package/lib/templates/hooks-instructions.txt +0 -721
  46. package/lib/templates/refine-instructions.txt +0 -485
  47. package/lib/templates/schema-instructions.txt +0 -285
  48. package/lib/utils/ai.js +0 -896
  49. package/lib/utils/mixpanel.js +0 -101
  50. package/lib/utils/project.js +0 -167
@@ -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: process.env.MASTER_PROJECT_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: process.env.MASTER_PROJECT_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: "0a3e6aa01225ed03856dca545c2b5b3d",
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: true,
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: process.env.MASTER_PROJECT_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: true,
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: process.env.MASTER_PROJECT_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: true,
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: process.env.MASTER_PROJECT_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: "gs://dungeon_master_4/big_data",
42
+ writeToDisk: false,
43
43
  funnels: [
44
44
  {
45
45
  "sequence": ["foo", "bar", "baz", "qux", "garply", "durtle", "linny", "fonk", "crumn", "yak"],
@@ -1,20 +1,21 @@
1
1
  /**
2
- * This is the default configuration file for the data generator in COMPLEX mode
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 { weighNumRange, date, integer, pickAWinner, exhaust } = require('../lib/utils/utils.js');
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(u.makeName, 1234);
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(u.makeName, 50000);
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: process.env.MASTER_PROJECT_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('#' + u.makeName(2, ''));
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
- module.exports = config;
392
+ export default config;
@@ -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: process.env.MASTER_PROJECT_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
  };
@@ -66,7 +66,7 @@ function makeProducts(maxItems = 5) {
66
66
 
67
67
  /** @type {import('../types').Dungeon} */
68
68
  const config = {
69
- token: "a4152b30c5276306151de1146321997e",
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 "../../lib/utils/utils.js";
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: "10a2fd5d566edd19e803036b276fe91b",
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: true, format: "json", gzip: true):
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
@@ -15,7 +15,7 @@ const days = 100;
15
15
 
16
16
  /** @type {Config} */
17
17
  const config = {
18
- token: process.env.MASTER_PROJECT_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 "../../lib/utils/utils.js";
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: "048f954dddbfbd4bff34edd8b3d95c06",
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.3, 2500),
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) * 2);
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: 40 })) {
419
- record.amount = Math.floor((record.amount || 200) * 1.5);
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) < 500) {
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: true):
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
@@ -136,7 +136,7 @@ const seed = Math.random().toString()
136
136
 
137
137
  /** @type {import('../types').Dungeon} */
138
138
  const config = {
139
- token: process.env.MASTER_PROJECT_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 "../../lib/utils/utils.js";
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: "9ffdb0649ebbea4ff7a742725166b06f",
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
- if (meta && meta.userIsBornInDataset) {
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: true, format: "json", gzip: true):
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