make-mp-data 2.0.1 → 2.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 (64) hide show
  1. package/dungeons/adspend.js +96 -0
  2. package/dungeons/anon.js +104 -0
  3. package/dungeons/big.js +225 -0
  4. package/dungeons/business.js +345 -0
  5. package/dungeons/complex.js +396 -0
  6. package/dungeons/experiments.js +125 -0
  7. package/dungeons/foobar.js +241 -0
  8. package/dungeons/funnels.js +272 -0
  9. package/dungeons/gaming.js +315 -0
  10. package/dungeons/media.js +7 -7
  11. package/dungeons/mirror.js +129 -0
  12. package/dungeons/sanity.js +113 -0
  13. package/dungeons/scd.js +205 -0
  14. package/dungeons/simple.js +195 -0
  15. package/dungeons/userAgent.js +190 -0
  16. package/entry.js +57 -0
  17. package/index.js +96 -68
  18. package/lib/cli/cli.js +5 -4
  19. package/lib/core/config-validator.js +28 -12
  20. package/lib/core/context.js +147 -130
  21. package/lib/core/storage.js +45 -31
  22. package/lib/data/defaults.js +2 -2
  23. package/lib/generators/adspend.js +1 -2
  24. package/lib/generators/events.js +35 -24
  25. package/lib/generators/funnels.js +1 -2
  26. package/lib/generators/mirror.js +1 -2
  27. package/lib/orchestrators/mixpanel-sender.js +15 -7
  28. package/lib/orchestrators/user-loop.js +21 -10
  29. package/lib/orchestrators/worker-manager.js +5 -2
  30. package/lib/utils/ai.js +36 -63
  31. package/lib/utils/chart.js +5 -0
  32. package/lib/utils/instructions.txt +593 -0
  33. package/lib/utils/utils.js +162 -38
  34. package/package.json +23 -9
  35. package/types.d.ts +376 -376
  36. package/.claude/settings.local.json +0 -21
  37. package/.gcloudignore +0 -18
  38. package/.gitattributes +0 -2
  39. package/.prettierrc +0 -0
  40. package/.vscode/launch.json +0 -80
  41. package/.vscode/settings.json +0 -69
  42. package/.vscode/tasks.json +0 -12
  43. package/dungeons/customers/.gitkeep +0 -0
  44. package/env.yaml +0 -1
  45. package/lib/cloud-function.js +0 -20
  46. package/scratch.mjs +0 -62
  47. package/scripts/dana.mjs +0 -137
  48. package/scripts/deploy.sh +0 -15
  49. package/scripts/jsdoctest.js +0 -5
  50. package/scripts/new-dungeon.sh +0 -98
  51. package/scripts/new-project.mjs +0 -14
  52. package/scripts/run-index.sh +0 -2
  53. package/scripts/update-deps.sh +0 -5
  54. package/tests/benchmark/concurrency.mjs +0 -52
  55. package/tests/cli.test.js +0 -124
  56. package/tests/coverage/.gitkeep +0 -0
  57. package/tests/e2e.test.js +0 -379
  58. package/tests/int.test.js +0 -715
  59. package/tests/testCases.mjs +0 -229
  60. package/tests/testSoup.mjs +0 -28
  61. package/tests/unit.test.js +0 -910
  62. package/tmp/.gitkeep +0 -0
  63. package/tsconfig.json +0 -18
  64. package/vitest.config.js +0 -47
@@ -3,11 +3,11 @@
3
3
  * Provides centralized state management and dependency injection
4
4
  */
5
5
 
6
- /** @typedef {import('../../types').Dungeon} Dungeon */
7
- /** @typedef {import('../../types').Storage} Storage */
8
- /** @typedef {import('../../types').Context} Context */
9
- /** @typedef {import('../../types').RuntimeState} RuntimeState */
10
- /** @typedef {import('../../types').Defaults} Defaults */
6
+ /** @typedef {import('../../types.js').Dungeon} Dungeon */
7
+ /** @typedef {import('../../types.js').Storage} Storage */
8
+ /** @typedef {import('../../types.js').Context} Context */
9
+ /** @typedef {import('../../types.js').RuntimeState} RuntimeState */
10
+ /** @typedef {import('../../types.js').Defaults} Defaults */
11
11
 
12
12
  import dayjs from "dayjs";
13
13
  import { campaigns, devices, locations } from '../data/defaults.js';
@@ -20,26 +20,35 @@ import * as u from '../utils/utils.js';
20
20
  * @returns {Defaults} Defaults object with factory functions
21
21
  */
22
22
  function createDefaults(config, campaignData) {
23
- const { singleCountry } = config;
24
-
25
- // Pre-compute weighted arrays based on configuration
26
- const locationsUsers = singleCountry ?
27
- locations.filter(l => l.country === singleCountry) :
28
- locations;
29
-
30
- const locationsEvents = singleCountry ?
31
- locations.filter(l => l.country === singleCountry) :
32
- locations;
33
-
34
- return {
35
- locationsUsers: () => u.weighArray(locationsUsers),
36
- locationsEvents: () => u.weighArray(locationsEvents),
37
- iOSDevices: () => u.weighArray(devices.iosDevices),
38
- androidDevices: () => u.weighArray(devices.androidDevices),
39
- desktopDevices: () => u.weighArray(devices.desktopDevices),
40
- browsers: () => u.weighArray(devices.browsers),
41
- campaigns: () => u.weighArray(campaignData)
42
- };
23
+ const { singleCountry } = config;
24
+
25
+ // Pre-compute weighted arrays based on configuration
26
+ const locationsUsers = singleCountry ?
27
+ locations.filter(l => l.country === singleCountry) :
28
+ locations;
29
+
30
+ const locationsEvents = singleCountry ?
31
+ locations.filter(l => l.country === singleCountry) :
32
+ locations;
33
+
34
+ // PERFORMANCE: Pre-calculate weighted arrays to avoid repeated weighArray calls
35
+ const weighedLocationsUsers = u.weighArray(locationsUsers);
36
+ const weighedLocationsEvents = u.weighArray(locationsEvents);
37
+ const weighedIOSDevices = u.weighArray(devices.iosDevices);
38
+ const weighedAndroidDevices = u.weighArray(devices.androidDevices);
39
+ const weighedDesktopDevices = u.weighArray(devices.desktopDevices);
40
+ const weighedBrowsers = u.weighArray(devices.browsers);
41
+ const weighedCampaigns = u.weighArray(campaignData);
42
+
43
+ return {
44
+ locationsUsers: () => weighedLocationsUsers,
45
+ locationsEvents: () => weighedLocationsEvents,
46
+ iOSDevices: () => weighedIOSDevices,
47
+ androidDevices: () => weighedAndroidDevices,
48
+ desktopDevices: () => weighedDesktopDevices,
49
+ browsers: () => weighedBrowsers,
50
+ campaigns: () => weighedCampaigns
51
+ };
43
52
  }
44
53
 
45
54
  /**
@@ -47,107 +56,115 @@ function createDefaults(config, campaignData) {
47
56
  * @returns {RuntimeState} Runtime state with counters and flags
48
57
  */
49
58
  function createRuntimeState() {
50
- return {
51
- operations: 0,
52
- eventCount: 0,
53
- userCount: 0,
54
- isBatchMode: false,
55
- verbose: false,
56
- isCLI: false
57
- };
59
+ return {
60
+ operations: 0,
61
+ eventCount: 0,
62
+ userCount: 0,
63
+ isBatchMode: false,
64
+ verbose: false,
65
+ isCLI: false
66
+ };
58
67
  }
59
68
 
60
69
  /**
61
70
  * Context factory that creates a complete context object for data generation
62
71
  * @param {Dungeon} config - Validated configuration object
63
72
  * @param {Storage|null} storage - Storage containers (optional, can be set later)
73
+ * @param {boolean} [isCliMode] - Whether running in CLI mode (optional, will detect if not provided)
64
74
  * @returns {Context} Context object containing all state and dependencies
65
75
  */
66
- export function createContext(config, storage = null) {
67
- // Import campaign data (could be made configurable)
68
- const campaignData = campaigns;
69
-
70
- // Create computed defaults based on config
71
- const defaults = createDefaults(config, campaignData);
72
-
73
- // Create runtime state
74
- const runtime = createRuntimeState();
75
-
76
- // Set runtime flags from config
77
- runtime.verbose = config.verbose || false;
78
- runtime.isBatchMode = config.batchSize && config.batchSize < config.numEvents;
79
- runtime.isCLI = process.argv[1].endsWith('index.js') || process.argv[1].endsWith('cli.js');
80
-
81
- const context = {
82
- config,
83
- storage,
84
- defaults,
85
- campaigns: campaignData,
86
- runtime,
87
-
88
- // Helper methods for updating state
89
- incrementOperations() {
90
- runtime.operations++;
91
- },
92
-
93
- incrementEvents() {
94
- runtime.eventCount++;
95
- },
96
-
97
- incrementUsers() {
98
- runtime.userCount++;
99
- },
100
-
101
- setStorage(storageObj) {
102
- this.storage = storageObj;
103
- },
104
-
105
- // Getter methods for runtime state
106
- getOperations() {
107
- return runtime.operations;
108
- },
109
-
110
- getEventCount() {
111
- return runtime.eventCount;
112
- },
113
-
114
- getUserCount() {
115
- return runtime.userCount;
116
- },
117
-
118
- incrementUserCount() {
119
- runtime.userCount++;
120
- },
121
-
122
- incrementEventCount() {
123
- runtime.eventCount++;
124
- },
125
-
126
- isBatchMode() {
127
- return runtime.isBatchMode;
128
- },
129
-
130
- isCLI() {
131
- return runtime.isCLI;
132
- },
133
-
134
- // Time helper methods
135
- getTimeShift() {
136
- const actualNow = dayjs().add(2, "day");
137
- return actualNow.diff(dayjs.unix(global.FIXED_NOW), "seconds");
138
- },
139
-
140
- getDaysShift() {
141
- const actualNow = dayjs().add(2, "day");
142
- return actualNow.diff(dayjs.unix(global.FIXED_NOW), "days");
143
- },
144
-
145
- // Time constants (previously globals)
146
- FIXED_NOW: global.FIXED_NOW,
147
- FIXED_BEGIN: global.FIXED_BEGIN
148
- };
149
-
150
- return context;
76
+ export function createContext(config, storage = null, isCliMode = null) {
77
+ // Import campaign data (could be made configurable)
78
+ const campaignData = campaigns;
79
+
80
+ // Create computed defaults based on config
81
+ const defaults = createDefaults(config, campaignData);
82
+
83
+ // Create runtime state
84
+ const runtime = createRuntimeState();
85
+
86
+ // Set runtime flags from config
87
+ runtime.verbose = config.verbose || false;
88
+ runtime.isBatchMode = config.batchSize && config.batchSize < config.numEvents;
89
+ runtime.isCLI = isCliMode !== null ? isCliMode : (process.argv[1]?.endsWith('index.js') || process.argv[1]?.endsWith('entry.js') || false);
90
+ if (runtime.isCLI) runtime.verbose = true; // Always verbose in CLI mode
91
+
92
+ const context = {
93
+ config,
94
+ storage,
95
+ defaults,
96
+ campaigns: campaignData,
97
+ runtime,
98
+
99
+ // Helper methods for updating state
100
+ incrementOperations() {
101
+ runtime.operations++;
102
+ },
103
+
104
+ incrementEvents() {
105
+ runtime.eventCount++;
106
+ },
107
+
108
+ incrementUsers() {
109
+ runtime.userCount++;
110
+ },
111
+
112
+ setStorage(storageObj) {
113
+ this.storage = storageObj;
114
+ },
115
+
116
+ // Getter methods for runtime state
117
+ getOperations() {
118
+ return runtime.operations;
119
+ },
120
+
121
+ getEventCount() {
122
+ return runtime.eventCount;
123
+ },
124
+
125
+ getUserCount() {
126
+ return runtime.userCount;
127
+ },
128
+
129
+ incrementUserCount() {
130
+ runtime.userCount++;
131
+ },
132
+
133
+ incrementEventCount() {
134
+ runtime.eventCount++;
135
+ },
136
+
137
+ isBatchMode() {
138
+ return runtime.isBatchMode;
139
+ },
140
+
141
+ isCLI() {
142
+ return runtime.isCLI;
143
+ },
144
+
145
+ // Time helper methods
146
+ getTimeShift() {
147
+ const actualNow = dayjs().add(2, "day");
148
+ return actualNow.diff(dayjs.unix(this.FIXED_NOW), "seconds");
149
+ },
150
+
151
+ getDaysShift() {
152
+ const actualNow = dayjs().add(2, "day");
153
+ return actualNow.diff(dayjs.unix(this.FIXED_NOW), "days");
154
+ },
155
+
156
+ // Time constants (previously globals)
157
+ FIXED_NOW: global.FIXED_NOW,
158
+ FIXED_BEGIN: global.FIXED_BEGIN,
159
+
160
+ // PERFORMANCE: Pre-calculated time shift (instead of calculating per-event)
161
+ TIME_SHIFT_SECONDS: (() => {
162
+ const actualNow = dayjs().add(2, "day");
163
+ return actualNow.diff(dayjs.unix(global.FIXED_NOW), "seconds");
164
+ })(),
165
+ };
166
+
167
+ return context;
151
168
  }
152
169
 
153
170
  /**
@@ -157,8 +174,8 @@ export function createContext(config, storage = null) {
157
174
  * @returns {Context} Updated context object
158
175
  */
159
176
  export function updateContextWithStorage(context, storage) {
160
- context.storage = storage;
161
- return context;
177
+ context.storage = storage;
178
+ return context;
162
179
  }
163
180
 
164
181
  /**
@@ -167,14 +184,14 @@ export function updateContextWithStorage(context, storage) {
167
184
  * @throws {Error} If context is missing required properties
168
185
  */
169
186
  export function validateContext(context) {
170
- const required = ['config', 'defaults', 'campaigns', 'runtime'];
171
- const missing = required.filter(prop => !context[prop]);
172
-
173
- if (missing.length > 0) {
174
- throw new Error(`Context is missing required properties: ${missing.join(', ')}`);
175
- }
176
-
177
- if (!context.config.numUsers || !context.config.numEvents) {
178
- throw new Error('Context config must have numUsers and numEvents');
179
- }
187
+ const required = ['config', 'defaults', 'campaigns', 'runtime'];
188
+ const missing = required.filter(prop => !context[prop]);
189
+
190
+ if (missing.length > 0) {
191
+ throw new Error(`Context is missing required properties: ${missing.join(', ')}`);
192
+ }
193
+
194
+ if (!context.config.numUsers || !context.config.numEvents) {
195
+ throw new Error('Context config must have numUsers and numEvents');
196
+ }
180
197
  }
@@ -3,7 +3,10 @@
3
3
  * Extracted from index.js to eliminate global dependencies
4
4
  */
5
5
 
6
- /** @typedef {import('../../types').Context} Context */
6
+ /** @typedef {import('../../types.js').Context} Context */
7
+ /** @typedef {import('../../types.js').HookedArray<any>} HookedArray */
8
+ /** @typedef {import('../../types.js').Storage} Storage */
9
+ /** @typedef {import('../../types.js').hookArrayOptions<any>} hookArrayOptions */
7
10
 
8
11
  import { existsSync } from "fs";
9
12
  import pLimit from 'p-limit';
@@ -14,25 +17,19 @@ import * as u from "../utils/utils.js";
14
17
  /**
15
18
  * Creates a hooked array that transforms data on push and handles batching/disk writes
16
19
  * @param {Array} arr - Base array to enhance
17
- * @param {Object} opts - Configuration options
18
- * @param {Function} opts.hook - Transform function applied to each item
19
- * @param {string} opts.type - Type identifier for the hook function
20
- * @param {string} opts.filepath - Base filename for disk writes
21
- * @param {string} opts.format - Output format ('csv' or 'json')
22
- * @param {number} opts.concurrency - Max concurrent file operations
23
- * @param {Context} opts.context - Context object with config, batchSize, etc.
24
- * @returns {Promise<Array>} Enhanced array with hookPush and flush methods
20
+ * @param {hookArrayOptions} opts - Configuration options
21
+ * @returns {Promise<HookedArray>} Enhanced array with hookPush and flush methods
25
22
  */
26
- export async function createHookArray(arr = [], opts = {}) {
23
+ export async function createHookArray(arr = [], opts) {
27
24
  const {
28
25
  hook = a => a,
29
26
  type = "",
30
27
  filepath = "./defaultFile",
31
28
  format = "csv",
32
29
  concurrency = 1,
33
- context = {},
30
+ context = /** @type {Context} */ ({}),
34
31
  ...rest
35
- } = opts;
32
+ } = opts || {};
36
33
 
37
34
  const FILE_CONN = pLimit(concurrency);
38
35
  const { config = {}, runtime = {} } = context;
@@ -75,28 +72,41 @@ export async function createHookArray(arr = [], opts = {}) {
75
72
  if (item === null || item === undefined) return false;
76
73
  if (typeof item === 'object' && Object.keys(item).length === 0) return false;
77
74
 
78
- const allMetaData = { ...rest, ...meta };
75
+ // Performance optimization: skip hook overhead for passthrough hooks
76
+ const isPassthroughHook = hook.toString().includes('return record') || hook.length === 1;
79
77
 
80
- if (Array.isArray(item)) {
81
- for (const i of item) {
78
+ if (isPassthroughHook) {
79
+ // Fast path for passthrough hooks - no transformation needed
80
+ if (Array.isArray(item)) {
81
+ arr.push(...item);
82
+ } else {
83
+ arr.push(item);
84
+ }
85
+ } else {
86
+ // Slow path for actual transformation hooks
87
+ const allMetaData = { ...rest, ...meta };
88
+
89
+ if (Array.isArray(item)) {
90
+ for (const i of item) {
91
+ try {
92
+ const enriched = await hook(i, type, allMetaData);
93
+ if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
94
+ else arr.push(enriched);
95
+ } catch (e) {
96
+ console.error(`\n\nyour hook had an error\n\n`, e);
97
+ arr.push(i);
98
+ }
99
+ }
100
+ } else {
82
101
  try {
83
- const enriched = await hook(i, type, allMetaData);
102
+ const enriched = await hook(item, type, allMetaData);
84
103
  if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
85
104
  else arr.push(enriched);
86
105
  } catch (e) {
87
106
  console.error(`\n\nyour hook had an error\n\n`, e);
88
- arr.push(i);
107
+ arr.push(item);
89
108
  }
90
109
  }
91
- } else {
92
- try {
93
- const enriched = await hook(item, type, allMetaData);
94
- if (Array.isArray(enriched)) enriched.forEach(e => arr.push(e));
95
- else arr.push(enriched);
96
- } catch (e) {
97
- console.error(`\n\nyour hook had an error\n\n`, e);
98
- arr.push(item);
99
- }
100
110
  }
101
111
 
102
112
  if (arr.length > BATCH_SIZE) {
@@ -105,6 +115,8 @@ export async function createHookArray(arr = [], opts = {}) {
105
115
  batch++;
106
116
  const writePath = getWritePath();
107
117
  const writeResult = await FILE_CONN(() => writeToDisk(arr, { writePath }));
118
+ // Ensure array is cleared after successful write
119
+ arr.length = 0;
108
120
  return writeResult;
109
121
  } else {
110
122
  return Promise.resolve(false);
@@ -116,7 +128,7 @@ export async function createHookArray(arr = [], opts = {}) {
116
128
  let writeResult;
117
129
 
118
130
  if (config.verbose) {
119
- console.log(`\n\n\twriting ${writePath}\n\n`);
131
+ console.log(`\n\twriting ${writePath}\n`);
120
132
  }
121
133
 
122
134
  switch (format) {
@@ -130,7 +142,7 @@ export async function createHookArray(arr = [], opts = {}) {
130
142
  throw new Error(`format ${format} is not supported`);
131
143
  }
132
144
 
133
- if (isBatchMode) data.length = 0;
145
+ // Array clearing now handled in transformThenPush to ensure proper timing
134
146
  return writeResult;
135
147
  }
136
148
 
@@ -144,7 +156,8 @@ export async function createHookArray(arr = [], opts = {}) {
144
156
  }
145
157
 
146
158
  // Enhance the array with our methods
147
- const enrichedArray = arr;
159
+ /** @type {HookedArray} */
160
+ const enrichedArray = /** @type {any} */ (arr);
148
161
  enrichedArray.hookPush = transformThenPush;
149
162
  enrichedArray.flush = flush;
150
163
  enrichedArray.getWriteDir = getWriteDir;
@@ -168,11 +181,12 @@ export class StorageManager {
168
181
 
169
182
  /**
170
183
  * Initialize all storage containers for the data generation process
171
- * @returns {import('../../types').Storage} Storage containers object
184
+ * @returns {Promise<Storage>} Storage containers object
172
185
  */
173
186
  async initializeContainers() {
174
187
  const { config } = this.context;
175
188
 
189
+ /** @type {Storage} */
176
190
  const storage = {
177
191
  eventData: await createHookArray([], {
178
192
  hook: config.hook,
@@ -194,7 +208,7 @@ export class StorageManager {
194
208
 
195
209
  adSpendData: await createHookArray([], {
196
210
  hook: config.hook,
197
- type: "adspend",
211
+ type: "ad-spend",
198
212
  filepath: `${config.simulationName || 'adspend'}-ADSPEND`,
199
213
  format: config.format || "csv",
200
214
  concurrency: config.concurrency || 1,
@@ -4,8 +4,8 @@
4
4
  * @fileoverview Contains default values for campaigns, devices, locations, and domains
5
5
  */
6
6
 
7
- /** @typedef {import('../../types.d.ts').main.Dungeon} Config */
8
- /** @typedef {import('../../types.d.ts').main.ValueValid} ValueValid */
7
+ /** @typedef {import('../../types.js').Dungeon} Config */
8
+ /** @typedef {import('../../types.js').ValueValid} ValueValid */
9
9
 
10
10
  //? https://docs.mixpanel.com/docs/data-structure/property-reference#default-properties
11
11
 
@@ -6,7 +6,6 @@
6
6
  /** @typedef {import('../../types').Context} Context */
7
7
 
8
8
  import dayjs from "dayjs";
9
- import { md5 } from "ak-tools";
10
9
  import * as u from "../utils/utils.js";
11
10
 
12
11
  /**
@@ -74,7 +73,7 @@ function createAdSpendEvent(network, campaign, day, chance) {
74
73
 
75
74
  // Create unique identifiers
76
75
  const id = network.utm_source[0] + '-' + campaign;
77
- const uid = md5(id);
76
+ const uid = u.quickHash(id);
78
77
 
79
78
  return {
80
79
  event: "$ad_spend",
@@ -10,7 +10,6 @@
10
10
  /** @typedef {import('../../types').Context} Context */
11
11
 
12
12
  import dayjs from "dayjs";
13
- import { md5 } from "ak-tools";
14
13
  import * as u from "../utils/utils.js";
15
14
 
16
15
  /**
@@ -77,7 +76,7 @@ export async function makeEvent(
77
76
 
78
77
  // Add default properties based on configuration
79
78
  if (hasLocation) {
80
- defaultProps.location = u.shuffleArray(defaults.locationsEvents()).pop();
79
+ defaultProps.location = u.pickRandom(defaults.locationsEvents());
81
80
  }
82
81
 
83
82
  if (hasBrowser) {
@@ -91,31 +90,34 @@ export async function makeEvent(
91
90
 
92
91
  // Add campaigns with attribution likelihood
93
92
  if (hasCampaigns && chance.bool({ likelihood: 25 })) {
94
- defaultProps.campaigns = u.shuffleArray(defaults.campaigns()).pop();
93
+ defaultProps.campaigns = u.pickRandom(defaults.campaigns());
95
94
  }
96
95
 
97
96
  // Select device from pool
98
97
  const devices = devicePool.flat();
99
98
  if (devices.length) {
100
- defaultProps.device = u.shuffleArray(devices).pop();
99
+ defaultProps.device = u.pickRandom(devices);
101
100
  }
102
101
 
103
102
  // Set event time using TimeSoup for realistic distribution
104
103
  if (earliestTime) {
105
104
  if (isFirstEvent) {
106
- eventTemplate.time = dayjs.unix(earliestTime).toISOString();
105
+ // Apply time shift to move to present day using precomputed value
106
+ eventTemplate.time = dayjs.unix(earliestTime).add(context.TIME_SHIFT_SECONDS, 'seconds').toISOString();
107
107
  } else {
108
- eventTemplate.time = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean);
108
+ // Get time from TimeSoup and apply precomputed time shift
109
+ const soupTime = u.TimeSoup(earliestTime, context.FIXED_NOW, peaks, deviation, mean);
110
+ eventTemplate.time = dayjs(soupTime).add(context.TIME_SHIFT_SECONDS, 'seconds').toISOString();
109
111
  }
110
112
  }
111
113
 
112
114
  // Add anonymous and session identifiers
113
115
  if (anonymousIds.length) {
114
- eventTemplate.device_id = chance.pickone(anonymousIds);
116
+ eventTemplate.device_id = u.pickRandom(anonymousIds);
115
117
  }
116
118
 
117
119
  if (sessionIds.length) {
118
- eventTemplate.session_id = chance.pickone(sessionIds);
120
+ eventTemplate.session_id = u.pickRandom(sessionIds);
119
121
  }
120
122
 
121
123
  // Sometimes add user_id (for attribution modeling)
@@ -128,16 +130,28 @@ export async function makeEvent(
128
130
  eventTemplate.user_id = distinct_id;
129
131
  }
130
132
 
131
- // Merge custom properties with super properties
132
- const props = { ...chosenEvent.properties, ...superProps };
133
-
133
+ // PERFORMANCE: Process properties directly without creating intermediate object
134
134
  // Add custom properties from event configuration
135
- for (const key in props) {
136
- try {
137
- eventTemplate[key] = u.choose(props[key]);
138
- } catch (e) {
139
- console.error(`error with ${key} in ${chosenEvent.event} event`, e);
140
- // Continue processing other properties
135
+ if (chosenEvent.properties) {
136
+ for (const key in chosenEvent.properties) {
137
+ try {
138
+ eventTemplate[key] = u.choose(chosenEvent.properties[key]);
139
+ } catch (e) {
140
+ console.error(`error with ${key} in ${chosenEvent.event} event`, e);
141
+ // Continue processing other properties
142
+ }
143
+ }
144
+ }
145
+
146
+ // Add super properties (override event properties if needed)
147
+ if (superProps) {
148
+ for (const key in superProps) {
149
+ try {
150
+ eventTemplate[key] = u.choose(superProps[key]);
151
+ } catch (e) {
152
+ console.error(`error with ${key} in super props`, e);
153
+ // Continue processing other properties
154
+ }
141
155
  }
142
156
  }
143
157
 
@@ -150,14 +164,11 @@ export async function makeEvent(
150
164
  addGroupProperties(eventTemplate, groupKeys);
151
165
 
152
166
  // Generate unique insert_id
153
- eventTemplate.insert_id = md5(JSON.stringify(eventTemplate));
167
+ const distinctId = eventTemplate.user_id || eventTemplate.device_id || eventTemplate.distinct_id || distinct_id;
168
+ const tuple = `${eventTemplate.event}-${eventTemplate.time}-${distinctId}`;
169
+ eventTemplate.insert_id = u.quickHash(tuple);
154
170
 
155
- // Apply time shift to move events to current timeline
156
- if (earliestTime) {
157
- const timeShift = dayjs().add(2, "day").diff(dayjs.unix(context.FIXED_NOW), "seconds");
158
- const timeShifted = dayjs(eventTemplate.time).add(timeShift, "seconds").toISOString();
159
- eventTemplate.time = timeShifted;
160
- }
171
+ // Note: Time shift already applied above during timestamp calculation
161
172
 
162
173
  return eventTemplate;
163
174
  }
@@ -6,7 +6,6 @@
6
6
  /** @typedef {import('../../types').Context} Context */
7
7
 
8
8
  import dayjs from "dayjs";
9
- import { clone } from "ak-tools";
10
9
  import * as u from "../utils/utils.js";
11
10
  import { makeEvent } from "./events.js";
12
11
 
@@ -129,7 +128,7 @@ function buildFunnelEvents(context, sequence, chosenFunnelProps) {
129
128
 
130
129
  return sequence.map((eventName) => {
131
130
  const foundEvent = config.events?.find((e) => e.event === eventName);
132
- const eventSpec = clone(foundEvent) || { event: eventName, properties: {} };
131
+ const eventSpec = u.deepClone(foundEvent) || { event: eventName, properties: {} };
133
132
 
134
133
  // Process event properties
135
134
  for (const key in eventSpec.properties) {
@@ -6,7 +6,6 @@
6
6
  /** @typedef {import('../../types').Context} Context */
7
7
 
8
8
  import dayjs from "dayjs";
9
- import { clone } from "ak-tools";
10
9
  import * as u from "../utils/utils.js";
11
10
 
12
11
  /**
@@ -44,7 +43,7 @@ export async function makeMirror(context) {
44
43
  if (shouldProcessEvent(oldEvent.event, events)) {
45
44
  // Clone event only when needed
46
45
  if (!newEvent) {
47
- newEvent = clone(oldEvent);
46
+ newEvent = u.deepClone(oldEvent);
48
47
  }
49
48
 
50
49
  // Apply the specified strategy