bulltrackers-module 1.0.777 → 1.0.779

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 (24) hide show
  1. package/functions/alert-system/helpers/alert_helpers.js +114 -90
  2. package/functions/alert-system/helpers/alert_manifest_loader.js +88 -99
  3. package/functions/alert-system/index.js +81 -138
  4. package/functions/alert-system/tests/stage1-alert-manifest.test.js +94 -0
  5. package/functions/alert-system/tests/stage2-alert-metadata.test.js +93 -0
  6. package/functions/alert-system/tests/stage3-alert-handler.test.js +79 -0
  7. package/functions/api-v2/helpers/data-fetchers/firestore.js +613 -478
  8. package/functions/api-v2/routes/popular_investors.js +7 -7
  9. package/functions/api-v2/routes/profile.js +2 -1
  10. package/functions/api-v2/tests/stage4-profile-paths.test.js +52 -0
  11. package/functions/api-v2/tests/stage5-aum-bigquery.test.js +81 -0
  12. package/functions/api-v2/tests/stage7-pi-page-views.test.js +55 -0
  13. package/functions/api-v2/tests/stage8-watchlist-membership.test.js +49 -0
  14. package/functions/api-v2/tests/stage9-user-alert-settings.test.js +81 -0
  15. package/functions/computation-system-v2/computations/BehavioralAnomaly.js +104 -81
  16. package/functions/computation-system-v2/computations/NewSectorExposure.js +7 -7
  17. package/functions/computation-system-v2/computations/NewSocialPost.js +6 -6
  18. package/functions/computation-system-v2/computations/PositionInvestedIncrease.js +11 -11
  19. package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +1 -1
  20. package/functions/computation-system-v2/config/bulltrackers.config.js +8 -0
  21. package/functions/computation-system-v2/framework/core/Manifest.js +1 -0
  22. package/functions/computation-system-v2/handlers/scheduler.js +15 -24
  23. package/functions/core/utils/bigquery_utils.js +32 -0
  24. package/package.json +1 -1
@@ -23,7 +23,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
23
23
  // [FIX] Check if computation date is earlier than today (backfill protection)
24
24
  const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
25
25
  const isHistoricalData = computationDate < today;
26
-
26
+
27
27
  // If it's historical data, check if this alert already exists
28
28
  if (isHistoricalData) {
29
29
  // Check if we've already created alerts for this PI/date/alertType combination
@@ -35,26 +35,26 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
35
35
  .where('alertType', '==', alertType.id)
36
36
  .limit(1)
37
37
  .get();
38
-
38
+
39
39
  if (!existingAlertsSnapshot.empty) {
40
40
  logger.log('INFO', `[processAlertForPI] Skipping duplicate alert for historical data: PI ${piCid}, date ${computationDate}, alert type ${alertType.id}`);
41
41
  return;
42
42
  }
43
-
43
+
44
44
  logger.log('WARN', `[processAlertForPI] Processing alert for historical data (backfill): PI ${piCid}, date ${computationDate}, alert type ${alertType.id}. This alert will only be sent to developers.`);
45
45
  }
46
-
46
+
47
47
  // 1. Get PI username from rankings or subscriptions
48
48
  const piUsername = await getPIUsername(db, piCid);
49
-
49
+
50
50
  // 2. Find all users subscribed to this PI and alert type
51
51
  // Use computationName (e.g., 'RiskScoreIncrease') to map to alertConfig keys
52
52
  let subscriptions = await findSubscriptionsForPI(db, logger, piCid, alertType.computationName, computationDate, dependencies);
53
-
53
+
54
54
  // [FIX] If it's historical data, only send to developer accounts
55
55
  if (isHistoricalData) {
56
56
  const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
57
-
57
+
58
58
  // Filter subscriptions to only include developers
59
59
  const devSubscriptions = [];
60
60
  for (const subscription of subscriptions) {
@@ -63,32 +63,32 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
63
63
  devSubscriptions.push(subscription);
64
64
  }
65
65
  }
66
-
66
+
67
67
  subscriptions = devSubscriptions;
68
68
  logger.log('INFO', `[processAlertForPI] Historical data: Filtered to ${subscriptions.length} developer subscriptions for PI ${piCid}, alert type ${alertType.id}`);
69
69
  }
70
-
70
+
71
71
  if (subscriptions.length === 0) {
72
72
  logger.log('INFO', `[processAlertForPI] No subscriptions found for PI ${piCid}, alert type ${alertType.id}`);
73
73
  return;
74
74
  }
75
-
75
+
76
76
  logger.log('INFO', `[processAlertForPI] Processing ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertType.id}`);
77
-
77
+
78
78
  // 3. Generate alert message
79
79
  const alertMessage = generateAlertMessage(alertType, piUsername, computationMetadata);
80
-
80
+
81
81
  // 4. Create notifications for each subscribed user (using CID and collection registry)
82
82
  const { collectionRegistry } = dependencies;
83
83
  const config = dependencies.config || {};
84
84
  const notificationPromises = [];
85
-
85
+
86
86
  // Check watchlistAlerts preference for each user
87
87
  const { manageNotificationPreferences } = require('../../api-v2/helpers/data-fetchers/firestore.js');
88
-
88
+
89
89
  for (const subscription of subscriptions) {
90
90
  const userCid = subscription.userCid;
91
-
91
+
92
92
  // [FIX] Check user's watchlistAlerts preference before creating alert
93
93
  try {
94
94
  const prefs = await manageNotificationPreferences(db, userCid, 'get');
@@ -100,16 +100,16 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
100
100
  logger.log('WARN', `[processAlertForPI] Error checking watchlistAlerts preference for user ${userCid}: ${prefError.message}`);
101
101
  // Continue anyway - fail open for important alerts
102
102
  }
103
-
103
+
104
104
  // [NEW] Evaluate dynamic conditions based on user's subscription mode
105
105
  // Check if user is a developer for bypass
106
106
  const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
107
107
  const isDev = await isDeveloper(db, String(userCid));
108
-
108
+
109
109
  // Get user's configuration from subscription
110
110
  const userDynamicConfig = subscription.dynamicConfig?.[alertType.configKey] || {};
111
111
  const userUseDynamic = subscription.useDynamic?.[alertType.configKey] === true;
112
-
112
+
113
113
  // Evaluate conditions (will pass if static mode or no conditions)
114
114
  const evaluation = evaluateDynamicConditions(
115
115
  alertType,
@@ -119,20 +119,20 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
119
119
  isDev,
120
120
  logger
121
121
  );
122
-
122
+
123
123
  if (!evaluation.passes) {
124
124
  logger.log('DEBUG', `[processAlertForPI] User ${userCid} dynamic conditions not met for ${alertType.id}: ${evaluation.reason}`);
125
125
  continue; // Skip this user
126
126
  }
127
-
127
+
128
128
  if (evaluation.developerBypass) {
129
129
  logger.log('INFO', `[processAlertForPI] Developer ${userCid} bypass: ${evaluation.reason}`);
130
130
  } else {
131
131
  logger.log('DEBUG', `[processAlertForPI] User ${userCid} alert ${alertType.id} in ${evaluation.mode} mode`);
132
132
  }
133
-
133
+
134
134
  const notificationId = `alert_${Date.now()}_${userCid}_${piCid}_${Math.random().toString(36).substring(2, 9)}`;
135
-
135
+
136
136
  const notificationData = {
137
137
  id: notificationId,
138
138
  type: 'watchlistAlerts', // Use watchlistAlerts type for watchlist-based alerts
@@ -157,7 +157,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
157
157
  ...(computationMetadata || {})
158
158
  }
159
159
  };
160
-
160
+
161
161
  // Write to alerts collection (not notifications) - alerts are separate from system notifications
162
162
  // Write directly to new path: SignedInUsers/{userCid}/alerts/{alertId}
163
163
  const alertData = {
@@ -176,7 +176,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
176
176
  computationName: alertType.computationName,
177
177
  ...(computationMetadata || {})
178
178
  };
179
-
179
+
180
180
  // Combined promise: write to Firestore AND send FCM push notification
181
181
  const writeAndPushPromise = (async () => {
182
182
  // 1. Write to Firestore first (this is the source of truth)
@@ -185,7 +185,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
185
185
  .collection('alerts')
186
186
  .doc(notificationId)
187
187
  .set(alertData);
188
-
188
+
189
189
  // 2. Send FCM push notification (non-blocking, don't fail if push fails)
190
190
  // This enables notifications even when user is offline
191
191
  try {
@@ -194,46 +194,70 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
194
194
  // Log but don't throw - FCM failure shouldn't fail the alert
195
195
  logger.log('WARN', `[processAlertForPI] FCM push failed for CID ${userCid}: ${fcmError.message}`);
196
196
  }
197
-
197
+
198
198
  return { userCid, success: true };
199
199
  })().catch(err => {
200
200
  logger.log('ERROR', `[processAlertForPI] Failed to write alert for CID ${userCid}: ${err.message}`, err);
201
201
  throw err; // Re-throw so we know if writes are failing
202
202
  });
203
-
203
+
204
204
  notificationPromises.push(writeAndPushPromise);
205
205
  }
206
-
206
+
207
207
  // Wait for all notifications to be written and push notifications sent
208
208
  await Promise.all(notificationPromises);
209
-
209
+
210
+ // 4b. Write to BigQuery pi_alert_history (for computation system and analytics)
211
+ if (process.env.BIGQUERY_ENABLED !== 'false') {
212
+ try {
213
+ const { ensurePIAlertHistoryTable, insertRowsWithMerge } = require('../../core/utils/bigquery_utils');
214
+ await ensurePIAlertHistoryTable(logger);
215
+ const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
216
+ const now = new Date().toISOString();
217
+ const row = {
218
+ date: computationDate,
219
+ pi_id: Number(piCid),
220
+ alert_type: alertType.computationName || alertType.id,
221
+ triggered: true,
222
+ trigger_count: subscriptions.length,
223
+ triggered_for: subscriptions.map(s => String(s.userCid)),
224
+ metadata: computationMetadata && typeof computationMetadata === 'object' ? computationMetadata : {},
225
+ last_triggered: now,
226
+ last_updated: now
227
+ };
228
+ await insertRowsWithMerge(datasetId, 'pi_alert_history', [row], ['date', 'pi_id', 'alert_type'], logger);
229
+ } catch (bqErr) {
230
+ logger.log('WARN', `[processAlertForPI] pi_alert_history write failed: ${bqErr.message}`);
231
+ }
232
+ }
233
+
210
234
  // 5. Notify the PI themselves if they are a signed-in user (Optional feature)
211
235
  await notifyPIIfSignedIn(db, logger, piCid, alertType, subscriptions.length);
212
236
 
213
237
  // 6. Update global rootdata collection for computation system
214
238
  // (Wrap in try-catch to prevent crashing the alert if metrics fail)
215
239
  try {
216
- const { runRootDataIndexer } = require('../../root-data-indexer/index');
217
- const triggeredUserCids = subscriptions.map(s => s.userCid);
218
-
219
- // Update alert history root data by running root data indexer for the specific date
220
- // The indexer will detect and update PIAlertHistoryData availability
221
- const indexerConfig = {
222
- availabilityCollection: 'root_data_availability',
223
- targetDate: computationDate,
224
- collections: {
225
- piAlertHistory: 'PIAlertHistoryData'
226
- }
227
- };
228
-
229
- await runRootDataIndexer(indexerConfig, { db, logger });
230
- logger.log('INFO', `[processAlertForPI] Updated root data indexer for date ${computationDate}`);
240
+ const { runRootDataIndexer } = require('../../root-data-indexer/index');
241
+ const triggeredUserCids = subscriptions.map(s => s.userCid);
242
+
243
+ // Update alert history root data by running root data indexer for the specific date
244
+ // The indexer will detect and update PIAlertHistoryData availability
245
+ const indexerConfig = {
246
+ availabilityCollection: 'root_data_availability',
247
+ targetDate: computationDate,
248
+ collections: {
249
+ piAlertHistory: 'PIAlertHistoryData'
250
+ }
251
+ };
252
+
253
+ await runRootDataIndexer(indexerConfig, { db, logger });
254
+ logger.log('INFO', `[processAlertForPI] Updated root data indexer for date ${computationDate}`);
231
255
  } catch (e) {
232
- logger.log('WARN', `[processAlertForPI] Failed to update history rootdata: ${e.message}`);
256
+ logger.log('WARN', `[processAlertForPI] Failed to update history rootdata: ${e.message}`);
233
257
  }
234
-
258
+
235
259
  logger.log('SUCCESS', `[processAlertForPI] Created ${notificationPromises.length} notifications for PI ${piCid}, alert type ${alertType.id}`);
236
-
260
+
237
261
  // [FIX] Mark alert as processed to prevent duplicates on backfill
238
262
  if (isHistoricalData) {
239
263
  try {
@@ -255,7 +279,7 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
255
279
  // Don't throw - this is non-critical
256
280
  }
257
281
  }
258
-
282
+
259
283
  } catch (error) {
260
284
  logger.log('ERROR', `[processAlertForPI] Error processing alert for PI ${piCid}`, error);
261
285
  throw error;
@@ -269,24 +293,24 @@ async function processAlertForPI(db, logger, piCid, alertType, computationMetada
269
293
  */
270
294
  async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computationDate, dependencies = {}) {
271
295
  const subscriptions = [];
272
-
296
+
273
297
  // [DYNAMIC] Get configKey from alertType metadata instead of hardcoded mapping
274
298
  // The alertType is passed in dependencies and contains the configKey from computation metadata
275
299
  const alertType = dependencies.alertType;
276
300
  const configKey = alertType?.configKey;
277
-
301
+
278
302
  if (!configKey) {
279
303
  logger.log('WARN', `[findSubscriptionsForPI] No configKey found for alert type: ${alertTypeId}`);
280
304
  return subscriptions;
281
305
  }
282
-
306
+
283
307
  // Check for developer accounts with pretendSubscribedToAllAlerts flag enabled
284
308
  // This allows developers to test the alert system without manually configuring subscriptions
285
309
  try {
286
310
  const { isDeveloper } = require('../../api-v2/helpers/data-fetchers/firestore.js');
287
311
  const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
288
312
  const config = dependencies.config || {};
289
-
313
+
290
314
  // Get PI username from master list
291
315
  let piUsername = `PI-${piCid}`;
292
316
  try {
@@ -295,7 +319,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
295
319
  } catch (e) {
296
320
  // PI not in master list, use fallback
297
321
  }
298
-
322
+
299
323
  // Default alert config with all alert types enabled (for dev override)
300
324
  const allAlertsEnabledConfig = {
301
325
  increasedRisk: true,
@@ -307,22 +331,22 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
307
331
  behavioralAnomaly: true,
308
332
  testSystemProbe: true // Test alerts for developers
309
333
  };
310
-
334
+
311
335
  // Check all developer accounts
312
336
  const devOverridesCollection = config.devOverridesCollection || 'dev_overrides';
313
337
  const devOverridesSnapshot = await db.collection(devOverridesCollection).get();
314
-
338
+
315
339
  for (const devOverrideDoc of devOverridesSnapshot.docs) {
316
340
  const devUserCid = Number(devOverrideDoc.id);
317
-
341
+
318
342
  // Verify this is actually a developer account (security check) - using api-v2 helper
319
343
  const isDev = await isDeveloper(db, String(devUserCid));
320
344
  if (!isDev) {
321
345
  continue;
322
346
  }
323
-
347
+
324
348
  const devOverrideData = devOverrideDoc.data();
325
-
349
+
326
350
  // Check if this developer has the pretendSubscribedToAllAlerts flag enabled
327
351
  if (devOverrideData.enabled === true && devOverrideData.pretendSubscribedToAllAlerts === true) {
328
352
  // Add this developer as a subscription for this PI and alert type
@@ -334,7 +358,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
334
358
  watchlistName: 'Dev Override - All Alerts',
335
359
  alertConfig: allAlertsEnabledConfig
336
360
  });
337
-
361
+
338
362
  logger.log('INFO', `[findSubscriptionsForPI] DEV OVERRIDE: Added developer ${devUserCid} to subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
339
363
  }
340
364
  }
@@ -342,19 +366,19 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
342
366
  // Don't fail the entire function if dev override check fails
343
367
  logger.log('WARN', `[findSubscriptionsForPI] Error checking dev overrides: ${error.message}`);
344
368
  }
345
-
369
+
346
370
  // Step 1: Load WatchlistMembershipData/{date} to find which users have this PI in their watchlist
347
371
  const piCidStr = String(piCid);
348
372
  let userCids = [];
349
-
373
+
350
374
  try {
351
375
  const membershipRef = db.collection('WatchlistMembershipData').doc(computationDate);
352
376
  const membershipDoc = await membershipRef.get();
353
-
377
+
354
378
  if (membershipDoc.exists) {
355
379
  const membershipData = membershipDoc.data();
356
380
  const piMembership = membershipData[piCidStr];
357
-
381
+
358
382
  if (piMembership && piMembership.users && Array.isArray(piMembership.users)) {
359
383
  userCids = piMembership.users.map(cid => String(cid));
360
384
  logger.log('INFO', `[findSubscriptionsForPI] Found ${userCids.length} users with PI ${piCid} in watchlist from WatchlistMembershipData/${computationDate}`);
@@ -372,29 +396,29 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
372
396
  // Return subscriptions array which may contain dev overrides
373
397
  return subscriptions;
374
398
  }
375
-
399
+
376
400
  // Step 2: For each user, read their watchlists from SignedInUsers/{cid}/watchlists
377
401
  // Read directly from new path (no migration needed)
378
402
  for (const userCidStr of userCids) {
379
403
  try {
380
404
  const userCid = Number(userCidStr);
381
-
405
+
382
406
  // Read all watchlists for this user from new path
383
407
  const watchlistsSnapshot = await db.collection('SignedInUsers')
384
408
  .doc(String(userCid))
385
409
  .collection('watchlists')
386
410
  .get();
387
-
411
+
388
412
  if (watchlistsSnapshot.empty) {
389
413
  continue;
390
414
  }
391
-
415
+
392
416
  // Get watchlists from snapshot
393
417
  const watchlists = watchlistsSnapshot.docs.map(doc => ({
394
418
  id: doc.id,
395
419
  ...doc.data()
396
420
  }));
397
-
421
+
398
422
  // Step 3: Check each watchlist for the PI and alert config
399
423
  for (const watchlistData of watchlists) {
400
424
  if (watchlistData.type === 'static' && watchlistData.items && Array.isArray(watchlistData.items)) {
@@ -403,12 +427,12 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
403
427
  // Check if this alert type is enabled
404
428
  const isTestProbe = alertTypeId === 'TestSystemProbe';
405
429
  const isEnabled = item.alertConfig && item.alertConfig[configKey] === true;
406
-
430
+
407
431
  // [FIX] Check if this is a test alert (respect testAlerts preference)
408
432
  // Load alert type from dependencies to check isTest flag
409
433
  const alertType = dependencies.alertType;
410
434
  const isTestAlert = alertType && alertType.isTest === true;
411
-
435
+
412
436
  let shouldSendAlert = false;
413
437
  if (isTestAlert) {
414
438
  // Check if user has testAlerts enabled in their notification preferences
@@ -416,7 +440,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
416
440
  const { manageNotificationPreferences } = require('../../api-v2/helpers/data-fetchers/firestore.js');
417
441
  const prefs = await manageNotificationPreferences(db, userCid, 'get');
418
442
  shouldSendAlert = prefs.testAlerts === true;
419
-
443
+
420
444
  if (!shouldSendAlert) {
421
445
  logger.log('DEBUG', `[findSubscriptionsForPI] User ${userCid} has testAlerts disabled, skipping test alert ${alertTypeId}`);
422
446
  }
@@ -429,7 +453,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
429
453
  // For non-test alerts, use normal alert config check
430
454
  shouldSendAlert = isEnabled;
431
455
  }
432
-
456
+
433
457
  if (shouldSendAlert) {
434
458
  subscriptions.push({
435
459
  userCid: userCid,
@@ -451,7 +475,7 @@ async function findSubscriptionsForPI(db, logger, piCid, alertTypeId, computatio
451
475
  continue;
452
476
  }
453
477
  }
454
-
478
+
455
479
  logger.log('INFO', `[findSubscriptionsForPI] Found ${subscriptions.length} subscriptions for PI ${piCid}, alert type ${alertTypeId}`);
456
480
  return subscriptions;
457
481
  }
@@ -477,22 +501,22 @@ async function getPIUsername(db, piCid) {
477
501
  // Try to get from master list first (single source of truth) - using api-v2 helper
478
502
  const { fetchPopularInvestorMasterList } = require('../../api-v2/helpers/data-fetchers/firestore.js');
479
503
  const piData = await fetchPopularInvestorMasterList(db, String(piCid));
480
-
504
+
481
505
  if (piData && piData.username) {
482
506
  return piData.username;
483
507
  }
484
-
508
+
485
509
  // Fallback: try to get from any subscription
486
510
  const subscriptionsSnapshot = await db.collection('watchlist_subscriptions')
487
511
  .where('piCid', '==', Number(piCid))
488
512
  .limit(1)
489
513
  .get();
490
-
514
+
491
515
  if (!subscriptionsSnapshot.empty) {
492
516
  const subData = subscriptionsSnapshot.docs[0].data();
493
517
  return subData.piUsername || `PI-${piCid}`;
494
518
  }
495
-
519
+
496
520
  return `PI-${piCid}`;
497
521
  } catch (error) {
498
522
  return `PI-${piCid}`;
@@ -506,14 +530,14 @@ async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
506
530
  try {
507
531
  const userRef = db.collection('signedInUsers').doc(String(piCid));
508
532
  const userDoc = await userRef.get();
509
-
510
- if (!userDoc.exists) return;
533
+
534
+ if (!userDoc.exists) return;
511
535
 
512
536
  const notificationRef = db.collection('signedInUsers')
513
537
  .doc(String(piCid))
514
538
  .collection('user_alerts_metrics') // Changed from user_alerts/triggered to avoid collision
515
539
  .doc(`trigger_${Date.now()}_${alertType.id}`);
516
-
540
+
517
541
  await notificationRef.set({
518
542
  alertType: alertType.id,
519
543
  alertTypeName: alertType.name,
@@ -522,7 +546,7 @@ async function notifyPIIfSignedIn(db, logger, piCid, alertType, alertCount) {
522
546
  computationDate: new Date().toISOString().split('T')[0],
523
547
  createdAt: FieldValue.serverTimestamp()
524
548
  });
525
-
549
+
526
550
  logger.log('INFO', `[notifyPIIfSignedIn] Notified PI ${piCid} that ${alertCount} users received ${alertType.id} alert`);
527
551
  } catch (error) {
528
552
  // Silent fail - non-critical
@@ -573,12 +597,12 @@ function tryDecompress(data) {
573
597
  function readComputationResults(docData) {
574
598
  try {
575
599
  const decompressed = tryDecompress(docData);
576
-
600
+
577
601
  if (decompressed && typeof decompressed === 'object') {
578
602
  if (decompressed.cids && Array.isArray(decompressed.cids)) {
579
603
  const userDataKeys = Object.keys(decompressed)
580
604
  .filter(key => key !== 'cids' && key !== 'metadata' && /^\d+$/.test(key));
581
-
605
+
582
606
  if (userDataKeys.length > 0) {
583
607
  return {
584
608
  cids: decompressed.cids,
@@ -595,11 +619,11 @@ function readComputationResults(docData) {
595
619
  };
596
620
  }
597
621
  }
598
-
622
+
599
623
  const cids = Object.keys(decompressed)
600
624
  .filter(key => /^\d+$/.test(key))
601
625
  .map(key => Number(key));
602
-
626
+
603
627
  if (cids.length > 0) {
604
628
  return {
605
629
  cids: cids,
@@ -633,14 +657,14 @@ async function readComputationResultsWithShards(db, docData, docRef, logger = nu
633
657
  try {
634
658
  const bucketName = docData.gcsBucket || docData.gcsUri.split('/')[2];
635
659
  const fileName = docData.gcsPath || docData.gcsUri.split('/').slice(3).join('/');
636
-
660
+
637
661
  if (logger) {
638
662
  logger.log('INFO', `[AlertSystem] Reading computation results from GCS: ${fileName}`);
639
663
  }
640
-
664
+
641
665
  // Stream download is memory efficient for large files
642
666
  const [fileContent] = await storage.bucket(bucketName).file(fileName).download();
643
-
667
+
644
668
  // Assume Gzip (as writer does it), if fails try plain
645
669
  let decompressedData;
646
670
  try {
@@ -649,7 +673,7 @@ async function readComputationResultsWithShards(db, docData, docRef, logger = nu
649
673
  // Fallback for uncompressed GCS files
650
674
  decompressedData = JSON.parse(fileContent.toString('utf8'));
651
675
  }
652
-
676
+
653
677
  // Process the decompressed data through readComputationResults
654
678
  return readComputationResults(decompressedData);
655
679
  } catch (gcsErr) {
@@ -666,7 +690,7 @@ async function readComputationResultsWithShards(db, docData, docRef, logger = nu
666
690
  if (docData._sharded === true && docData._shardCount) {
667
691
  const shardsCol = docRef.collection('_shards');
668
692
  const shardsSnapshot = await shardsCol.get();
669
-
693
+
670
694
  if (!shardsSnapshot.empty) {
671
695
  let mergedData = {};
672
696
  for (const shardDoc of shardsSnapshot.docs) {