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.
- package/functions/alert-system/helpers/alert_helpers.js +114 -90
- package/functions/alert-system/helpers/alert_manifest_loader.js +88 -99
- package/functions/alert-system/index.js +81 -138
- package/functions/alert-system/tests/stage1-alert-manifest.test.js +94 -0
- package/functions/alert-system/tests/stage2-alert-metadata.test.js +93 -0
- package/functions/alert-system/tests/stage3-alert-handler.test.js +79 -0
- package/functions/api-v2/helpers/data-fetchers/firestore.js +613 -478
- package/functions/api-v2/routes/popular_investors.js +7 -7
- package/functions/api-v2/routes/profile.js +2 -1
- package/functions/api-v2/tests/stage4-profile-paths.test.js +52 -0
- package/functions/api-v2/tests/stage5-aum-bigquery.test.js +81 -0
- package/functions/api-v2/tests/stage7-pi-page-views.test.js +55 -0
- package/functions/api-v2/tests/stage8-watchlist-membership.test.js +49 -0
- package/functions/api-v2/tests/stage9-user-alert-settings.test.js +81 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +104 -81
- package/functions/computation-system-v2/computations/NewSectorExposure.js +7 -7
- package/functions/computation-system-v2/computations/NewSocialPost.js +6 -6
- package/functions/computation-system-v2/computations/PositionInvestedIncrease.js +11 -11
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +1 -1
- package/functions/computation-system-v2/config/bulltrackers.config.js +8 -0
- package/functions/computation-system-v2/framework/core/Manifest.js +1 -0
- package/functions/computation-system-v2/handlers/scheduler.js +15 -24
- package/functions/core/utils/bigquery_utils.js +32 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Scheduler V3.
|
|
2
|
+
* @fileoverview Scheduler V3.4: Enhanced Logging for Verification
|
|
3
3
|
* * * 1. Reconcile: Checks ENTIRE graph for stale hashes, triggers ROOTS if needed.
|
|
4
4
|
* * 2. Purge: Actively removes tasks that don't match the current deployment hash.
|
|
5
5
|
* * 3. Watchdog: Recovers "Zombie" tasks (running but stuck).
|
|
6
|
-
* *
|
|
6
|
+
* * 4. Logging: Detailed output for Tombstoned/Existing tasks to verify ETA logic.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const { CloudTasksClient } = require('@google-cloud/tasks');
|
|
@@ -18,8 +18,8 @@ const config = require('../config/bulltrackers.config');
|
|
|
18
18
|
|
|
19
19
|
// Config
|
|
20
20
|
const CLOUD_TASKS_CONCURRENCY = 20;
|
|
21
|
-
const PLANNING_LOOKBACK_DAYS = 7;
|
|
22
|
-
const PLANNING_LOOKAHEAD_HOURS = 24;
|
|
21
|
+
const PLANNING_LOOKBACK_DAYS = 7;
|
|
22
|
+
const PLANNING_LOOKAHEAD_HOURS = 24;
|
|
23
23
|
const ZOMBIE_THRESHOLD_MINUTES = 15;
|
|
24
24
|
|
|
25
25
|
// Cache singleton instances
|
|
@@ -45,7 +45,6 @@ async function initialize() {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* ENTRY POINT 1: The Planner
|
|
48
|
-
* Trigger: Cloud Scheduler -> "0 * * * *" (Hourly)
|
|
49
48
|
*/
|
|
50
49
|
async function planComputations(req, res) {
|
|
51
50
|
try {
|
|
@@ -62,7 +61,6 @@ async function planComputations(req, res) {
|
|
|
62
61
|
|
|
63
62
|
console.log(`[Planner] Reconciling window: ${windowStart.toISOString()} to ${windowEnd.toISOString()}`);
|
|
64
63
|
|
|
65
|
-
// Helper to find Roots for any given computation (Pass 1..N)
|
|
66
64
|
const manifestMap = new Map(manifest.map(m => [m.name, m]));
|
|
67
65
|
const getRoots = (entry, visited = new Set()) => {
|
|
68
66
|
if (visited.has(entry.name)) return [];
|
|
@@ -74,7 +72,7 @@ async function planComputations(req, res) {
|
|
|
74
72
|
.flatMap(p => getRoots(p, visited));
|
|
75
73
|
};
|
|
76
74
|
|
|
77
|
-
const tasksToSchedule = new Map();
|
|
75
|
+
const tasksToSchedule = new Map();
|
|
78
76
|
const stats = { checked: 0, scheduled: 0, mismatched: 0, missing: 0 };
|
|
79
77
|
|
|
80
78
|
const targetDates = [];
|
|
@@ -89,9 +87,7 @@ async function planComputations(req, res) {
|
|
|
89
87
|
const dateStr = dateObj.toISOString().split('T')[0];
|
|
90
88
|
const dailyStatus = await stateRepository.getDailyStatus(dateStr);
|
|
91
89
|
|
|
92
|
-
// Iterate ALL computations (not just Pass 1) to find stale nodes
|
|
93
90
|
for (const entry of manifest) {
|
|
94
|
-
// If this specific entry is not scheduled for today, skip it
|
|
95
91
|
if (!shouldRunOnDate(entry.schedule, dateObj)) continue;
|
|
96
92
|
|
|
97
93
|
stats.checked++;
|
|
@@ -107,15 +103,13 @@ async function planComputations(req, res) {
|
|
|
107
103
|
}
|
|
108
104
|
|
|
109
105
|
if (reason) {
|
|
110
|
-
// If entry is stale, we must schedule its ROOT(s) to trigger the chain
|
|
111
106
|
const roots = getRoots(entry);
|
|
112
107
|
|
|
113
108
|
roots.forEach(root => {
|
|
114
|
-
// Unique Task Key: RootName + Date + Hash
|
|
115
109
|
const taskKey = `root-${toKebab(root.originalName)}-${dateStr}-${root.hash}`;
|
|
116
110
|
|
|
117
111
|
if (!tasksToSchedule.has(taskKey)) {
|
|
118
|
-
//
|
|
112
|
+
// Calculate proper ETA (Next valid window)
|
|
119
113
|
const runAt = getNextRunWindow(root.schedule, dateObj);
|
|
120
114
|
|
|
121
115
|
tasksToSchedule.set(taskKey, {
|
|
@@ -260,31 +254,21 @@ function shouldRunOnDate(schedule, dateObj) {
|
|
|
260
254
|
return true;
|
|
261
255
|
}
|
|
262
256
|
|
|
263
|
-
/**
|
|
264
|
-
* Calculates the run time.
|
|
265
|
-
* If the Target Date's schedule is in the PAST, it projects execution to the NEXT available window (Today or Tomorrow).
|
|
266
|
-
*/
|
|
267
257
|
function getNextRunWindow(schedule, targetDateObj) {
|
|
268
258
|
const [h, m] = (schedule.time || '02:00').split(':').map(Number);
|
|
269
|
-
|
|
270
|
-
// 1. Calculate the ideal run time on the TARGET date
|
|
271
259
|
let runTime = new Date(targetDateObj);
|
|
272
260
|
runTime.setUTCHours(h, m, 0, 0);
|
|
273
261
|
|
|
274
262
|
const now = Date.now();
|
|
275
|
-
|
|
276
|
-
// 2. If ideal time is in the past, move it to the *next* occurrence relative to NOW
|
|
263
|
+
// If idealized time is in the past, shift to NEXT available window relative to NOW
|
|
277
264
|
if (runTime.getTime() < now) {
|
|
278
265
|
const nextWindow = new Date();
|
|
279
266
|
nextWindow.setUTCHours(h, m, 0, 0);
|
|
280
|
-
|
|
281
|
-
// If today's window has passed, move to tomorrow
|
|
282
267
|
if (nextWindow.getTime() <= now) {
|
|
283
268
|
nextWindow.setUTCDate(nextWindow.getUTCDate() + 1);
|
|
284
269
|
}
|
|
285
270
|
return nextWindow.getTime() / 1000;
|
|
286
271
|
}
|
|
287
|
-
|
|
288
272
|
return runTime.getTime() / 1000;
|
|
289
273
|
}
|
|
290
274
|
|
|
@@ -334,10 +318,17 @@ async function dispatchTasks(tasks) {
|
|
|
334
318
|
return { status: 'scheduled' };
|
|
335
319
|
|
|
336
320
|
} catch (e) {
|
|
321
|
+
// ALREADY_EXISTS (6) or CONFLICT (409)
|
|
337
322
|
if (e.code === 6 || e.code === 409) {
|
|
338
|
-
|
|
323
|
+
// ENHANCED LOGGING:
|
|
324
|
+
const eta = t.runAtSeconds
|
|
325
|
+
? new Date(t.runAtSeconds * 1000).toISOString()
|
|
326
|
+
: 'Immediate';
|
|
327
|
+
|
|
328
|
+
console.warn(`[Planner] ⚠️ Task collision (Tombstone/Exists): ${t.computation} | Target: ${t.targetDate} | Planned ETA: ${eta} | Hash: ${t.configHash}`);
|
|
339
329
|
return { status: 'exists' };
|
|
340
330
|
}
|
|
331
|
+
|
|
341
332
|
console.error(`[Planner] Failed task ${t.computation}: ${e.message}`);
|
|
342
333
|
return { status: 'error' };
|
|
343
334
|
}
|
|
@@ -595,6 +595,15 @@ const SCHEMAS = {
|
|
|
595
595
|
{ name: 'metadata', type: 'JSON', mode: 'NULLABLE' },
|
|
596
596
|
{ name: 'last_triggered', type: 'TIMESTAMP', mode: 'NULLABLE' },
|
|
597
597
|
{ name: 'last_updated', type: 'TIMESTAMP', mode: 'REQUIRED' }
|
|
598
|
+
],
|
|
599
|
+
user_alert_settings: [
|
|
600
|
+
{ name: 'date', type: 'DATE', mode: 'REQUIRED' },
|
|
601
|
+
{ name: 'user_id', type: 'STRING', mode: 'REQUIRED' },
|
|
602
|
+
{ name: 'watchlist_id', type: 'STRING', mode: 'REQUIRED' },
|
|
603
|
+
{ name: 'watchlist_name', type: 'STRING', mode: 'NULLABLE' },
|
|
604
|
+
{ name: 'visibility', type: 'STRING', mode: 'NULLABLE' },
|
|
605
|
+
{ name: 'items', type: 'JSON', mode: 'NULLABLE' },
|
|
606
|
+
{ name: 'updated_at', type: 'TIMESTAMP', mode: 'REQUIRED' }
|
|
598
607
|
]
|
|
599
608
|
};
|
|
600
609
|
|
|
@@ -891,6 +900,28 @@ async function ensurePIAlertHistoryTable(logger = null) {
|
|
|
891
900
|
);
|
|
892
901
|
}
|
|
893
902
|
|
|
903
|
+
/**
|
|
904
|
+
* Ensure user_alert_settings table exists
|
|
905
|
+
* @param {object} logger - Logger instance
|
|
906
|
+
* @returns {Promise<Table>}
|
|
907
|
+
*/
|
|
908
|
+
async function ensureUserAlertSettingsTable(logger = null) {
|
|
909
|
+
const datasetId = process.env.BIGQUERY_DATASET_ID || 'bulltrackers_data';
|
|
910
|
+
const tableId = 'user_alert_settings';
|
|
911
|
+
const schema = getSchema(tableId);
|
|
912
|
+
|
|
913
|
+
return await ensureTableExists(
|
|
914
|
+
datasetId,
|
|
915
|
+
tableId,
|
|
916
|
+
schema,
|
|
917
|
+
{
|
|
918
|
+
partitionField: 'date',
|
|
919
|
+
clusterFields: ['user_id', 'watchlist_id']
|
|
920
|
+
},
|
|
921
|
+
logger
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
894
925
|
/**
|
|
895
926
|
* Query portfolio data from BigQuery
|
|
896
927
|
* @param {string} dateStr - Date string (YYYY-MM-DD)
|
|
@@ -2272,6 +2303,7 @@ module.exports = {
|
|
|
2272
2303
|
ensurePIPageViewsTable,
|
|
2273
2304
|
ensureWatchlistMembershipTable,
|
|
2274
2305
|
ensurePIAlertHistoryTable,
|
|
2306
|
+
ensureUserAlertSettingsTable,
|
|
2275
2307
|
queryPortfolioData,
|
|
2276
2308
|
queryHistoryData,
|
|
2277
2309
|
querySocialData,
|