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.
|
|
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",
|