bulltrackers-module 1.0.554 → 1.0.555
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.
|
@@ -18,7 +18,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
18
18
|
|
|
19
19
|
// 2. Find all users subscribed to this PI and alert type
|
|
20
20
|
// Use computationName (e.g., 'RiskScoreIncrease') to map to alertConfig keys
|
|
21
|
-
const subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName);
|
|
21
|
+
const subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName, computationDate, dependencies);
|
|
22
22
|
|
|
23
23
|
if (subscriptions.length === 0) {
|
|
24
24
|
logger.log('INFO', `[processAlertForPI] No subscriptions found for PI ${piCid}, alert type ${alertType.id}`);
|
|
@@ -65,7 +65,6 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
// Use writeWithMigration to write to new path (with legacy fallback)
|
|
68
|
-
// notifications is a subcollection, so we need isCollection: true and documentId
|
|
69
68
|
const writePromise = writeWithMigration(
|
|
70
69
|
db,
|
|
71
70
|
'signedInUsers',
|
|
@@ -92,10 +91,18 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
92
91
|
// Wait for all notifications to be written
|
|
93
92
|
await Promise.all(notificationPromises);
|
|
94
93
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
// 5. Notify the PI themselves if they are a signed-in user (Optional feature)
|
|
95
|
+
await notifyPIIfSignedIn(db, logger, piCid, alertType, subscriptions.length);
|
|
96
|
+
|
|
97
|
+
// 6. Update global rootdata collection for computation system
|
|
98
|
+
// (Wrap in try-catch to prevent crashing the alert if metrics fail)
|
|
99
|
+
try {
|
|
100
|
+
const { updateAlertHistoryRootData } = require('../../generic-api/user-api/helpers/rootdata/rootdata_aggregation_helpers');
|
|
101
|
+
const triggeredUserCids = subscriptions.map(s => s.userCid);
|
|
102
|
+
await updateAlertHistoryRootData(db, logger, piCid, alertType, computationMetadata, computationDate, triggeredUserCids);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
logger.log('WARN', `[processAlertForPI] Failed to update history rootdata: ${e.message}`);
|
|
105
|
+
}
|
|
99
106
|
|
|
100
107
|
logger.log('SUCCESS', `[processAlertForPI] Created ${notificationPromises.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
|
|
101
108
|
|
|
@@ -107,20 +114,19 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
|
|
|
107
114
|
|
|
108
115
|
/**
|
|
109
116
|
* Find all users who should receive alerts for a PI and alert type
|
|
110
|
-
*
|
|
117
|
+
* Uses WatchlistMembershipData/{date} to find users, then reads watchlists from SignedInUsers/{cid}/watchlists
|
|
111
118
|
*/
|
|
112
|
-
async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
|
|
119
|
+
async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computationDate, dependencies = {}) {
|
|
113
120
|
const subscriptions = [];
|
|
114
121
|
|
|
115
122
|
// Map computation names to watchlist alertConfig keys
|
|
116
|
-
// The alertTypeId is actually the computation name (e.g., 'RiskScoreIncrease')
|
|
117
123
|
const computationToConfigKey = {
|
|
118
124
|
'RiskScoreIncrease': 'increasedRisk',
|
|
119
125
|
'SignificantVolatility': 'volatilityChanges',
|
|
120
126
|
'NewSectorExposure': 'newSector',
|
|
121
127
|
'PositionInvestedIncrease': 'increasedPositionSize',
|
|
122
128
|
'NewSocialPost': 'newSocialPost',
|
|
123
|
-
'TestSystemProbe': 'increasedRisk' // Hack: Map to 'increasedRisk'
|
|
129
|
+
'TestSystemProbe': 'increasedRisk' // Hack: Map to 'increasedRisk' key
|
|
124
130
|
};
|
|
125
131
|
|
|
126
132
|
const configKey = computationToConfigKey[alertTypeId];
|
|
@@ -129,7 +135,121 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
|
|
|
129
135
|
return subscriptions;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
//
|
|
138
|
+
// Step 1: Load WatchlistMembershipData/{date} to find which users have this PI in their watchlist
|
|
139
|
+
const piCidStr = String(piCid);
|
|
140
|
+
let userCids = [];
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const membershipRef = db.collection('WatchlistMembershipData').doc(computationDate);
|
|
144
|
+
const membershipDoc = await membershipRef.get();
|
|
145
|
+
|
|
146
|
+
if (membershipDoc.exists) {
|
|
147
|
+
const membershipData = membershipDoc.data();
|
|
148
|
+
const piMembership = membershipData[piCidStr];
|
|
149
|
+
|
|
150
|
+
if (piMembership && piMembership.users && Array.isArray(piMembership.users)) {
|
|
151
|
+
userCids = piMembership.users.map(cid => String(cid));
|
|
152
|
+
logger.log('INFO', `[findSubscriptionsForPI] Found ${userCids.length} users with PI ${piCid} in watchlist from WatchlistMembershipData/${computationDate}`);
|
|
153
|
+
} else {
|
|
154
|
+
logger.log('INFO', `[findSubscriptionsForPI] No users found for PI ${piCid} in WatchlistMembershipData/${computationDate}`);
|
|
155
|
+
return subscriptions;
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
logger.log('WARN', `[findSubscriptionsForPI] WatchlistMembershipData/${computationDate} not found, falling back to scanning all watchlists`);
|
|
159
|
+
// Fallback to legacy method if membership data doesn't exist
|
|
160
|
+
return await findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey);
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
logger.log('ERROR', `[findSubscriptionsForPI] Error loading WatchlistMembershipData: ${error.message}`);
|
|
164
|
+
// Fallback to legacy method on error
|
|
165
|
+
return await findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
|
|
169
|
+
const { readWithMigration } = require('../../generic-api/user-api/helpers/core/path_resolution_helpers');
|
|
170
|
+
const { collectionRegistry } = dependencies;
|
|
171
|
+
const config = dependencies.config || {};
|
|
172
|
+
|
|
173
|
+
for (const userCidStr of userCids) {
|
|
174
|
+
try {
|
|
175
|
+
const userCid = Number(userCidStr);
|
|
176
|
+
|
|
177
|
+
// Read all watchlists for this user from new path
|
|
178
|
+
const watchlistsResult = await readWithMigration(
|
|
179
|
+
db,
|
|
180
|
+
'signedInUsers',
|
|
181
|
+
'watchlists',
|
|
182
|
+
{ cid: userCid },
|
|
183
|
+
{
|
|
184
|
+
isCollection: true,
|
|
185
|
+
dataType: 'watchlists',
|
|
186
|
+
config,
|
|
187
|
+
logger,
|
|
188
|
+
collectionRegistry: collectionRegistry
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (!watchlistsResult) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Get watchlists from snapshot or data
|
|
197
|
+
let watchlists = [];
|
|
198
|
+
if (watchlistsResult.snapshot && !watchlistsResult.snapshot.empty) {
|
|
199
|
+
watchlists = watchlistsResult.snapshot.docs.map(doc => ({
|
|
200
|
+
id: doc.id,
|
|
201
|
+
...doc.data()
|
|
202
|
+
}));
|
|
203
|
+
} else if (watchlistsResult.data) {
|
|
204
|
+
// If it's a single document, wrap it
|
|
205
|
+
watchlists = [watchlistsResult.data];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Step 3: Check each watchlist for the PI and alert config
|
|
209
|
+
for (const watchlistData of watchlists) {
|
|
210
|
+
if (watchlistData.type === 'static' && watchlistData.items && Array.isArray(watchlistData.items)) {
|
|
211
|
+
for (const item of watchlistData.items) {
|
|
212
|
+
if (Number(item.cid) === Number(piCid)) {
|
|
213
|
+
// Check if this alert type is enabled
|
|
214
|
+
const isTestProbe = alertTypeId === 'TestSystemProbe';
|
|
215
|
+
const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
|
|
216
|
+
|
|
217
|
+
// If it's the probe OR the user explicitly enabled it
|
|
218
|
+
if (isTestProbe || isEnabled) {
|
|
219
|
+
subscriptions.push({
|
|
220
|
+
userCid: userCid,
|
|
221
|
+
piCid: piCid,
|
|
222
|
+
piUsername: item.username || `PI-${piCid}`,
|
|
223
|
+
watchlistId: watchlistData.id || watchlistData.watchlistId,
|
|
224
|
+
watchlistName: watchlistData.name || 'Unnamed Watchlist',
|
|
225
|
+
alertConfig: item.alertConfig
|
|
226
|
+
});
|
|
227
|
+
break; // Found in this watchlist, no need to check other items
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logger.log('WARN', `[findSubscriptionsForPI] Error reading watchlists for user ${userCidStr}: ${error.message}`);
|
|
235
|
+
// Continue with next user
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
logger.log('INFO', `[findSubscriptionsForPI] Found ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
|
|
241
|
+
return subscriptions;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Legacy fallback method - scans all watchlists (used if WatchlistMembershipData is not available)
|
|
246
|
+
*/
|
|
247
|
+
async function findSubscriptionsForPILegacy(db, logger, piCid, alertTypeId, configKey) {
|
|
248
|
+
const subscriptions = [];
|
|
249
|
+
|
|
250
|
+
logger.log('INFO', `[findSubscriptionsForPILegacy] Using legacy method to find subscriptions for PI ${piCid}`);
|
|
251
|
+
|
|
252
|
+
// Get all watchlists from legacy path
|
|
133
253
|
const watchlistsCollection = db.collection('watchlists');
|
|
134
254
|
const watchlistsSnapshot = await watchlistsCollection.get();
|
|
135
255
|
|
|
@@ -144,8 +264,10 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
|
|
|
144
264
|
if (listData.type === 'static' && listData.items && Array.isArray(listData.items)) {
|
|
145
265
|
for (const item of listData.items) {
|
|
146
266
|
if (Number(item.cid) === Number(piCid)) {
|
|
147
|
-
|
|
148
|
-
|
|
267
|
+
const isTestProbe = alertTypeId === 'TestSystemProbe';
|
|
268
|
+
const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
|
|
269
|
+
|
|
270
|
+
if (isTestProbe || isEnabled) {
|
|
149
271
|
subscriptions.push({
|
|
150
272
|
userCid: userCid,
|
|
151
273
|
piCid: piCid,
|
|
@@ -154,19 +276,14 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId) {
|
|
|
154
276
|
watchlistName: listData.name || 'Unnamed Watchlist',
|
|
155
277
|
alertConfig: item.alertConfig
|
|
156
278
|
});
|
|
157
|
-
break;
|
|
279
|
+
break;
|
|
158
280
|
}
|
|
159
281
|
}
|
|
160
282
|
}
|
|
161
283
|
}
|
|
162
|
-
|
|
163
|
-
// Check dynamic watchlists - if the PI matches the computation result, check alertConfig
|
|
164
|
-
// Note: Dynamic watchlists are handled separately as they're based on computation results
|
|
165
|
-
// For now, we only process static watchlists here
|
|
166
284
|
}
|
|
167
285
|
}
|
|
168
286
|
|
|
169
|
-
logger.log('INFO', `[findSubscriptionsForPI] Found ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
|
|
170
287
|
return subscriptions;
|
|
171
288
|
}
|
|
172
289
|
|
|
@@ -178,10 +295,6 @@ function shouldTriggerAlert(subscription, alertTypeId) {
|
|
|
178
295
|
if (!subscription.thresholds || Object.keys(subscription.thresholds).length === 0) {
|
|
179
296
|
return true;
|
|
180
297
|
}
|
|
181
|
-
|
|
182
|
-
// Threshold checking logic would go here
|
|
183
|
-
// For now, we'll implement basic threshold checks in the trigger function
|
|
184
|
-
// This is a placeholder for future enhancement
|
|
185
298
|
return true;
|
|
186
299
|
}
|
|
187
300
|
|
|
@@ -195,9 +308,7 @@ async function getPIUsername(db, piCid) {
|
|
|
195
308
|
const { getPIUsernameFromMasterList } = require('../../generic-api/user-api/helpers/core/user_status_helpers');
|
|
196
309
|
const username = await getPIUsernameFromMasterList(db, piCid, null, null);
|
|
197
310
|
|
|
198
|
-
if (username)
|
|
199
|
-
return username;
|
|
200
|
-
}
|
|
311
|
+
if (username) return username;
|
|
201
312
|
|
|
202
313
|
// Fallback: try to get from any subscription
|
|
203
314
|
const subscriptionsSnapshot = await db.collection('watchlist_subscriptions')
|
|
@@ -221,18 +332,14 @@ async function getPIUsername(db, piCid) {
|
|
|
221
332
|
*/
|
|
222
333
|
async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
|
|
223
334
|
try {
|
|
224
|
-
|
|
225
|
-
const userRef = db.collection('signed_in_users').doc(String(piCid));
|
|
335
|
+
const userRef = db.collection('signedInUsers').doc(String(piCid));
|
|
226
336
|
const userDoc = await userRef.get();
|
|
227
337
|
|
|
228
|
-
if (!userDoc.exists)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// Create a notification for the PI
|
|
233
|
-
const notificationRef = db.collection('user_alerts')
|
|
338
|
+
if (!userDoc.exists) return;
|
|
339
|
+
|
|
340
|
+
const notificationRef = db.collection('signedInUsers')
|
|
234
341
|
.doc(String(piCid))
|
|
235
|
-
.collection('
|
|
342
|
+
.collection('user_alerts_metrics') // Changed from user_alerts/triggered to avoid collision
|
|
236
343
|
.doc(`trigger_${Date.now()}_${alertType.id}`);
|
|
237
344
|
|
|
238
345
|
await notificationRef.set({
|
|
@@ -246,8 +353,8 @@ async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
|
|
|
246
353
|
|
|
247
354
|
logger.log('INFO', `[notifyPIIfSignedIn] Notified PI ${piCid} that ${alertCount} users received ${alertType.id} alert`);
|
|
248
355
|
} catch (error) {
|
|
249
|
-
|
|
250
|
-
|
|
356
|
+
// Silent fail - non-critical
|
|
357
|
+
logger.log('DEBUG', `[notifyPIIfSignedIn] Skipped for PI ${piCid}`);
|
|
251
358
|
}
|
|
252
359
|
}
|
|
253
360
|
|
|
@@ -290,25 +397,17 @@ function tryDecompress(data) {
|
|
|
290
397
|
|
|
291
398
|
/**
|
|
292
399
|
* Read and decompress computation results
|
|
293
|
-
* Handles two result formats:
|
|
294
|
-
* 1. { cids: [...], metadata: {...} } - Global metadata
|
|
295
|
-
* 2. { cids: [...], userId1: {...}, userId2: {...} } - Per-user metadata
|
|
296
400
|
*/
|
|
297
401
|
function readComputationResults(docData) {
|
|
298
402
|
try {
|
|
299
|
-
// Try to decompress if needed
|
|
300
403
|
const decompressed = tryDecompress(docData);
|
|
301
404
|
|
|
302
|
-
// If decompressed is an object with cids, return it
|
|
303
405
|
if (decompressed && typeof decompressed === 'object') {
|
|
304
|
-
// Handle structure: { cids: [...], metadata: {...} }
|
|
305
406
|
if (decompressed.cids && Array.isArray(decompressed.cids)) {
|
|
306
|
-
// Check if there's per-user data (keys that are numbers)
|
|
307
407
|
const userDataKeys = Object.keys(decompressed)
|
|
308
408
|
.filter(key => key !== 'cids' && key !== 'metadata' && /^\d+$/.test(key));
|
|
309
409
|
|
|
310
410
|
if (userDataKeys.length > 0) {
|
|
311
|
-
// Format 2: Per-user metadata stored as { cids: [...], userId: {...} }
|
|
312
411
|
return {
|
|
313
412
|
cids: decompressed.cids,
|
|
314
413
|
perUserData: userDataKeys.reduce((acc, key) => {
|
|
@@ -318,7 +417,6 @@ function readComputationResults(docData) {
|
|
|
318
417
|
globalMetadata: decompressed.metadata || {}
|
|
319
418
|
};
|
|
320
419
|
} else {
|
|
321
|
-
// Format 1: Global metadata
|
|
322
420
|
return {
|
|
323
421
|
cids: decompressed.cids,
|
|
324
422
|
metadata: decompressed.metadata || {}
|
|
@@ -326,7 +424,6 @@ function readComputationResults(docData) {
|
|
|
326
424
|
}
|
|
327
425
|
}
|
|
328
426
|
|
|
329
|
-
// If it's a flat object with CID keys but no cids array, extract them
|
|
330
427
|
const cids = Object.keys(decompressed)
|
|
331
428
|
.filter(key => /^\d+$/.test(key))
|
|
332
429
|
.map(key => Number(key));
|
|
@@ -344,7 +441,6 @@ function readComputationResults(docData) {
|
|
|
344
441
|
};
|
|
345
442
|
}
|
|
346
443
|
}
|
|
347
|
-
|
|
348
444
|
return { cids: [], metadata: {}, perUserData: {} };
|
|
349
445
|
} catch (error) {
|
|
350
446
|
console.error('[readComputationResults] Error reading results', error);
|
|
@@ -357,9 +453,7 @@ function readComputationResults(docData) {
|
|
|
357
453
|
*/
|
|
358
454
|
async function readComputationResultsWithShards(db, docData, docRef) {
|
|
359
455
|
try {
|
|
360
|
-
// Check if data is sharded
|
|
361
456
|
if (docData._sharded === true && docData._shardCount) {
|
|
362
|
-
// Data is stored in shards - read all shards and merge
|
|
363
457
|
const shardsCol = docRef.collection('_shards');
|
|
364
458
|
const shardsSnapshot = await shardsCol.get();
|
|
365
459
|
|
|
@@ -373,8 +467,6 @@ async function readComputationResultsWithShards(db, docData, docRef) {
|
|
|
373
467
|
return readComputationResults(mergedData);
|
|
374
468
|
}
|
|
375
469
|
}
|
|
376
|
-
|
|
377
|
-
// Data is in the main document (compressed or not)
|
|
378
470
|
return readComputationResults(docData);
|
|
379
471
|
} catch (error) {
|
|
380
472
|
console.error('[readComputationResultsWithShards] Error reading sharded results', error);
|
|
@@ -387,6 +479,6 @@ module.exports = {
|
|
|
387
479
|
findSubscriptionsForPI,
|
|
388
480
|
getPIUsername,
|
|
389
481
|
readComputationResults,
|
|
390
|
-
readComputationResultsWithShards
|
|
391
|
-
|
|
392
|
-
|
|
482
|
+
readComputationResultsWithShards,
|
|
483
|
+
notifyPIIfSignedIn // Exporting this as well since it's defined
|
|
484
|
+
};
|