make-mp-data 3.0.5 → 3.1.0
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 +34 -0
- package/dungeons/complex.js +428 -0
- package/entry.js +1 -1
- package/index.js +7 -2
- package/lib/cli/cli.js +3 -11
- package/lib/core/config-validator.js +7 -10
- package/lib/orchestrators/mixpanel-sender.js +49 -37
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,6 +13,30 @@ under the hood, `make-mp-data` is modeling data adherent to match [Mixpanel's da
|
|
|
13
13
|
|
|
14
14
|
## 🚀 Quick Start
|
|
15
15
|
|
|
16
|
+
### Canonical Usage (v3.1.0+)
|
|
17
|
+
|
|
18
|
+
Two paths are guaranteed to "just work":
|
|
19
|
+
|
|
20
|
+
**1. As a CLI — send data to Mixpanel with one flag:**
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx make-mp-data --token YOUR_PROJECT_TOKEN
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**2. As an ES module — bare `await` call:**
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
import makeMpData from 'make-mp-data';
|
|
30
|
+
|
|
31
|
+
// Zero-config: generates events + users, returns them in memory
|
|
32
|
+
const result = await makeMpData({});
|
|
33
|
+
|
|
34
|
+
// Canonical "just send it": pass a token, data ships to Mixpanel
|
|
35
|
+
await makeMpData({ token: 'YOUR_PROJECT_TOKEN' });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Both paths return a `Result` object with `eventData`, `userProfilesData`, `importResults`, `files`, `eventCount`, `userCount`, and timing info.
|
|
39
|
+
|
|
16
40
|
### Basic Usage
|
|
17
41
|
|
|
18
42
|
Generate events and users, and write them to CSV files:
|
|
@@ -87,6 +111,16 @@ Here's a breakdown of the CLI options you can use with `make-mp-data`:
|
|
|
87
111
|
- `--complex`: create a complex set models including groups, SCD, and lookup tables.
|
|
88
112
|
- `--simple`: create a simple dataset including events, and users
|
|
89
113
|
|
|
114
|
+
### Custom Dungeon Configs
|
|
115
|
+
|
|
116
|
+
Pass a path to your own dungeon `.js` file. CLI flags override values from the dungeon — your file provides defaults, the CLI tunes them:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx make-mp-data ./my-dungeon.js --numUsers 500 --token YOUR_TOKEN
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
CLI flag defaults (like `region`, `concurrency`) **do not** clobber explicit values in your dungeon — only flags you actually pass take effect.
|
|
123
|
+
|
|
90
124
|
## ⏱️ TimeSoup — Realistic Time Distributions
|
|
91
125
|
|
|
92
126
|
TimeSoup controls how events are distributed across time. Out of the box, it produces realistic day-of-week and hour-of-day patterns derived from real Mixpanel data (weekday-heavy, Saturday valley, morning peak).
|
|
@@ -0,0 +1,428 @@
|
|
|
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
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import Chance from 'chance';
|
|
10
|
+
const chance = new Chance();
|
|
11
|
+
import dayjs from "dayjs";
|
|
12
|
+
import utc from "dayjs/plugin/utc.js";
|
|
13
|
+
dayjs.extend(utc);
|
|
14
|
+
import { weighNumRange, date, integer } from "../lib/utils/utils.js";
|
|
15
|
+
import * as u from 'ak-tools';
|
|
16
|
+
|
|
17
|
+
/** @type {import('../types.js').Dungeon} */
|
|
18
|
+
const config = {
|
|
19
|
+
token: "",
|
|
20
|
+
seed: "quite complexus",
|
|
21
|
+
numDays: 30, //how many days worth of data
|
|
22
|
+
numEvents: 100_000, //how many events
|
|
23
|
+
numUsers: 1000, //how many users
|
|
24
|
+
format: 'json', //csv or json
|
|
25
|
+
region: "US",
|
|
26
|
+
hasAnonIds: true, //if true, anonymousIds are created for each user
|
|
27
|
+
hasSessionIds: true, //if true, hasSessionIds are created for each user
|
|
28
|
+
|
|
29
|
+
hasLocation: true,
|
|
30
|
+
hasAndroidDevices: true,
|
|
31
|
+
hasIOSDevices: true,
|
|
32
|
+
hasDesktopDevices: true,
|
|
33
|
+
hasBrowser: true,
|
|
34
|
+
hasCampaigns: true,
|
|
35
|
+
isAnonymous: false,
|
|
36
|
+
hasAdSpend: true,
|
|
37
|
+
|
|
38
|
+
hasAvatar: true,
|
|
39
|
+
|
|
40
|
+
batchSize: 500_000,
|
|
41
|
+
concurrency: 1,
|
|
42
|
+
|
|
43
|
+
funnels: [],
|
|
44
|
+
events: [
|
|
45
|
+
{
|
|
46
|
+
"event": "checkout",
|
|
47
|
+
"weight": 2,
|
|
48
|
+
"properties": {
|
|
49
|
+
amount: weighNumRange(5, 500, .25),
|
|
50
|
+
currency: ["USD", "USD", "USD", "CAD", "EUR", "EUR", "BTC", "BTC", "ETH", "JPY"],
|
|
51
|
+
cart: makeProducts(12),
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"event": "add to cart",
|
|
56
|
+
"weight": 4,
|
|
57
|
+
"properties": {
|
|
58
|
+
amount: weighNumRange(5, 500, .25),
|
|
59
|
+
qty: integer(1, 5),
|
|
60
|
+
product_id: weighNumRange(1, 1000, 1.4)
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"event": "page view",
|
|
65
|
+
"weight": 10,
|
|
66
|
+
"properties": {
|
|
67
|
+
page: ["/", "/", "/", "/learn-more", "/pricing", "/contact", "/about", "/careers", "/sign-up", "/login", "/app", "/app", "/app", "/app"],
|
|
68
|
+
utm_source: ["$organic", "$organic", "$organic", "$organic", "google", "google", "google", "facebook", "facebook", "twitter", "linkedin"],
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"event": "watch video",
|
|
73
|
+
"weight": 8,
|
|
74
|
+
"properties": {
|
|
75
|
+
category: ["funny", "educational", "inspirational", "music", "news", "sports", "cooking", "DIY", "travel", "gaming"],
|
|
76
|
+
hashTags: makeHashTags,
|
|
77
|
+
watchTimeSec: weighNumRange(10, 600, .25,),
|
|
78
|
+
quality: ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p"],
|
|
79
|
+
format: ["mp4", "avi", "mov", "mpg"],
|
|
80
|
+
video_id: weighNumRange(1, 50000, 1.4),
|
|
81
|
+
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"event": "comment",
|
|
86
|
+
"weight": 2,
|
|
87
|
+
"properties": {
|
|
88
|
+
length: weighNumRange(1, 500, .25),
|
|
89
|
+
video_id: weighNumRange(1, 50000, 1.4),
|
|
90
|
+
has_replies: [true, false, false, false, false],
|
|
91
|
+
has_photo: [true, false, false, false, false],
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"event": "save video",
|
|
97
|
+
"weight": 4,
|
|
98
|
+
"properties": {
|
|
99
|
+
video_id: weighNumRange(1, 50000, 1.4),
|
|
100
|
+
ui_control: ["toolbar", "menu", "keyboard"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"event": "view item",
|
|
107
|
+
"weight": 8,
|
|
108
|
+
"properties": {
|
|
109
|
+
product_id: weighNumRange(1, 24, 3),
|
|
110
|
+
colors: ["light", "dark", "custom", "dark"]
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"event": "save item",
|
|
115
|
+
"weight": 5,
|
|
116
|
+
"properties": {
|
|
117
|
+
product_id: weighNumRange(1, 1000, 12),
|
|
118
|
+
colors: ["light", "dark", "custom", "dark"]
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"event": "support ticket",
|
|
123
|
+
"weight": 2,
|
|
124
|
+
"properties": {
|
|
125
|
+
product_id: weighNumRange(1, 1000, .6),
|
|
126
|
+
description: chance.sentence.bind(chance),
|
|
127
|
+
severity: ["low", "medium", "high"],
|
|
128
|
+
ticket_id: chance.guid.bind(chance)
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"event": "sign up",
|
|
133
|
+
"isFirstEvent": true,
|
|
134
|
+
"weight": 0,
|
|
135
|
+
"properties": {
|
|
136
|
+
plan: ["free", "free", "free", "free", "basic", "basic", "basic", "premium", "premium", "enterprise"],
|
|
137
|
+
dateOfRenewal: date(100, false),
|
|
138
|
+
codewords: u.makeName,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
superProps: {
|
|
143
|
+
linked_device: deviceAttributes()
|
|
144
|
+
// emotions: generateEmoji(),
|
|
145
|
+
|
|
146
|
+
},
|
|
147
|
+
/*
|
|
148
|
+
user properties work the same as event properties
|
|
149
|
+
each key should be an array or function reference
|
|
150
|
+
*/
|
|
151
|
+
userProps: {
|
|
152
|
+
title: chance.profession.bind(chance),
|
|
153
|
+
luckyNumber: weighNumRange(42, 420),
|
|
154
|
+
experiment: designExperiment(),
|
|
155
|
+
spiritAnimal: ["unicorn", "dragon", "phoenix", "sasquatch", "yeti", "kraken", "jackalope", "thunderbird", "mothman", "nessie", "chupacabra", "jersey devil", "bigfoot", "weindgo", "bunyip", "mokele-mbembe", "tatzelwurm", "megalodon"],
|
|
156
|
+
timezone: chance.timezone.bind(chance), // ["America/New_York", "America/Los_Angeles", "America/Chicago", "America/Denver", "America/Phoenix", "America/Anchorage", "Pacific/Honolulu"]
|
|
157
|
+
ip: chance.ip.bind(chance),
|
|
158
|
+
lastCart: makeProducts(5),
|
|
159
|
+
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/** each generates it's own table */
|
|
163
|
+
scdProps: {
|
|
164
|
+
role: {
|
|
165
|
+
type: "user",
|
|
166
|
+
frequency: "week",
|
|
167
|
+
values: ["admin", "collaborator", "user", "view only", "no access"],
|
|
168
|
+
timing: 'fuzzy',
|
|
169
|
+
max: 10
|
|
170
|
+
},
|
|
171
|
+
NPS: {
|
|
172
|
+
type: "user",
|
|
173
|
+
frequency: "day",
|
|
174
|
+
values: weighNumRange(1, 10, 2, 150),
|
|
175
|
+
timing: 'fuzzy',
|
|
176
|
+
max: 10
|
|
177
|
+
},
|
|
178
|
+
MRR: {
|
|
179
|
+
type: "company_id",
|
|
180
|
+
frequency: "month",
|
|
181
|
+
values: weighNumRange(0, 10000, .15),
|
|
182
|
+
timing: 'fixed',
|
|
183
|
+
max: 10
|
|
184
|
+
},
|
|
185
|
+
AccountHealthScore: {
|
|
186
|
+
type: "company_id",
|
|
187
|
+
frequency: "week",
|
|
188
|
+
values: weighNumRange(1, 10, .15),
|
|
189
|
+
timing: 'fixed',
|
|
190
|
+
max: 40
|
|
191
|
+
},
|
|
192
|
+
plan: {
|
|
193
|
+
type: "company_id",
|
|
194
|
+
frequency: "month",
|
|
195
|
+
values: ["free", "basic", "premium", "enterprise"],
|
|
196
|
+
timing: 'fixed',
|
|
197
|
+
max: 10
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
mirrorProps: {
|
|
202
|
+
isBot: { events: "*", values: [false, false, false, false, true] },
|
|
203
|
+
profit: { events: ["checkout"], values: [4, 2, 42, 420] },
|
|
204
|
+
watchTimeSec: {
|
|
205
|
+
events: ["watch video"],
|
|
206
|
+
values: weighNumRange(50, 1200, 2)
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/*
|
|
211
|
+
for group analytics keys, we need an array of arrays [[],[],[]]
|
|
212
|
+
each pair represents a group_key and the number of profiles for that key
|
|
213
|
+
*/
|
|
214
|
+
groupKeys: [
|
|
215
|
+
['company_id', 500, []],
|
|
216
|
+
['room_id', 10000, ["save video", "comment", "watch video"]],
|
|
217
|
+
|
|
218
|
+
],
|
|
219
|
+
groupProps: {
|
|
220
|
+
company_id: {
|
|
221
|
+
name: () => { return chance.company(); },
|
|
222
|
+
email: () => { return `CSM: ${chance.pickone(["AK", "Jessica", "Michelle", "Dana", "Brian", "Dave"])}`; },
|
|
223
|
+
"# of employees": weighNumRange(3, 10000),
|
|
224
|
+
"industry": ["tech", "finance", "healthcare", "education", "government", "non-profit"],
|
|
225
|
+
"segment": ["enterprise", "SMB", "mid-market"],
|
|
226
|
+
"products": [["core"], ["core"], ["core", "add-ons"], ["core", "pro-serve"], ["core", "add-ons", "pro-serve"], ["core", "BAA", "enterprise"], ["free"], ["free"], ["free", "addons"]],
|
|
227
|
+
},
|
|
228
|
+
room_id: {
|
|
229
|
+
name: () => { return `#${chance.word({ length: integer(4, 24), capitalize: true })}`; },
|
|
230
|
+
email: ["public", "private"],
|
|
231
|
+
"room provider": ["partner", "core", "core", "core"],
|
|
232
|
+
"room capacity": weighNumRange(3, 1000000),
|
|
233
|
+
"isPublic": [true, false, false, false, false],
|
|
234
|
+
"country": chance.country.bind(chance),
|
|
235
|
+
"isVerified": [true, true, false, false, false],
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
groupEvents: [{
|
|
239
|
+
attribute_to_user: false,
|
|
240
|
+
event: "card charged",
|
|
241
|
+
weight: 1,
|
|
242
|
+
frequency: 30,
|
|
243
|
+
group_key: "company_id",
|
|
244
|
+
group_size: 500,
|
|
245
|
+
properties: {
|
|
246
|
+
amount: weighNumRange(5, 500, .25),
|
|
247
|
+
currency: ["USD", "USD", "USD", "CAD", "EUR", "EUR", "BTC", "BTC", "ETH", "JPY"],
|
|
248
|
+
plan: ["basic", "premium", "enterprise"],
|
|
249
|
+
"payment method": []
|
|
250
|
+
}
|
|
251
|
+
}],
|
|
252
|
+
|
|
253
|
+
lookupTables: [
|
|
254
|
+
{
|
|
255
|
+
key: "product_id",
|
|
256
|
+
entries: 1000,
|
|
257
|
+
attributes: {
|
|
258
|
+
category: [
|
|
259
|
+
"Books", "Movies", "Music", "Games", "Electronics", "Computers", "Smart Home", "Home", "Garden & Tools", "Pet Supplies", "Food & Grocery", "Beauty", "Health", "Toys", "Kids", "Baby", "Handmade", "Sports", "Outdoors", "Automotive", "Industrial", "Entertainment", "Art"
|
|
260
|
+
],
|
|
261
|
+
"demand": ["high", "medium", "medium", "low"],
|
|
262
|
+
"supply": ["high", "medium", "medium", "low"],
|
|
263
|
+
"manufacturer": chance.company.bind(chance),
|
|
264
|
+
"price": weighNumRange(5, 500, .25),
|
|
265
|
+
"rating": weighNumRange(1, 5),
|
|
266
|
+
"reviews": weighNumRange(0, 35)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
key: "video_id",
|
|
272
|
+
entries: 50000,
|
|
273
|
+
attributes: {
|
|
274
|
+
isFlagged: [true, false, false, false, false],
|
|
275
|
+
copyright: ["all rights reserved", "creative commons", "creative commons", "public domain", "fair use"],
|
|
276
|
+
uploader_id: chance.guid.bind(chance),
|
|
277
|
+
"uploader influence": ["low", "low", "low", "medium", "medium", "high"],
|
|
278
|
+
thumbs: weighNumRange(0, 35),
|
|
279
|
+
rating: ["G", "PG", "PG-13", "R", "NC-17", "PG-13", "R", "NC-17", "R", "PG", "PG"]
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
}
|
|
283
|
+
],
|
|
284
|
+
|
|
285
|
+
hook: function (record, type, meta) {
|
|
286
|
+
// event hook: weekend watch time boost — videos watched on weekends get 1.5x duration
|
|
287
|
+
if (type === "event") {
|
|
288
|
+
if (record.event === "watch video" && record.time) {
|
|
289
|
+
const day = dayjs(record.time).day();
|
|
290
|
+
if (day === 0 || day === 6) {
|
|
291
|
+
record.watchTimeSec = Math.round((record.watchTimeSec || 60) * 1.5);
|
|
292
|
+
record.is_weekend = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// support tickets on high-severity get escalated flag
|
|
296
|
+
if (record.event === "support ticket" && record.severity === "high") {
|
|
297
|
+
record.escalated = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// everything hook: simulate cart abandonment — users who "add to cart" but never "checkout" lose their last add-to-cart
|
|
302
|
+
if (type === "everything") {
|
|
303
|
+
const hasCheckout = record.some(e => e.event === "checkout");
|
|
304
|
+
const hasAddToCart = record.some(e => e.event === "add to cart");
|
|
305
|
+
if (hasAddToCart && !hasCheckout) {
|
|
306
|
+
// mark all their add-to-cart events as abandoned
|
|
307
|
+
for (const e of record) {
|
|
308
|
+
if (e.event === "add to cart") {
|
|
309
|
+
e.abandoned = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return record;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return record;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
function makeHashTags() {
|
|
323
|
+
const possibleHashtags = [];
|
|
324
|
+
for (let i = 0; i < 20; i++) {
|
|
325
|
+
possibleHashtags.push('#' + u.makeName(2, ''));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const numHashtags = integer(integer(1, 5), integer(5, 10));
|
|
329
|
+
const hashtags = [];
|
|
330
|
+
for (let i = 0; i < numHashtags; i++) {
|
|
331
|
+
hashtags.push(chance.pickone(possibleHashtags));
|
|
332
|
+
}
|
|
333
|
+
return [hashtags];
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
function makeProducts(maxItems = 10) {
|
|
337
|
+
|
|
338
|
+
return function () {
|
|
339
|
+
const categories = ["Device Accessories", "eBooks", "Automotive", "Baby Products", "Beauty", "Books", "Camera & Photo", "Cell Phones & Accessories", "Collectible Coins", "Consumer Electronics", "Entertainment Collectibles", "Fine Art", "Grocery & Gourmet Food", "Health & Personal Care", "Home & Garden", "Independent Design", "Industrial & Scientific", "Accessories", "Major Appliances", "Music", "Musical Instruments", "Office Products", "Outdoors", "Personal Computers", "Pet Supplies", "Software", "Sports", "Sports Collectibles", "Tools & Home Improvement", "Toys & Games", "Video, DVD & Blu-ray", "Video Games", "Watches"];
|
|
340
|
+
const slugs = ['/sale/', '/featured/', '/home/', '/search/', '/wishlist/', '/'];
|
|
341
|
+
const assetExtension = ['.png', '.jpg', '.jpeg', '.heic', '.mp4', '.mov', '.avi'];
|
|
342
|
+
const data = [];
|
|
343
|
+
const numOfItems = integer(1, 12);
|
|
344
|
+
|
|
345
|
+
for (var i = 0; i < numOfItems; i++) {
|
|
346
|
+
const category = chance.pickone(categories);
|
|
347
|
+
const slug = chance.pickone(slugs);
|
|
348
|
+
const asset = chance.pickone(assetExtension);
|
|
349
|
+
const product_id = chance.guid();
|
|
350
|
+
const price = integer(1, 300);
|
|
351
|
+
const quantity = integer(1, 5);
|
|
352
|
+
|
|
353
|
+
const item = {
|
|
354
|
+
product_id: product_id,
|
|
355
|
+
sku: integer(11111, 99999),
|
|
356
|
+
amount: price,
|
|
357
|
+
quantity: quantity,
|
|
358
|
+
value: price * quantity,
|
|
359
|
+
featured: chance.pickone([true, false]),
|
|
360
|
+
category: category,
|
|
361
|
+
urlSlug: slug + category,
|
|
362
|
+
asset: `${category}-${integer(1, 20)}${asset}`
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
data.push(item);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return () => [data];
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
function designExperiment() {
|
|
374
|
+
return function () {
|
|
375
|
+
const variants = ["A", "B", "C", "Control"];
|
|
376
|
+
const variant = chance.pickone(variants);
|
|
377
|
+
const experiments = ["no password", "social sign in", "new tutorial", "new search"];
|
|
378
|
+
const experiment = chance.pickone(experiments);
|
|
379
|
+
const multi_variates = ["A/B", "A/B/C", "A/B/C/D", "Control"];
|
|
380
|
+
const multi_variate = chance.pickone(multi_variates);
|
|
381
|
+
const impression_id = chance.guid();
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
const chosen = {
|
|
386
|
+
variant,
|
|
387
|
+
experiment,
|
|
388
|
+
multi_variate,
|
|
389
|
+
impression_id
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return [chosen];
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function deviceAttributes(isMobile = false) {
|
|
397
|
+
return function () {
|
|
398
|
+
let devices = ["desktop", "laptop", "desktop", "laptop", "desktop", "laptop", "other"];
|
|
399
|
+
if (isMobile) devices = [...devices, "mobile", "mobile", "mobile", "tablet"];
|
|
400
|
+
const device = chance.pickone(devices);
|
|
401
|
+
let oses = ["Windows", "macOS", "Windows", "macOS", "macOS", "Linux", "Windows", "macOS", "Windows", "macOS", "macOS", "TempleOS"];
|
|
402
|
+
if (isMobile) oses = [...oses, "iOS", "Android", "iOS", "Android"];
|
|
403
|
+
const os = chance.pickone(oses);
|
|
404
|
+
const browser = chance.pickone(["Chrome", "Firefox", "Safari", "Edge", "Opera", "IE", "Brave", "Vivaldi"]);
|
|
405
|
+
const version = chance.integer({ min: 1, max: 15 });
|
|
406
|
+
const resolution = chance.pickone(["1920x1080", "1280x720", "1024x768", "800x600", "640x480"]);
|
|
407
|
+
const language = chance.pickone(["en-US", "en-US", "en-US", "en-GB", "es", "es", "fr", "de", "it", "ja", "zh", "ru"]);
|
|
408
|
+
|
|
409
|
+
const chosen = {
|
|
410
|
+
platform: device,
|
|
411
|
+
os,
|
|
412
|
+
browser,
|
|
413
|
+
version,
|
|
414
|
+
resolution,
|
|
415
|
+
language
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
return chosen;
|
|
419
|
+
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
export default config;
|
package/entry.js
CHANGED
|
@@ -59,7 +59,7 @@ import getCliParams from './lib/cli/cli.js';
|
|
|
59
59
|
process.exit(0);
|
|
60
60
|
} catch (error) {
|
|
61
61
|
console.error(`\n❌ Job failed: ${error.message}`);
|
|
62
|
-
if (process.env.NODE_ENV
|
|
62
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
63
63
|
console.error(error.stack);
|
|
64
64
|
}
|
|
65
65
|
process.exit(1);
|
package/index.js
CHANGED
|
@@ -34,6 +34,8 @@ import dayjs from "dayjs";
|
|
|
34
34
|
import utc from "dayjs/plugin/utc.js";
|
|
35
35
|
import { timer } from 'ak-tools';
|
|
36
36
|
import { existsSync } from 'fs';
|
|
37
|
+
import path from 'path';
|
|
38
|
+
import { pathToFileURL } from 'url';
|
|
37
39
|
import { dataLogger as logger } from './lib/utils/logger.js';
|
|
38
40
|
|
|
39
41
|
// Initialize dayjs and time constants
|
|
@@ -123,8 +125,11 @@ async function main(config) {
|
|
|
123
125
|
console.log(`\n🔍 Loading dungeon config from: ${firstArg}`);
|
|
124
126
|
}
|
|
125
127
|
try {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
+
const absolutePath = path.resolve(firstArg);
|
|
129
|
+
const dungeonConfig = await import(pathToFileURL(absolutePath).href);
|
|
130
|
+
const loaded = dungeonConfig.default || dungeonConfig;
|
|
131
|
+
// Merge: dungeon config provides defaults; CLI flags override
|
|
132
|
+
config = { ...loaded, ...config };
|
|
128
133
|
} catch (error) {
|
|
129
134
|
console.error(`\n❌ Error loading dungeon config from ${firstArg}: ${error.message}`);
|
|
130
135
|
throw error;
|
package/lib/cli/cli.js
CHANGED
|
@@ -93,21 +93,18 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
93
93
|
})
|
|
94
94
|
.option("region", {
|
|
95
95
|
demandOption: false,
|
|
96
|
-
default: 'US',
|
|
97
96
|
alias: 'r',
|
|
98
|
-
describe: 'either US or EU or IN',
|
|
97
|
+
describe: 'either US or EU or IN (defaults to dungeon config or US)',
|
|
99
98
|
type: 'string'
|
|
100
99
|
})
|
|
101
100
|
.option('concurrency', {
|
|
102
101
|
alias: 'conn',
|
|
103
|
-
default: 10,
|
|
104
102
|
demandOption: false,
|
|
105
|
-
describe: 'concurrency level for data generation',
|
|
103
|
+
describe: 'concurrency level for data generation (defaults to dungeon config)',
|
|
106
104
|
type: 'number'
|
|
107
105
|
})
|
|
108
106
|
.options("complex", {
|
|
109
107
|
demandOption: false,
|
|
110
|
-
default: false,
|
|
111
108
|
describe: 'use complex data model (model all entities)',
|
|
112
109
|
alias: 'c',
|
|
113
110
|
type: 'boolean',
|
|
@@ -115,7 +112,6 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
115
112
|
})
|
|
116
113
|
.options("simple", {
|
|
117
114
|
demandOption: false,
|
|
118
|
-
default: false,
|
|
119
115
|
describe: 'use simple data model (basic events and users)',
|
|
120
116
|
alias: 'simp',
|
|
121
117
|
type: 'boolean',
|
|
@@ -123,7 +119,6 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
123
119
|
})
|
|
124
120
|
.options("sanity", {
|
|
125
121
|
demandOption: false,
|
|
126
|
-
default: false,
|
|
127
122
|
describe: 'run sanity checks on the generated data',
|
|
128
123
|
alias: 'san',
|
|
129
124
|
type: 'boolean',
|
|
@@ -131,15 +126,13 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
131
126
|
})
|
|
132
127
|
.option("writeToDisk", {
|
|
133
128
|
demandOption: false,
|
|
134
|
-
|
|
135
|
-
describe: 'write data to disk',
|
|
129
|
+
describe: 'write data to disk (CLI defaults to true)',
|
|
136
130
|
alias: 'w',
|
|
137
131
|
type: 'boolean',
|
|
138
132
|
coerce: boolCoerce
|
|
139
133
|
})
|
|
140
134
|
.option("hasSessionIds", {
|
|
141
135
|
demandOption: false,
|
|
142
|
-
default: false,
|
|
143
136
|
describe: 'create session ids in the data',
|
|
144
137
|
alias: 'sid',
|
|
145
138
|
type: 'boolean',
|
|
@@ -147,7 +140,6 @@ DATA MODEL: https://github.com/ak--47/make-mp-data/blob/main/default.js
|
|
|
147
140
|
})
|
|
148
141
|
.option("hasAnonIds", {
|
|
149
142
|
demandOption: false,
|
|
150
|
-
default: false,
|
|
151
143
|
describe: 'create anonymous ids in the data',
|
|
152
144
|
alias: 'aid',
|
|
153
145
|
type: 'boolean',
|
|
@@ -341,18 +341,15 @@ function transformSCDPropsWithoutCredentials(config) {
|
|
|
341
341
|
|
|
342
342
|
// Missing credentials - handle based on job type
|
|
343
343
|
if (!isUIJob) {
|
|
344
|
-
// For programmatic/CLI usage
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
'
|
|
349
|
-
'
|
|
350
|
-
'
|
|
351
|
-
' - projectId: Your Mixpanel project ID\n' +
|
|
352
|
-
'Without these credentials, SCD data cannot be imported to Mixpanel.'
|
|
344
|
+
// For programmatic/CLI usage: warn but continue. SCD files still generate;
|
|
345
|
+
// mixpanel-sender already gates SCD import on credential presence.
|
|
346
|
+
if (token && config.verbose !== false) {
|
|
347
|
+
console.warn(
|
|
348
|
+
'⚠️ SCD properties configured but service credentials missing. ' +
|
|
349
|
+
'SCD files will be generated but NOT imported to Mixpanel. ' +
|
|
350
|
+
'Provide serviceAccount + serviceSecret + projectId to enable SCD import.'
|
|
353
351
|
);
|
|
354
352
|
}
|
|
355
|
-
// If not sending to Mixpanel (no token), allow generation for testing
|
|
356
353
|
return;
|
|
357
354
|
}
|
|
358
355
|
|
|
@@ -57,7 +57,8 @@ export async function sendToMixpanel(context) {
|
|
|
57
57
|
fixJson: false,
|
|
58
58
|
showProgress: !!config.verbose,
|
|
59
59
|
streamFormat: mpImportFormat,
|
|
60
|
-
workers: 35
|
|
60
|
+
workers: 35,
|
|
61
|
+
v2_compat: true,
|
|
61
62
|
};
|
|
62
63
|
|
|
63
64
|
log(`\n${'─'.repeat(50)}`);
|
|
@@ -65,8 +66,7 @@ export async function sendToMixpanel(context) {
|
|
|
65
66
|
log(`${'─'.repeat(50)}\n`);
|
|
66
67
|
|
|
67
68
|
// Import events
|
|
68
|
-
if (eventData?.length > 0 || isBATCH_MODE) {
|
|
69
|
-
log(` Events`);
|
|
69
|
+
if (eventData?.length > 0 || isBATCH_MODE || (writeToDisk && eventData)) {
|
|
70
70
|
let eventDataToImport = u.deepClone(eventData);
|
|
71
71
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && eventData && eventData.length === 0);
|
|
72
72
|
if (shouldReadFromFiles && eventData?.getWriteDir) {
|
|
@@ -75,17 +75,19 @@ export async function sendToMixpanel(context) {
|
|
|
75
75
|
// @ts-ignore
|
|
76
76
|
eventDataToImport = files.filter(f => f.includes('-EVENTS'));
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
if (eventDataToImport?.length > 0) {
|
|
79
|
+
log(` Events`);
|
|
80
|
+
const imported = await mp(creds, eventDataToImport, {
|
|
81
|
+
recordType: "event",
|
|
82
|
+
...commonOpts,
|
|
83
|
+
});
|
|
84
|
+
log(` -> ${comma(imported.success)} events sent\n`);
|
|
85
|
+
importResults.events = imported;
|
|
86
|
+
}
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
// Import user profiles
|
|
87
|
-
if (userProfilesData?.length > 0 || isBATCH_MODE) {
|
|
88
|
-
log(` User Profiles`);
|
|
90
|
+
if (userProfilesData?.length > 0 || isBATCH_MODE || (writeToDisk && userProfilesData)) {
|
|
89
91
|
let userProfilesToImport = u.deepClone(userProfilesData);
|
|
90
92
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && userProfilesData && userProfilesData.length === 0);
|
|
91
93
|
if (shouldReadFromFiles && userProfilesData?.getWriteDir) {
|
|
@@ -94,17 +96,19 @@ export async function sendToMixpanel(context) {
|
|
|
94
96
|
// @ts-ignore
|
|
95
97
|
userProfilesToImport = files.filter(f => f.includes('-USERS'));
|
|
96
98
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
if (userProfilesToImport?.length > 0) {
|
|
100
|
+
log(` User Profiles`);
|
|
101
|
+
const imported = await mp(creds, userProfilesToImport, {
|
|
102
|
+
recordType: "user",
|
|
103
|
+
...commonOpts,
|
|
104
|
+
});
|
|
105
|
+
log(` -> ${comma(imported.success)} user profiles sent\n`);
|
|
106
|
+
importResults.users = imported;
|
|
107
|
+
}
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
// Import ad spend data
|
|
106
|
-
if (adSpendData?.length > 0 || isBATCH_MODE) {
|
|
107
|
-
log(` Ad Spend`);
|
|
110
|
+
// Import ad spend data (only when feature enabled)
|
|
111
|
+
if (config.hasAdSpend && (adSpendData?.length > 0 || isBATCH_MODE || (writeToDisk && adSpendData))) {
|
|
108
112
|
let adSpendDataToImport = u.deepClone(adSpendData);
|
|
109
113
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && adSpendData && adSpendData.length === 0);
|
|
110
114
|
if (shouldReadFromFiles && adSpendData?.getWriteDir) {
|
|
@@ -113,20 +117,23 @@ export async function sendToMixpanel(context) {
|
|
|
113
117
|
// @ts-ignore
|
|
114
118
|
adSpendDataToImport = files.filter(f => f.includes('-ADSPEND'));
|
|
115
119
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
if (adSpendDataToImport?.length > 0) {
|
|
121
|
+
log(` Ad Spend`);
|
|
122
|
+
const imported = await mp(creds, adSpendDataToImport, {
|
|
123
|
+
recordType: "event",
|
|
124
|
+
...commonOpts,
|
|
125
|
+
});
|
|
126
|
+
log(` -> ${comma(imported.success)} ad spend events sent\n`);
|
|
127
|
+
importResults.adSpend = imported;
|
|
128
|
+
}
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
// Import group profiles
|
|
125
132
|
if (groupProfilesData && Array.isArray(groupProfilesData) && groupProfilesData.length > 0) {
|
|
126
133
|
for (const groupEntity of groupProfilesData) {
|
|
127
|
-
if (!groupEntity
|
|
134
|
+
if (!groupEntity) continue;
|
|
135
|
+
if (groupEntity.length === 0 && !isBATCH_MODE && !writeToDisk) continue;
|
|
128
136
|
const groupKey = groupEntity?.groupKey;
|
|
129
|
-
log(` Group Profiles (${groupKey})`);
|
|
130
137
|
let groupProfilesToImport = u.deepClone(groupEntity);
|
|
131
138
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEntity.length === 0);
|
|
132
139
|
if (shouldReadFromFiles && groupEntity?.getWriteDir) {
|
|
@@ -135,6 +142,8 @@ export async function sendToMixpanel(context) {
|
|
|
135
142
|
// @ts-ignore
|
|
136
143
|
groupProfilesToImport = files.filter(f => f.includes(`-${groupKey}-GROUPS`));
|
|
137
144
|
}
|
|
145
|
+
if (!groupProfilesToImport?.length) continue;
|
|
146
|
+
log(` Group Profiles (${groupKey})`);
|
|
138
147
|
const imported = await mp({ token, groupKey }, groupProfilesToImport, {
|
|
139
148
|
recordType: "group",
|
|
140
149
|
...commonOpts,
|
|
@@ -146,8 +155,7 @@ export async function sendToMixpanel(context) {
|
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
// Import group events
|
|
149
|
-
if (groupEventData?.length > 0) {
|
|
150
|
-
log(` Group Events`);
|
|
158
|
+
if (groupEventData?.length > 0 || isBATCH_MODE || (writeToDisk && groupEventData)) {
|
|
151
159
|
let groupEventDataToImport = u.deepClone(groupEventData);
|
|
152
160
|
const shouldReadFromFiles = isBATCH_MODE || (writeToDisk && groupEventData.length === 0);
|
|
153
161
|
if (shouldReadFromFiles && groupEventData?.getWriteDir) {
|
|
@@ -156,13 +164,17 @@ export async function sendToMixpanel(context) {
|
|
|
156
164
|
// @ts-ignore
|
|
157
165
|
groupEventDataToImport = files.filter(f => f.includes('-GROUP-EVENTS'));
|
|
158
166
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
// Skip if no data to import (avoids mp() throwing on empty input)
|
|
168
|
+
if (groupEventDataToImport?.length > 0) {
|
|
169
|
+
log(` Group Events`);
|
|
170
|
+
const imported = await mp(creds, groupEventDataToImport, {
|
|
171
|
+
recordType: "event",
|
|
172
|
+
...commonOpts,
|
|
173
|
+
strict: false
|
|
174
|
+
});
|
|
175
|
+
log(` -> ${comma(imported.success)} group events sent\n`);
|
|
176
|
+
importResults.groupEvents = imported;
|
|
177
|
+
}
|
|
166
178
|
}
|
|
167
179
|
|
|
168
180
|
// Import SCD data (requires service account)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "make-mp-data",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "builds all mixpanel primitives for a given project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"dotenv": "^16.4.5",
|
|
61
61
|
"hyparquet-writer": "^0.6.1",
|
|
62
62
|
"mixpanel": "^0.18.0",
|
|
63
|
-
"mixpanel-import": "^3.
|
|
63
|
+
"mixpanel-import": "^3.3.1",
|
|
64
64
|
"p-limit": "^3.1.0",
|
|
65
65
|
"pino": "^9.0.0",
|
|
66
66
|
"pino-pretty": "^11.0.0",
|