bulltrackers-module 1.0.56 → 1.0.58

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.
@@ -0,0 +1,207 @@
1
+ /**
2
+ * @fileoverview Main pipe: pipe.maintenance.runUserActivitySampler
3
+ * Samples user activity levels based on the Rankings API.
4
+ */
5
+ const { FieldValue } = require('@google-cloud/firestore');
6
+
7
+ /**
8
+ * Main pipe: pipe.maintenance.runUserActivitySampler
9
+ * @param {object} config - Configuration object.
10
+ * @param {object} dependencies - Contains db, logger, headerManager, proxyManager, firestoreUtils.
11
+ * @returns {Promise<object>} Summary of the sampling process.
12
+ */
13
+ exports.runUserActivitySampler = async (config, dependencies) => {
14
+ const { db, logger, headerManager, proxyManager, firestoreUtils } = dependencies;
15
+ const today = new Date().toISOString().slice(0, 10);
16
+ logger.log('INFO', '[UserActivitySampler] Starting user activity sampling...');
17
+
18
+ // Validate configuration
19
+ if (!config.rankingsApiUrl || !config.targetPublicUsersPerBlock || !config.apiBatchSize || !config.outputCollectionName || !config.allHighValueBlocks || !config.exclusionConfig) {
20
+ logger.log('ERROR', '[UserActivitySampler] Missing required configuration.');
21
+ throw new Error('Missing required configuration for User Activity Sampler.');
22
+ }
23
+
24
+ try {
25
+ await firestoreUtils.resetProxyLocks(dependencies, config); // Use shared config for proxy path
26
+
27
+ const exclusionIds = await firestoreUtils.getExclusionIds(dependencies, config.exclusionConfig, 'sampler'); // Pass 'sampler' or similar unique type
28
+ logger.log('INFO', `[UserActivitySampler] Fetched ${exclusionIds.size} exclusion IDs.`);
29
+
30
+ const blockResults = [];
31
+ let totalPublicSampled = 0;
32
+ let totalPrivateEstimate = 0;
33
+
34
+ for (const block of config.allHighValueBlocks) {
35
+ const blockId = block.startId;
36
+ logger.log('INFO', `[UserActivitySampler] Processing block ${blockId}...`);
37
+
38
+ let N_public_sampled_block = 0;
39
+ let N_sampled_total_block = 0;
40
+ const public_users_data = []; // Store { CID, LastActivity }
41
+
42
+ const MAX_ATTEMPTS = config.maxSamplingAttemptsPerBlock || 1000; // Prevent infinite loops
43
+ let attempts = 0;
44
+
45
+ while (N_public_sampled_block < config.targetPublicUsersPerBlock && attempts < MAX_ATTEMPTS) {
46
+ attempts++;
47
+ const cidsToSample = [];
48
+ // Generate CIDs, ensuring they are not excluded
49
+ while (cidsToSample.length < config.apiBatchSize) {
50
+ const randomId = String(Math.floor(Math.random() * 1000000) + blockId);
51
+ if (!exclusionIds.has(randomId)) {
52
+ cidsToSample.push(parseInt(randomId, 10));
53
+ // Add temporarily to exclusion set for this run to avoid duplicates within batches
54
+ exclusionIds.add(randomId);
55
+ }
56
+ }
57
+ N_sampled_total_block += cidsToSample.length;
58
+
59
+ let selectedHeader = null;
60
+ let wasSuccess = false;
61
+ try {
62
+ selectedHeader = await headerManager.selectHeader();
63
+ if (!selectedHeader) throw new Error("Could not select header.");
64
+
65
+ const response = await proxyManager.fetch(config.rankingsApiUrl, {
66
+ method: 'POST',
67
+ headers: { ...selectedHeader.header, 'Content-Type': 'application/json' },
68
+ body: JSON.stringify(cidsToSample),
69
+ });
70
+
71
+ if (!response || typeof response.json !== 'function') {
72
+ logger.log('WARN', `[UserActivitySampler] Invalid response structure from proxy for block ${blockId}, batch attempt ${attempts}. Skipping batch.`);
73
+ // Consider retrying or specific error handling
74
+ continue; // Skip to next attempt
75
+ }
76
+
77
+
78
+ if (!response.ok) {
79
+ // Log API errors but continue sampling other batches/blocks
80
+ const errorText = await response.text();
81
+ logger.log('WARN', `[UserActivitySampler] API error ${response.status} for block ${blockId}, batch attempt ${attempts}. Skipping batch. Error: ${errorText}`);
82
+ wasSuccess = false;
83
+ continue; // Skip to next attempt
84
+ }
85
+
86
+ wasSuccess = true;
87
+ const publicUsersBatch = await response.json();
88
+
89
+ if (Array.isArray(publicUsersBatch)) {
90
+ const N_public_returned_batch = publicUsersBatch.length;
91
+ N_public_sampled_block += N_public_returned_batch;
92
+ public_users_data.push(...publicUsersBatch.map(u => ({
93
+ CID: u.CID,
94
+ LastActivity: u.Value?.LastActivity // Handle potential missing Value
95
+ })));
96
+ } else {
97
+ logger.log('WARN', `[UserActivitySampler] API response was not an array for block ${blockId}, batch attempt ${attempts}. Skipping batch.`);
98
+ continue;
99
+ }
100
+
101
+
102
+ } catch (fetchError) {
103
+ logger.log('ERROR', `[UserActivitySampler] Fetch failed for block ${blockId}, batch attempt ${attempts}. Skipping batch.`, { errorMessage: fetchError.message });
104
+ wasSuccess = false; // Mark as failure for header performance
105
+ // Continue to next attempt
106
+ } finally {
107
+ if (selectedHeader) {
108
+ headerManager.updatePerformance(selectedHeader.id, wasSuccess);
109
+ }
110
+ }
111
+ // Small delay between batches to avoid overwhelming API/proxies
112
+ await new Promise(resolve => setTimeout(resolve, config.delayBetweenBatchesMs || 200));
113
+
114
+ } // End while loop for block sampling
115
+
116
+ if (attempts >= MAX_ATTEMPTS) {
117
+ logger.log('WARN', `[UserActivitySampler] Reached max sampling attempts (${MAX_ATTEMPTS}) for block ${blockId}. Proceeding with ${N_public_sampled_block} users.`);
118
+ }
119
+
120
+
121
+ const f_private_block = N_sampled_total_block > 0 ? 1 - (N_public_sampled_block / N_sampled_total_block) : 0;
122
+
123
+ // Categorize users
124
+ const counts = { A1: 0, A2: 0, A3: 0, A4: 0 };
125
+ const now = new Date();
126
+ const oneDayAgo = new Date(now.getTime() - (24 * 60 * 60 * 1000));
127
+ const oneWeekAgo = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));
128
+ const oneMonthAgo = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); // Approx
129
+ const threeMonthsAgo = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); // Approx
130
+
131
+
132
+ public_users_data.forEach(user => {
133
+ if (!user.LastActivity) {
134
+ counts.A4++; // Assume inactive if LastActivity is missing
135
+ return;
136
+ }
137
+ try {
138
+ const lastActivityDate = new Date(user.LastActivity);
139
+ if (isNaN(lastActivityDate)) {
140
+ counts.A4++; // Treat invalid dates as inactive
141
+ return;
142
+ }
143
+
144
+ if (lastActivityDate >= oneDayAgo) counts.A1++;
145
+ else if (lastActivityDate >= oneWeekAgo) counts.A2++;
146
+ else if (lastActivityDate >= threeMonthsAgo) counts.A3++; // Changed from oneMonthAgo based on description
147
+ else counts.A4++;
148
+ } catch(e) {
149
+ logger.log('WARN', `[UserActivitySampler] Error parsing LastActivity date '${user.LastActivity}' for user ${user.CID}. Counting as A4.`);
150
+ counts.A4++;
151
+ }
152
+
153
+ });
154
+
155
+ const fractions = {};
156
+ for (const category in counts) {
157
+ fractions[category] = N_public_sampled_block > 0 ? (counts[category] / N_public_sampled_block) : 0;
158
+ }
159
+
160
+ // TODO: Fetch or define N_block (total users for this block)
161
+ const N_block = 1000000;
162
+
163
+ const estimatedCounts = {};
164
+ for (const category in fractions) {
165
+ estimatedCounts[category] = Math.round(fractions[category] * N_block);
166
+ }
167
+
168
+ const result = {
169
+ blockId: blockId,
170
+ sampledDate: today,
171
+ f_private: f_private_block,
172
+ publicSampleSize: N_public_sampled_block,
173
+ totalUsersInBlock: N_block, // Add this if available
174
+ activityCounts_Sample: counts,
175
+ activityFractions_Sample: fractions,
176
+ estimatedCounts_TotalBlock: estimatedCounts,
177
+ lastUpdated: FieldValue.serverTimestamp()
178
+ };
179
+
180
+ blockResults.push(result);
181
+ totalPublicSampled += N_public_sampled_block;
182
+ totalPrivateEstimate += f_private_block * N_block;
183
+
184
+ // Store result for this block
185
+ const docRef = db.collection(config.outputCollectionName).doc(`${blockId}_${today}`);
186
+ await docRef.set(result);
187
+ logger.log('INFO', `[UserActivitySampler] Stored results for block ${blockId}. Sampled: ${N_public_sampled_block}. Private fraction: ${f_private_block.toFixed(3)}.`);
188
+
189
+ } // End for loop through blocks
190
+
191
+ await headerManager.flushPerformanceUpdates();
192
+ logger.log('SUCCESS', `[UserActivitySampler] Sampling complete. Processed ${blockResults.length} blocks. Total public sampled: ${totalPublicSampled}.`);
193
+
194
+ return {
195
+ success: true,
196
+ processedBlocks: blockResults.length,
197
+ totalPublicSampled: totalPublicSampled,
198
+ estimatedTotalPrivate: Math.round(totalPrivateEstimate)
199
+ };
200
+
201
+ } catch (error) {
202
+ logger.log('ERROR', '[UserActivitySampler] Fatal error during sampling process.', { errorMessage: error.message, errorStack: error.stack });
203
+ // Attempt to flush performance even on error
204
+ try { await headerManager.flushPerformanceUpdates(); } catch (e) { /* ignore flush error */ }
205
+ throw error; // Re-throw to indicate failure
206
+ }
207
+ };
package/index.js CHANGED
@@ -81,6 +81,7 @@ const maintenance = {
81
81
  handleInvalidSpeculator: require('./functions/invalid-speculator-handler/helpers/handler_helpers').handleInvalidSpeculator,
82
82
  runFetchInsights: require('./functions/fetch-insights/helpers/handler_helpers').fetchAndStoreInsights,
83
83
  runFetchPrices: require('./functions/etoro-price-fetcher/helpers/handler_helpers').fetchAndStorePrices,
84
+ runUserActivitySampler: require('./functions/user-activity-sampler/helpers/sampler_helpers').runUserActivitySampler,
84
85
  };
85
86
 
86
87
  // --- Pipe 7: Proxy ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -15,7 +15,8 @@
15
15
  "functions/speculator-cleanup-orchestrator/",
16
16
  "functions/fetch-insights/",
17
17
  "functions/etoro-price-fetcher/",
18
- "functions/appscript-api/"
18
+ "functions/appscript-api/",
19
+ "functions/user-activity-sampler/"
19
20
  ],
20
21
  "keywords": [
21
22
  "bulltrackers",