neoagent 2.4.2-beta.3 → 2.4.2-beta.5
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/.env.example +0 -9
- package/docs/configuration.md +0 -10
- package/flutter_app/lib/main.dart +0 -1
- package/flutter_app/lib/main_account_settings.dart +57 -16
- package/flutter_app/lib/main_controller.dart +0 -160
- package/flutter_app/lib/main_models.dart +6 -0
- package/flutter_app/lib/main_operations.dart +17 -1
- package/flutter_app/lib/main_runtime.dart +0 -22
- package/flutter_app/lib/main_shared.dart +0 -161
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
- package/flutter_app/pubspec.lock +0 -8
- package/flutter_app/pubspec.yaml +0 -1
- package/landing/index.html +1 -1
- package/package.json +1 -1
- package/server/admin/admin.css +23 -0
- package/server/admin/admin.js +89 -32
- package/server/admin/analytics.js +254 -48
- package/server/admin/index.html +36 -2
- package/server/admin/users.js +94 -37
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/NOTICES +0 -183
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +77607 -78350
- package/server/routes/account.js +11 -2
- package/server/routes/admin.js +106 -24
- package/server/routes/runtime.js +1 -4
- package/server/services/ai/engine.js +13 -11
- package/server/services/ai/models.js +10 -4
- package/flutter_app/lib/src/analytics_service.dart +0 -294
- package/server/config/analytics.js +0 -30
package/server/routes/account.js
CHANGED
|
@@ -116,10 +116,19 @@ router.get('/usage', (req, res) => {
|
|
|
116
116
|
const userLimits = db.prepare('SELECT rate_limit_4h, rate_limit_weekly FROM users WHERE id = ?').get(userId);
|
|
117
117
|
const h4Tokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-4 hours')").get(userId).t;
|
|
118
118
|
const weeklyTokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-7 days')").get(userId).t;
|
|
119
|
+
|
|
120
|
+
const globalLimit4h = process.env.NEOAGENT_RATE_LIMIT_4H ? parseInt(process.env.NEOAGENT_RATE_LIMIT_4H, 10) : null;
|
|
121
|
+
const globalLimitWeekly = process.env.NEOAGENT_RATE_LIMIT_WEEKLY ? parseInt(process.env.NEOAGENT_RATE_LIMIT_WEEKLY, 10) : null;
|
|
122
|
+
|
|
123
|
+
const custom4h = userLimits?.rate_limit_4h ?? null;
|
|
124
|
+
const customWeekly = userLimits?.rate_limit_weekly ?? null;
|
|
125
|
+
|
|
119
126
|
res.json({
|
|
120
127
|
limits: {
|
|
121
|
-
fourHour:
|
|
122
|
-
weekly:
|
|
128
|
+
fourHour: custom4h ?? globalLimit4h,
|
|
129
|
+
weekly: customWeekly ?? globalLimitWeekly,
|
|
130
|
+
fourHourIsCustom: custom4h !== null,
|
|
131
|
+
weeklyIsCustom: customWeekly !== null,
|
|
123
132
|
},
|
|
124
133
|
usage: {
|
|
125
134
|
fourHour: h4Tokens,
|
package/server/routes/admin.js
CHANGED
|
@@ -416,20 +416,69 @@ router.delete('/api/settings/apikey', requireAdminAuth, settingsLimiter, (req, r
|
|
|
416
416
|
|
|
417
417
|
router.get('/api/analytics', requireAdminAuth, (req, res) => {
|
|
418
418
|
const db = require('../db/database');
|
|
419
|
+
const range = Math.min(Math.max(parseInt(req.query.range) || 30, 1), 365);
|
|
419
420
|
const now = new Date().toISOString();
|
|
420
421
|
const dayAgo = new Date(Date.now() - 86_400_000).toISOString();
|
|
421
422
|
const weekAgo = new Date(Date.now() - 7 * 86_400_000).toISOString();
|
|
423
|
+
const rangeAgo = new Date(Date.now() - range * 86_400_000).toISOString();
|
|
422
424
|
try {
|
|
425
|
+
const totalRunsRow = db.prepare('SELECT COUNT(*) AS n, COALESCE(SUM(total_tokens),0) AS t FROM agent_runs').get();
|
|
426
|
+
const successRow = db.prepare("SELECT COUNT(*) AS n FROM agent_runs WHERE status = 'completed'").get();
|
|
423
427
|
const stats = {
|
|
424
|
-
totalUsers:
|
|
425
|
-
activeToday:
|
|
426
|
-
newThisWeek:
|
|
427
|
-
totalRuns:
|
|
428
|
-
runsToday:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
totalUsers: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
|
|
429
|
+
activeToday: db.prepare('SELECT COUNT(*) AS n FROM users WHERE last_login > ?').get(dayAgo).n,
|
|
430
|
+
newThisWeek: db.prepare('SELECT COUNT(*) AS n FROM users WHERE created_at > ?').get(weekAgo).n,
|
|
431
|
+
totalRuns: totalRunsRow.n,
|
|
432
|
+
runsToday: db.prepare('SELECT COUNT(*) AS n FROM agent_runs WHERE created_at > ?').get(dayAgo).n,
|
|
433
|
+
runsThisWeek: db.prepare('SELECT COUNT(*) AS n FROM agent_runs WHERE created_at > ?').get(weekAgo).n,
|
|
434
|
+
totalTokens: totalRunsRow.t,
|
|
435
|
+
tokensToday: db.prepare("SELECT COALESCE(SUM(total_tokens),0) AS n FROM agent_runs WHERE created_at > ?").get(dayAgo).n,
|
|
436
|
+
avgTokensPerRun: totalRunsRow.n > 0 ? Math.round(totalRunsRow.t / totalRunsRow.n) : 0,
|
|
437
|
+
successRate: totalRunsRow.n > 0 ? Math.round((successRow.n / totalRunsRow.n) * 100) : 0,
|
|
438
|
+
activeSessions: db.prepare('SELECT COUNT(*) AS n FROM user_sessions WHERE revoked_at IS NULL AND expires_at > ?').get(now).n,
|
|
439
|
+
totalStorage: db.prepare('SELECT COALESCE(SUM(byte_size),0) AS n FROM artifacts').get().n,
|
|
432
440
|
};
|
|
441
|
+
|
|
442
|
+
// Time-series: runs + tokens per day for selected range
|
|
443
|
+
const runsByDay = db.prepare(`
|
|
444
|
+
SELECT date(created_at) AS date,
|
|
445
|
+
COUNT(*) AS runs,
|
|
446
|
+
COALESCE(SUM(total_tokens), 0) AS tokens
|
|
447
|
+
FROM agent_runs
|
|
448
|
+
WHERE created_at > ?
|
|
449
|
+
GROUP BY date(created_at)
|
|
450
|
+
ORDER BY date
|
|
451
|
+
`).all(rangeAgo);
|
|
452
|
+
|
|
453
|
+
// New users per day for selected range
|
|
454
|
+
const usersByDay = db.prepare(`
|
|
455
|
+
SELECT date(created_at) AS date, COUNT(*) AS newUsers
|
|
456
|
+
FROM users
|
|
457
|
+
WHERE created_at > ?
|
|
458
|
+
GROUP BY date(created_at)
|
|
459
|
+
ORDER BY date
|
|
460
|
+
`).all(rangeAgo);
|
|
461
|
+
|
|
462
|
+
// Model breakdown (top 10 by runs)
|
|
463
|
+
const modelBreakdown = db.prepare(`
|
|
464
|
+
SELECT COALESCE(model, 'unknown') AS model,
|
|
465
|
+
COUNT(*) AS runs,
|
|
466
|
+
COALESCE(SUM(total_tokens), 0) AS tokens
|
|
467
|
+
FROM agent_runs
|
|
468
|
+
WHERE created_at > ?
|
|
469
|
+
GROUP BY model
|
|
470
|
+
ORDER BY runs DESC
|
|
471
|
+
LIMIT 10
|
|
472
|
+
`).all(rangeAgo);
|
|
473
|
+
|
|
474
|
+
// Run status breakdown
|
|
475
|
+
const statusBreakdown = db.prepare(`
|
|
476
|
+
SELECT status, COUNT(*) AS count
|
|
477
|
+
FROM agent_runs
|
|
478
|
+
GROUP BY status
|
|
479
|
+
ORDER BY count DESC
|
|
480
|
+
`).all();
|
|
481
|
+
|
|
433
482
|
const topUsers = db.prepare(`
|
|
434
483
|
SELECT u.id, u.username, u.display_name,
|
|
435
484
|
COALESCE(r.runs, 0) AS runs,
|
|
@@ -437,25 +486,25 @@ router.get('/api/analytics', requireAdminAuth, (req, res) => {
|
|
|
437
486
|
COALESCE(a.storage, 0) AS storage
|
|
438
487
|
FROM users u
|
|
439
488
|
LEFT JOIN (
|
|
440
|
-
SELECT user_id,
|
|
441
|
-
COUNT(*) AS runs,
|
|
442
|
-
COALESCE(SUM(total_tokens),0) AS tokens
|
|
489
|
+
SELECT user_id, COUNT(*) AS runs, COALESCE(SUM(total_tokens),0) AS tokens
|
|
443
490
|
FROM agent_runs GROUP BY user_id
|
|
444
491
|
) r ON r.user_id = u.id
|
|
445
492
|
LEFT JOIN (
|
|
446
493
|
SELECT user_id, COALESCE(SUM(byte_size),0) AS storage
|
|
447
494
|
FROM artifacts GROUP BY user_id
|
|
448
495
|
) a ON a.user_id = u.id
|
|
449
|
-
ORDER BY
|
|
496
|
+
ORDER BY tokens DESC LIMIT 10
|
|
450
497
|
`).all();
|
|
498
|
+
|
|
451
499
|
const recentRuns = db.prepare(`
|
|
452
|
-
SELECT r.id, u.username, r.title, r.status, r.total_tokens,
|
|
500
|
+
SELECT r.id, u.username, r.title, r.status, r.model, r.total_tokens,
|
|
453
501
|
r.created_at, r.completed_at
|
|
454
502
|
FROM agent_runs r
|
|
455
503
|
JOIN users u ON u.id = r.user_id
|
|
456
|
-
ORDER BY r.created_at DESC LIMIT
|
|
504
|
+
ORDER BY r.created_at DESC LIMIT 25
|
|
457
505
|
`).all();
|
|
458
|
-
|
|
506
|
+
|
|
507
|
+
res.json({ stats, runsByDay, usersByDay, modelBreakdown, statusBreakdown, topUsers, recentRuns });
|
|
459
508
|
} catch (err) {
|
|
460
509
|
res.status(500).json({ error: String(err.message || err) });
|
|
461
510
|
}
|
|
@@ -618,29 +667,62 @@ router.put('/api/providers', requireAdminAuth, express.json(), (req, res) => {
|
|
|
618
667
|
res.json({ ok: true });
|
|
619
668
|
});
|
|
620
669
|
|
|
670
|
+
// --- Global default rate limits ---
|
|
671
|
+
|
|
672
|
+
router.get('/api/config/rate-limits', requireAdminAuth, (req, res) => {
|
|
673
|
+
res.json({
|
|
674
|
+
rate_limit_4h: process.env.NEOAGENT_RATE_LIMIT_4H ? parseInt(process.env.NEOAGENT_RATE_LIMIT_4H, 10) : null,
|
|
675
|
+
rate_limit_weekly: process.env.NEOAGENT_RATE_LIMIT_WEEKLY ? parseInt(process.env.NEOAGENT_RATE_LIMIT_WEEKLY, 10) : null,
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
router.put('/api/config/rate-limits', requireAdminAuth, express.json(), (req, res) => {
|
|
680
|
+
const { rate_limit_4h, rate_limit_weekly } = req.body || {};
|
|
681
|
+
const parse = (v) => (v !== null && v !== undefined && v !== '' ? parseInt(v, 10) : null);
|
|
682
|
+
const v4h = parse(rate_limit_4h);
|
|
683
|
+
const vWeekly = parse(rate_limit_weekly);
|
|
684
|
+
upsertEnvValue(ENV_FILE, 'NEOAGENT_RATE_LIMIT_4H', v4h !== null ? String(v4h) : '');
|
|
685
|
+
upsertEnvValue(ENV_FILE, 'NEOAGENT_RATE_LIMIT_WEEKLY', vWeekly !== null ? String(vWeekly) : '');
|
|
686
|
+
process.env.NEOAGENT_RATE_LIMIT_4H = v4h !== null ? String(v4h) : '';
|
|
687
|
+
process.env.NEOAGENT_RATE_LIMIT_WEEKLY = vWeekly !== null ? String(vWeekly) : '';
|
|
688
|
+
res.json({ ok: true });
|
|
689
|
+
});
|
|
690
|
+
|
|
621
691
|
// --- Models ---
|
|
622
692
|
|
|
623
693
|
router.get('/api/models', requireAdminAuth, async (req, res) => {
|
|
624
694
|
const { getSupportedModels } = require('../services/ai/models');
|
|
625
695
|
try {
|
|
626
696
|
const models = await getSupportedModels(null, null);
|
|
627
|
-
const
|
|
628
|
-
const
|
|
629
|
-
res.json({ models,
|
|
697
|
+
const disabledStr = process.env.NEOAGENT_DISABLED_MODELS || '';
|
|
698
|
+
const disabledModels = disabledStr ? disabledStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
699
|
+
res.json({ models, disabledModels });
|
|
630
700
|
} catch (err) {
|
|
631
701
|
res.status(500).json({ error: String(err.message || err) });
|
|
632
702
|
}
|
|
633
703
|
});
|
|
634
704
|
|
|
635
|
-
router.put('/api/models/
|
|
636
|
-
const {
|
|
637
|
-
if (!Array.isArray(
|
|
638
|
-
const value =
|
|
639
|
-
upsertEnvValue(ENV_FILE, '
|
|
640
|
-
process.env.
|
|
705
|
+
router.put('/api/models/config', requireAdminAuth, express.json(), (req, res) => {
|
|
706
|
+
const { disabledModels } = req.body || {};
|
|
707
|
+
if (!Array.isArray(disabledModels)) return res.status(400).json({ error: 'disabledModels must be an array' });
|
|
708
|
+
const value = disabledModels.join(',');
|
|
709
|
+
upsertEnvValue(ENV_FILE, 'NEOAGENT_DISABLED_MODELS', value);
|
|
710
|
+
process.env.NEOAGENT_DISABLED_MODELS = value;
|
|
641
711
|
res.json({ ok: true });
|
|
642
712
|
});
|
|
643
713
|
|
|
714
|
+
router.get('/api/users/:id/rate-limits', requireAdminAuth, (req, res) => {
|
|
715
|
+
const db = require('../db/database');
|
|
716
|
+
const { id } = req.params;
|
|
717
|
+
try {
|
|
718
|
+
const row = db.prepare('SELECT rate_limit_4h, rate_limit_weekly FROM users WHERE id = ?').get(id);
|
|
719
|
+
if (!row) return res.status(404).json({ error: 'User not found' });
|
|
720
|
+
res.json({ limits: row });
|
|
721
|
+
} catch (err) {
|
|
722
|
+
res.status(500).json({ error: String(err.message || err) });
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
644
726
|
router.put('/api/users/:id/rate-limits', requireAdminAuth, express.json(), (req, res) => {
|
|
645
727
|
const db = require('../db/database');
|
|
646
728
|
const { id } = req.params;
|
package/server/routes/runtime.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const express = require('express');
|
|
4
|
-
const { getAnalyticsConfig } = require('../config/analytics');
|
|
5
4
|
|
|
6
5
|
const router = express.Router();
|
|
7
6
|
|
|
8
7
|
router.get('/config', (req, res) => {
|
|
9
|
-
res.json({
|
|
10
|
-
analytics: getAnalyticsConfig(),
|
|
11
|
-
});
|
|
8
|
+
res.json({});
|
|
12
9
|
});
|
|
13
10
|
|
|
14
11
|
module.exports = router;
|
|
@@ -1215,18 +1215,20 @@ class AgentEngine {
|
|
|
1215
1215
|
const aiSettings = getAiSettings(userId, agentId);
|
|
1216
1216
|
|
|
1217
1217
|
const userLimits = db.prepare('SELECT rate_limit_4h, rate_limit_weekly FROM users WHERE id = ?').get(userId);
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1218
|
+
const globalLimit4h = process.env.NEOAGENT_RATE_LIMIT_4H ? parseInt(process.env.NEOAGENT_RATE_LIMIT_4H, 10) : null;
|
|
1219
|
+
const globalLimitWeekly = process.env.NEOAGENT_RATE_LIMIT_WEEKLY ? parseInt(process.env.NEOAGENT_RATE_LIMIT_WEEKLY, 10) : null;
|
|
1220
|
+
const effective4h = userLimits?.rate_limit_4h ?? globalLimit4h;
|
|
1221
|
+
const effectiveWeekly = userLimits?.rate_limit_weekly ?? globalLimitWeekly;
|
|
1222
|
+
if (effective4h) {
|
|
1223
|
+
const h4Tokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-4 hours')").get(userId).t;
|
|
1224
|
+
if (h4Tokens >= effective4h) {
|
|
1225
|
+
throw new Error(`Rate limit exceeded: You have used ${h4Tokens} tokens in the last 4 hours (limit: ${effective4h}).`);
|
|
1224
1226
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1227
|
+
}
|
|
1228
|
+
if (effectiveWeekly) {
|
|
1229
|
+
const weeklyTokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-7 days')").get(userId).t;
|
|
1230
|
+
if (weeklyTokens >= effectiveWeekly) {
|
|
1231
|
+
throw new Error(`Rate limit exceeded: You have used ${weeklyTokens} tokens in the last 7 days (limit: ${effectiveWeekly}).`);
|
|
1230
1232
|
}
|
|
1231
1233
|
}
|
|
1232
1234
|
|
|
@@ -430,8 +430,8 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
const
|
|
434
|
-
const
|
|
433
|
+
const globalDisabledStr = process.env.NEOAGENT_DISABLED_MODELS || '';
|
|
434
|
+
const globalDisabledSet = globalDisabledStr ? new Set(globalDisabledStr.split(',').map(s => s.trim()).filter(Boolean)) : null;
|
|
435
435
|
|
|
436
436
|
return all.map((model) => {
|
|
437
437
|
const provider = providerById.get(model.provider);
|
|
@@ -440,15 +440,21 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
440
440
|
const priceTier = model.provider === 'ollama'
|
|
441
441
|
? 'free'
|
|
442
442
|
: (model.priceTier ?? classifyPriceTier(model.id));
|
|
443
|
-
|
|
443
|
+
|
|
444
444
|
let available = provider?.available !== false;
|
|
445
|
-
if (
|
|
445
|
+
if (available && globalDisabledSet?.has(model.id)) {
|
|
446
446
|
available = false;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
const bareId = model.id.includes('/') ? model.id.slice(model.id.indexOf('/') + 1) : null;
|
|
450
|
+
const inputCostPerM = model.provider === 'ollama'
|
|
451
|
+
? 0
|
|
452
|
+
: (openrouterPricingCache.get(model.id) ?? (bareId ? openrouterPricingCache.get(bareId) : undefined) ?? null);
|
|
453
|
+
|
|
449
454
|
return {
|
|
450
455
|
...model,
|
|
451
456
|
priceTier,
|
|
457
|
+
inputCostPerM,
|
|
452
458
|
available,
|
|
453
459
|
providerStatus: provider?.status || 'unknown',
|
|
454
460
|
providerStatusLabel: provider?.statusLabel || 'Unknown'
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import 'package:mixpanel_flutter/mixpanel_flutter.dart';
|
|
2
|
-
|
|
3
|
-
class AppAnalytics {
|
|
4
|
-
Mixpanel? _mixpanel;
|
|
5
|
-
bool _initialized = false;
|
|
6
|
-
bool _enabled = false;
|
|
7
|
-
String? _currentToken;
|
|
8
|
-
bool _consentGranted = false;
|
|
9
|
-
final List<_QueuedEvent> _queue = <_QueuedEvent>[];
|
|
10
|
-
|
|
11
|
-
bool get enabled => _enabled && _consentGranted;
|
|
12
|
-
bool get consentGranted => _consentGranted;
|
|
13
|
-
bool get isConfigured => (_currentToken ?? '').isNotEmpty;
|
|
14
|
-
|
|
15
|
-
Future<void> initialize({
|
|
16
|
-
required String? token,
|
|
17
|
-
required bool consentGranted,
|
|
18
|
-
}) async {
|
|
19
|
-
final normalizedToken = token?.trim() ?? '';
|
|
20
|
-
final shouldEnable = normalizedToken.isNotEmpty;
|
|
21
|
-
if (_initialized &&
|
|
22
|
-
normalizedToken == _currentToken &&
|
|
23
|
-
shouldEnable == _enabled &&
|
|
24
|
-
consentGranted == _consentGranted) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
_consentGranted = consentGranted;
|
|
29
|
-
_initialized = false;
|
|
30
|
-
_enabled = false;
|
|
31
|
-
_currentToken = normalizedToken.isEmpty ? null : normalizedToken;
|
|
32
|
-
_mixpanel = null;
|
|
33
|
-
|
|
34
|
-
if (!shouldEnable) {
|
|
35
|
-
_queue.clear();
|
|
36
|
-
_initialized = true;
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!consentGranted) {
|
|
41
|
-
_queue.clear();
|
|
42
|
-
_initialized = true;
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
_mixpanel = await Mixpanel.init(
|
|
48
|
-
normalizedToken,
|
|
49
|
-
trackAutomaticEvents: false,
|
|
50
|
-
);
|
|
51
|
-
_enabled = true;
|
|
52
|
-
_initialized = true;
|
|
53
|
-
await _flushQueue();
|
|
54
|
-
} catch (_) {
|
|
55
|
-
_queue.clear();
|
|
56
|
-
_mixpanel = null;
|
|
57
|
-
_enabled = false;
|
|
58
|
-
_initialized = true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
Future<void> setConsentGranted(bool consentGranted) async {
|
|
63
|
-
if (_consentGranted == consentGranted && _initialized) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
_consentGranted = consentGranted;
|
|
68
|
-
if (!consentGranted) {
|
|
69
|
-
_mixpanel = null;
|
|
70
|
-
_enabled = false;
|
|
71
|
-
_initialized = true;
|
|
72
|
-
_queue.clear();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
await initialize(token: _currentToken, consentGranted: true);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
Future<void> track(
|
|
80
|
-
String eventName, {
|
|
81
|
-
Map<String, Object?> properties = const <String, Object?>{},
|
|
82
|
-
}) async {
|
|
83
|
-
final event = _QueuedEvent(
|
|
84
|
-
eventName: eventName,
|
|
85
|
-
properties: _cleanProperties(properties),
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
if (!_initialized) {
|
|
89
|
-
_queue.add(event);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!enabled || _mixpanel == null) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await _sendEvent(event);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
Future<void> trackAppOpened({
|
|
101
|
-
required String appMode,
|
|
102
|
-
required String platform,
|
|
103
|
-
required String backendMode,
|
|
104
|
-
required String selectedSection,
|
|
105
|
-
required String deploymentProfile,
|
|
106
|
-
required bool authenticated,
|
|
107
|
-
}) {
|
|
108
|
-
return track(
|
|
109
|
-
'app_opened',
|
|
110
|
-
properties: <String, Object?>{
|
|
111
|
-
'app_mode': appMode,
|
|
112
|
-
'platform': platform,
|
|
113
|
-
'backend_mode': backendMode,
|
|
114
|
-
'selected_section': selectedSection,
|
|
115
|
-
'deployment_profile': deploymentProfile,
|
|
116
|
-
'authenticated': authenticated,
|
|
117
|
-
},
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
Future<void> trackBackendUrlSaved({
|
|
122
|
-
required String backendMode,
|
|
123
|
-
}) {
|
|
124
|
-
return track(
|
|
125
|
-
'backend_url_saved',
|
|
126
|
-
properties: <String, Object?>{
|
|
127
|
-
'backend_mode': backendMode,
|
|
128
|
-
},
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
Future<void> trackSectionChanged({
|
|
133
|
-
required String section,
|
|
134
|
-
required String previousSection,
|
|
135
|
-
}) {
|
|
136
|
-
return track(
|
|
137
|
-
'section_changed',
|
|
138
|
-
properties: <String, Object?>{
|
|
139
|
-
'section': section,
|
|
140
|
-
'previous_section': previousSection,
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
Future<void> trackChatMessageSent({
|
|
146
|
-
required int length,
|
|
147
|
-
required bool steeringLiveRun,
|
|
148
|
-
}) {
|
|
149
|
-
return track(
|
|
150
|
-
'chat_message_sent',
|
|
151
|
-
properties: <String, Object?>{
|
|
152
|
-
'length': length,
|
|
153
|
-
'steering_live_run': steeringLiveRun,
|
|
154
|
-
},
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
Future<void> trackRecordingStarted({
|
|
159
|
-
required String kind,
|
|
160
|
-
}) {
|
|
161
|
-
return track(
|
|
162
|
-
'recording_started',
|
|
163
|
-
properties: <String, Object?>{
|
|
164
|
-
'kind': kind,
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
Future<void> trackRecordingStopped({
|
|
170
|
-
required String kind,
|
|
171
|
-
required String stopReason,
|
|
172
|
-
}) {
|
|
173
|
-
return track(
|
|
174
|
-
'recording_stopped',
|
|
175
|
-
properties: <String, Object?>{
|
|
176
|
-
'kind': kind,
|
|
177
|
-
'stop_reason': stopReason,
|
|
178
|
-
},
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
Future<void> trackAppUpdateCheck({
|
|
183
|
-
required bool silent,
|
|
184
|
-
}) {
|
|
185
|
-
return track(
|
|
186
|
-
'app_update_check',
|
|
187
|
-
properties: <String, Object?>{
|
|
188
|
-
'silent': silent,
|
|
189
|
-
},
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
Future<void> trackTaskRunRequested({
|
|
194
|
-
required int taskId,
|
|
195
|
-
}) {
|
|
196
|
-
return track(
|
|
197
|
-
'task_run_requested',
|
|
198
|
-
properties: <String, Object?>{
|
|
199
|
-
'task_id': taskId,
|
|
200
|
-
},
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
Future<void> trackWidgetRefreshRequested({
|
|
205
|
-
required bool all,
|
|
206
|
-
}) {
|
|
207
|
-
return track(
|
|
208
|
-
'widget_refresh_requested',
|
|
209
|
-
properties: <String, Object?>{
|
|
210
|
-
'all': all,
|
|
211
|
-
},
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
Future<void> trackAppUpdateTriggered() {
|
|
216
|
-
return track('app_update_triggered');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
Future<void> trackSignedIn({
|
|
220
|
-
required String authMethod,
|
|
221
|
-
required bool isRegistration,
|
|
222
|
-
}) {
|
|
223
|
-
return track(
|
|
224
|
-
'signed_in',
|
|
225
|
-
properties: <String, Object?>{
|
|
226
|
-
'auth_method': authMethod,
|
|
227
|
-
'is_registration': isRegistration,
|
|
228
|
-
},
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
Future<void> trackSignedOut() {
|
|
233
|
-
return track('signed_out');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
Future<void> trackOnboardingDismissed() {
|
|
237
|
-
return track('onboarding_dismissed');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
Future<void> dispose() async {
|
|
241
|
-
try {
|
|
242
|
-
await _flushQueue();
|
|
243
|
-
} catch (_) {}
|
|
244
|
-
_queue.clear();
|
|
245
|
-
_mixpanel = null;
|
|
246
|
-
_initialized = false;
|
|
247
|
-
_enabled = false;
|
|
248
|
-
_currentToken = null;
|
|
249
|
-
_consentGranted = false;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
Future<void> _flushQueue() async {
|
|
253
|
-
if (!enabled || _mixpanel == null || _queue.isEmpty) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
final pending = List<_QueuedEvent>.from(_queue);
|
|
258
|
-
_queue.clear();
|
|
259
|
-
for (final event in pending) {
|
|
260
|
-
await _sendEvent(event);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
Future<void> _sendEvent(_QueuedEvent event) async {
|
|
265
|
-
final mixpanel = _mixpanel;
|
|
266
|
-
if (mixpanel == null) {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
try {
|
|
271
|
-
await mixpanel.track(event.eventName, properties: event.properties);
|
|
272
|
-
} catch (_) {}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
Map<String, Object?> _cleanProperties(Map<String, Object?> properties) {
|
|
276
|
-
final cleaned = <String, Object?>{};
|
|
277
|
-
for (final entry in properties.entries) {
|
|
278
|
-
final value = entry.value;
|
|
279
|
-
if (value == null) continue;
|
|
280
|
-
cleaned[entry.key] = value;
|
|
281
|
-
}
|
|
282
|
-
return cleaned;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
class _QueuedEvent {
|
|
287
|
-
const _QueuedEvent({
|
|
288
|
-
required this.eventName,
|
|
289
|
-
required this.properties,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
final String eventName;
|
|
293
|
-
final Map<String, Object?> properties;
|
|
294
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const DEFAULT_MIXPANEL_TOKEN = '4a47ae6a05cf39a8faf0438a1200dde6';
|
|
4
|
-
|
|
5
|
-
function normalizeToken(value) {
|
|
6
|
-
return String(value || '').trim();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function resolveMixpanelToken() {
|
|
10
|
-
if (Object.prototype.hasOwnProperty.call(process.env, 'NEOAGENT_MIXPANEL_TOKEN')) {
|
|
11
|
-
return normalizeToken(process.env.NEOAGENT_MIXPANEL_TOKEN);
|
|
12
|
-
}
|
|
13
|
-
return normalizeToken(DEFAULT_MIXPANEL_TOKEN);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function getAnalyticsConfig() {
|
|
17
|
-
const mixpanelToken = resolveMixpanelToken();
|
|
18
|
-
return {
|
|
19
|
-
mixpanel: {
|
|
20
|
-
enabled: mixpanelToken.length > 0,
|
|
21
|
-
token: mixpanelToken || null,
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
module.exports = {
|
|
27
|
-
DEFAULT_MIXPANEL_TOKEN,
|
|
28
|
-
getAnalyticsConfig,
|
|
29
|
-
resolveMixpanelToken,
|
|
30
|
-
};
|