@harshankur/viewcounter 3.0.1 → 3.2.0
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 +50 -6
- package/README.md +444 -104
- package/admin/apple-touch-icon.png +0 -0
- package/admin/assets/world-map.json +1 -0
- package/admin/css/admin.css +2568 -0
- package/admin/favicon.ico +0 -0
- package/admin/favicon.svg +9 -0
- package/admin/icon-192.png +0 -0
- package/admin/icon-512.png +0 -0
- package/admin/index.html +95 -0
- package/admin/js/api.js +146 -0
- package/admin/js/appTabs.js +100 -0
- package/admin/js/charts.js +842 -0
- package/admin/js/clamp.js +41 -0
- package/admin/js/constants.js +239 -0
- package/admin/js/dataTable.js +478 -0
- package/admin/js/dom.js +83 -0
- package/admin/js/format.js +130 -0
- package/admin/js/i18n.js +80 -0
- package/admin/js/icons.js +168 -0
- package/admin/js/listbox.js +145 -0
- package/admin/js/logs.js +318 -0
- package/admin/js/main.js +399 -0
- package/admin/js/modal.js +171 -0
- package/admin/js/overview.js +905 -0
- package/admin/js/passwordPrompt.js +75 -0
- package/admin/js/table.js +94 -0
- package/admin/js/theme.js +72 -0
- package/admin/js/toast.js +47 -0
- package/admin/js/viewDialogs.js +224 -0
- package/admin/js/views.js +751 -0
- package/admin/locales/en.json +683 -0
- package/admin/site.webmanifest +20 -0
- package/config/index.js +122 -4
- package/constants.js +334 -3
- package/db/AdminRepository.js +488 -0
- package/db/DatabaseManager.js +148 -26
- package/db/LogRepository.js +354 -0
- package/db/adminSchema.js +329 -0
- package/db/adminSessionStore.js +104 -0
- package/db/analysis.js +479 -0
- package/db/rejectionCounter.js +117 -0
- package/db/retention.js +97 -0
- package/index.js +91 -24
- package/middleware/adminAuth.js +244 -0
- package/middleware/adminValidation.js +319 -0
- package/middleware/auth.js +2 -2
- package/middleware/security.js +26 -2
- package/middleware/validation.js +50 -2
- package/package.json +20 -10
- package/routes/admin.js +546 -0
- package/routes/analytics.js +207 -19
- package/tracker/tracker.js +191 -0
- package/utils/appIdUtils.js +1 -1
- package/utils/cookieUtils.js +47 -0
- package/utils/durationUtils.js +33 -0
- package/utils/errorUtils.js +39 -1
- package/utils/geoCity.js +87 -0
- package/utils/ipUtils.js +1 -1
- package/utils/privacyUtils.js +2 -2
- package/utils/referrerParser.js +23 -5
- package/utils/secretStore.js +1 -1
- package/utils/userAgentParser.js +52 -3
- package/utils/visitorContext.js +70 -0
package/db/DatabaseManager.js
CHANGED
|
@@ -9,19 +9,36 @@ const {
|
|
|
9
9
|
QUERY_LIMITS,
|
|
10
10
|
SERVER,
|
|
11
11
|
TOP_N_RESULTS,
|
|
12
|
+
TRACKING,
|
|
12
13
|
TREND_PERIOD,
|
|
14
|
+
VIEW_LOG_SOURCE,
|
|
13
15
|
} = require('../constants');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Every analytics read and the duplicate check see only live rows. A view an
|
|
19
|
+
* admin has moved to the trash no longer counts anywhere, and one that is
|
|
20
|
+
* restored counts again, without any stored aggregate needing to be rebuilt.
|
|
21
|
+
*/
|
|
22
|
+
const LIVE_ROW = 'deleted_at IS NULL';
|
|
14
23
|
const PrivacyUtils = require('../utils/privacyUtils');
|
|
15
24
|
const logger = require('../utils/logger');
|
|
16
25
|
const { getError, logWarning, ErrorType, WarningType } = require('../utils/errorUtils');
|
|
17
26
|
const { truncate } = require('../utils/stringUtils');
|
|
18
27
|
const { isValidAppId } = require('../utils/appIdUtils');
|
|
28
|
+
const {
|
|
29
|
+
NEW_TABLE_ADMIN_COLUMNS,
|
|
30
|
+
NEW_TABLE_ADMIN_INDEXES,
|
|
31
|
+
ensureLogTables,
|
|
32
|
+
migrateAppTable,
|
|
33
|
+
} = require('./adminSchema');
|
|
34
|
+
const LogRepository = require('./LogRepository');
|
|
35
|
+
const AdminRepository = require('./AdminRepository');
|
|
19
36
|
|
|
20
37
|
/**
|
|
21
38
|
* Columns returned for a session lookup.
|
|
22
39
|
*
|
|
23
40
|
* Deliberately explicit rather than `SELECT *`. The previous wildcard returned
|
|
24
|
-
* `visitor_hash`
|
|
41
|
+
* `visitor_hash` (the pseudonymous visitor identifier itself) to any caller
|
|
25
42
|
* of the sessions endpoint.
|
|
26
43
|
*/
|
|
27
44
|
const SESSION_COLUMNS = [
|
|
@@ -49,7 +66,7 @@ const SESSION_COLUMNS = [
|
|
|
49
66
|
*
|
|
50
67
|
* `id` is a generated UUID and is the row's identity; `app_id` is a uniqueness
|
|
51
68
|
* *constraint*, not an identity (CODE_STANDARDS.md §8). The distinction matters
|
|
52
|
-
* the first time an app is renamed, or deleted and a later one reuses the name
|
|
69
|
+
* the first time an app is renamed, or deleted and a later one reuses the name:
|
|
53
70
|
* with the natural key as the primary key, anything referencing the old row
|
|
54
71
|
* would silently re-point at the new one.
|
|
55
72
|
*/
|
|
@@ -66,7 +83,7 @@ const APP_REGISTRY_DDL = `
|
|
|
66
83
|
* DDL for one app's event table.
|
|
67
84
|
*
|
|
68
85
|
* `appId` is interpolated because MySQL cannot bind an identifier as a
|
|
69
|
-
* parameter. Every caller must have passed it through `isValidAppId` first
|
|
86
|
+
* parameter. Every caller must have passed it through `isValidAppId` first;
|
|
70
87
|
* the assertion below is the backstop, not the primary gate.
|
|
71
88
|
*
|
|
72
89
|
* @param {string} appId
|
|
@@ -99,6 +116,7 @@ function appTableDDL(appId) {
|
|
|
99
116
|
\`event_type\` VARCHAR(${FIELD_MAX_LENGTH.EVENT_TYPE}) DEFAULT '${EVENT_TYPE.PAGEVIEW}',
|
|
100
117
|
\`event_data\` JSON DEFAULT NULL,
|
|
101
118
|
\`is_unique\` TINYINT(1) DEFAULT 1,
|
|
119
|
+
${NEW_TABLE_ADMIN_COLUMNS.join(',\n ')},
|
|
102
120
|
INDEX \`idx_timestamp\` (\`timestamp\`),
|
|
103
121
|
INDEX \`idx_visitor_timestamp\` (\`visitor_hash\`, \`timestamp\`),
|
|
104
122
|
INDEX \`idx_masked_ip\` (\`masked_ip\`),
|
|
@@ -112,7 +130,8 @@ function appTableDDL(appId) {
|
|
|
112
130
|
INDEX \`idx_device_type\` (\`device_type\`),
|
|
113
131
|
INDEX \`idx_session_id\` (\`session_id\`),
|
|
114
132
|
INDEX \`idx_event_type\` (\`event_type\`),
|
|
115
|
-
INDEX \`idx_is_unique\` (\`is_unique\`)
|
|
133
|
+
INDEX \`idx_is_unique\` (\`is_unique\`),
|
|
134
|
+
${NEW_TABLE_ADMIN_INDEXES.join(',\n ')}
|
|
116
135
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
117
136
|
`;
|
|
118
137
|
}
|
|
@@ -123,7 +142,7 @@ function appTableDDL(appId) {
|
|
|
123
142
|
* `mysql2/promise`'s pool emits the RAW callback-style connection on its
|
|
124
143
|
* `connection` event, not the promise-wrapped one. Its `query()` returns a
|
|
125
144
|
* `Query`, and mysql2 deliberately makes `.then()`/`.catch()` on a `Query`
|
|
126
|
-
* throw
|
|
145
|
+
* throw, so treating it as a promise crashes the process on the very first
|
|
127
146
|
* database connection. The callback form is the correct API for that object.
|
|
128
147
|
*
|
|
129
148
|
* Failure is swallowed on purpose: MariaDB and MySQL < 5.7.8 have no
|
|
@@ -161,6 +180,29 @@ class DatabaseManager {
|
|
|
161
180
|
this.config = config;
|
|
162
181
|
this.pool = null;
|
|
163
182
|
this.mode = config.mode || 'connect';
|
|
183
|
+
this.logs = new LogRepository(this);
|
|
184
|
+
this.admin = new AdminRepository(this);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Bring the service's own tables and every app table up to the current
|
|
189
|
+
* schema. Idempotent, and run on every start, so an upgrade needs no manual
|
|
190
|
+
* migration step. Runs in `connect` mode too: the admin columns and log
|
|
191
|
+
* tables are the service's bookkeeping, like the app registry.
|
|
192
|
+
*
|
|
193
|
+
* @param {string[]} appIds
|
|
194
|
+
* @throws {Error} ErrorType.MIGRATION_FAILED
|
|
195
|
+
*/
|
|
196
|
+
async migrate(appIds = []) {
|
|
197
|
+
this.assertReady();
|
|
198
|
+
await ensureLogTables(this.pool);
|
|
199
|
+
|
|
200
|
+
for (const appId of appIds) {
|
|
201
|
+
const result = await migrateAppTable(this.pool, appId);
|
|
202
|
+
if (result.backfilled > 0) {
|
|
203
|
+
logger.info(`Assigned public IDs to ${result.backfilled} existing row(s) in '${appId}'`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
164
206
|
}
|
|
165
207
|
|
|
166
208
|
/** @throws {Error} when a query is attempted before initialize() */
|
|
@@ -171,7 +213,14 @@ class DatabaseManager {
|
|
|
171
213
|
}
|
|
172
214
|
|
|
173
215
|
/**
|
|
174
|
-
*
|
|
216
|
+
* Connect, create the schema in create mode, and bring the given apps'
|
|
217
|
+
* tables up to the current schema in either mode.
|
|
218
|
+
*
|
|
219
|
+
* The migration belongs here, not only at server startup: every query this
|
|
220
|
+
* manager runs assumes the current schema (public_id, deleted_at, the log
|
|
221
|
+
* tables), so an application that mounts the routers and calls only
|
|
222
|
+
* initialize() must still get tables those queries can run against.
|
|
223
|
+
* Idempotent: a table already in shape is read and left alone.
|
|
175
224
|
*/
|
|
176
225
|
async initialize(allowedAppIds = []) {
|
|
177
226
|
try {
|
|
@@ -206,8 +255,6 @@ class DatabaseManager {
|
|
|
206
255
|
|
|
207
256
|
await this.pool.query('SELECT 1');
|
|
208
257
|
logger.info(`Database connected (mode: ${this.mode})`);
|
|
209
|
-
|
|
210
|
-
return true;
|
|
211
258
|
} catch (cause) {
|
|
212
259
|
throw getError(ErrorType.DATABASE_CONNECTION_FAILED, {
|
|
213
260
|
host: this.config.host,
|
|
@@ -216,6 +263,11 @@ class DatabaseManager {
|
|
|
216
263
|
cause: cause.message,
|
|
217
264
|
});
|
|
218
265
|
}
|
|
266
|
+
|
|
267
|
+
// Outside the try: a failed migration is reported as MIGRATION_FAILED
|
|
268
|
+
// naming the table, not as a connection failure.
|
|
269
|
+
await this.migrate(allowedAppIds);
|
|
270
|
+
return true;
|
|
219
271
|
}
|
|
220
272
|
|
|
221
273
|
/**
|
|
@@ -296,7 +348,7 @@ class DatabaseManager {
|
|
|
296
348
|
/**
|
|
297
349
|
* Provision a new app: validate, create its table, record it.
|
|
298
350
|
*
|
|
299
|
-
* Idempotent
|
|
351
|
+
* Idempotent: re-registering an existing app is a no-op rather than an
|
|
300
352
|
* error, so a retried provisioning call cannot fail halfway.
|
|
301
353
|
*
|
|
302
354
|
* @param {string} appId
|
|
@@ -320,8 +372,10 @@ class DatabaseManager {
|
|
|
320
372
|
);
|
|
321
373
|
|
|
322
374
|
// The table is (re)created regardless, so an app registered before its
|
|
323
|
-
// table existed still converges to a working state.
|
|
375
|
+
// table existed still converges to a working state. A table that
|
|
376
|
+
// already existed in an older shape is migrated in place.
|
|
324
377
|
await this.pool.query(appTableDDL(appId));
|
|
378
|
+
await migrateAppTable(this.pool, appId);
|
|
325
379
|
|
|
326
380
|
if (existing.length > 0) {
|
|
327
381
|
logWarning(WarningType.APP_ALREADY_REGISTERED, { appId });
|
|
@@ -386,9 +440,19 @@ class DatabaseManager {
|
|
|
386
440
|
sessionId,
|
|
387
441
|
eventType = EVENT_TYPE.PAGEVIEW,
|
|
388
442
|
eventData,
|
|
443
|
+
hostname = null,
|
|
444
|
+
language = null,
|
|
445
|
+
utmSource = null,
|
|
446
|
+
utmMedium = null,
|
|
447
|
+
utmCampaign = null,
|
|
448
|
+
utmTerm = null,
|
|
449
|
+
utmContent = null,
|
|
450
|
+
region = null,
|
|
451
|
+
city = null,
|
|
389
452
|
uniqueWindowHours = SERVER.DEFAULT_UNIQUE_VISITOR_WINDOW_HOURS,
|
|
390
453
|
userAgent = '',
|
|
391
454
|
visitorSecret,
|
|
455
|
+
source = VIEW_LOG_SOURCE.REGISTER_VIEW,
|
|
392
456
|
} = data;
|
|
393
457
|
|
|
394
458
|
// Privacy boundary. Neither the raw IP nor the raw User-Agent is bound
|
|
@@ -407,6 +471,7 @@ class DatabaseManager {
|
|
|
407
471
|
const [existing] = await this.pool.query(
|
|
408
472
|
`SELECT id FROM \`${appId}\`
|
|
409
473
|
WHERE visitor_hash = ? AND event_type = ? AND timestamp > DATE_SUB(NOW(), INTERVAL ? HOUR)
|
|
474
|
+
AND ${LIVE_ROW}
|
|
410
475
|
LIMIT 1`,
|
|
411
476
|
[hashedVisitor, EVENT_TYPE.PAGEVIEW, uniqueWindowHours]
|
|
412
477
|
);
|
|
@@ -416,15 +481,21 @@ class DatabaseManager {
|
|
|
416
481
|
}
|
|
417
482
|
}
|
|
418
483
|
|
|
484
|
+
const publicId = crypto.randomUUID();
|
|
485
|
+
const storedEventType = truncate(eventType, FIELD_MAX_LENGTH.EVENT_TYPE);
|
|
486
|
+
|
|
419
487
|
const [result] = await this.pool.query(
|
|
420
488
|
`INSERT INTO \`${appId}\` (
|
|
421
|
-
masked_ip, visitor_hash, country, timestamp, devicesize,
|
|
489
|
+
public_id, masked_ip, visitor_hash, country, timestamp, devicesize,
|
|
422
490
|
page_path, page_title,
|
|
423
491
|
referrer, referrer_domain, source_type,
|
|
424
492
|
browser, browser_version, os, os_version, device_type,
|
|
425
|
-
session_id, event_type, event_data, is_unique
|
|
426
|
-
|
|
493
|
+
session_id, event_type, event_data, is_unique,
|
|
494
|
+
hostname, language, utm_source, utm_medium, utm_campaign, utm_term, utm_content,
|
|
495
|
+
region, city
|
|
496
|
+
) VALUES (?, ?, ?, ?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
427
497
|
[
|
|
498
|
+
publicId,
|
|
428
499
|
truncate(maskedIp, FIELD_MAX_LENGTH.MASKED_IP),
|
|
429
500
|
hashedVisitor,
|
|
430
501
|
truncate(country, FIELD_MAX_LENGTH.COUNTRY),
|
|
@@ -440,19 +511,66 @@ class DatabaseManager {
|
|
|
440
511
|
truncate(osVersion, FIELD_MAX_LENGTH.OS_VERSION),
|
|
441
512
|
truncate(deviceType, FIELD_MAX_LENGTH.DEVICE_TYPE),
|
|
442
513
|
truncate(sessionId, FIELD_MAX_LENGTH.SESSION_ID),
|
|
443
|
-
|
|
514
|
+
storedEventType,
|
|
444
515
|
eventData ? JSON.stringify(eventData) : null,
|
|
445
516
|
isUnique,
|
|
517
|
+
truncate(hostname, FIELD_MAX_LENGTH.HOSTNAME),
|
|
518
|
+
truncate(language, FIELD_MAX_LENGTH.LANGUAGE),
|
|
519
|
+
truncate(utmSource, FIELD_MAX_LENGTH.UTM),
|
|
520
|
+
truncate(utmMedium, FIELD_MAX_LENGTH.UTM),
|
|
521
|
+
truncate(utmCampaign, FIELD_MAX_LENGTH.UTM),
|
|
522
|
+
truncate(utmTerm, FIELD_MAX_LENGTH.UTM),
|
|
523
|
+
truncate(utmContent, FIELD_MAX_LENGTH.UTM),
|
|
524
|
+
truncate(region, FIELD_MAX_LENGTH.REGION),
|
|
525
|
+
truncate(city, FIELD_MAX_LENGTH.CITY),
|
|
446
526
|
]
|
|
447
527
|
);
|
|
448
528
|
|
|
529
|
+
// The view register log. Written after the row, and never able to fail
|
|
530
|
+
// the request: the view is already stored.
|
|
531
|
+
await this.logs.writeViewLog({
|
|
532
|
+
appId,
|
|
533
|
+
source,
|
|
534
|
+
viewId: publicId,
|
|
535
|
+
eventType: storedEventType,
|
|
536
|
+
isUnique: isUnique === 1,
|
|
537
|
+
hostname,
|
|
538
|
+
});
|
|
539
|
+
|
|
449
540
|
return {
|
|
450
541
|
duplicate: isUnique === 0,
|
|
451
542
|
insertId: result.insertId,
|
|
543
|
+
publicId,
|
|
452
544
|
isUnique: isUnique === 1,
|
|
453
545
|
};
|
|
454
546
|
}
|
|
455
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Record how long a view's page was visible and how far it was scrolled.
|
|
550
|
+
*
|
|
551
|
+
* A page reports this when it is hidden or left, possibly more than once
|
|
552
|
+
* (hidden, shown again, then left), each time with its running total, so
|
|
553
|
+
* the larger value always wins. Only a live view from the last
|
|
554
|
+
* TRACKING.ENGAGE_WINDOW_HOURS is updated: an old or trashed view keeps
|
|
555
|
+
* what it had.
|
|
556
|
+
*
|
|
557
|
+
* @param {string} appId already validated
|
|
558
|
+
* @param {{ viewId: string, engagedMs: number, scrollDepth: number }} engagement
|
|
559
|
+
* @returns {Promise<boolean>} whether a view was updated
|
|
560
|
+
*/
|
|
561
|
+
async addEngagement(appId, { viewId, engagedMs, scrollDepth }) {
|
|
562
|
+
this.assertReady();
|
|
563
|
+
const [result] = await this.pool.query(
|
|
564
|
+
`UPDATE \`${appId}\`
|
|
565
|
+
SET engaged_ms = GREATEST(COALESCE(engaged_ms, 0), ?),
|
|
566
|
+
scroll_depth = GREATEST(COALESCE(scroll_depth, 0), ?)
|
|
567
|
+
WHERE public_id = ? AND ${LIVE_ROW}
|
|
568
|
+
AND timestamp > DATE_SUB(NOW(), INTERVAL ? HOUR)`,
|
|
569
|
+
[engagedMs, scrollDepth, viewId, TRACKING.ENGAGE_WINDOW_HOURS]
|
|
570
|
+
);
|
|
571
|
+
return result.affectedRows > 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
456
574
|
/**
|
|
457
575
|
* Register a view (backward compatible wrapper)
|
|
458
576
|
*/
|
|
@@ -477,14 +595,15 @@ class DatabaseManager {
|
|
|
477
595
|
COUNT(*) as total_views,
|
|
478
596
|
SUM(CASE WHEN is_unique = 1 THEN 1 ELSE 0 END) as unique_views,
|
|
479
597
|
COUNT(DISTINCT visitor_hash) as unique_visitors
|
|
480
|
-
FROM \`${appId}
|
|
598
|
+
FROM \`${appId}\`
|
|
599
|
+
WHERE ${LIVE_ROW}`
|
|
481
600
|
);
|
|
482
601
|
|
|
483
602
|
const stats = totalStats[0];
|
|
484
603
|
|
|
485
604
|
const [byCountry] = await this.pool.query(
|
|
486
605
|
`SELECT country, COUNT(*) as count FROM \`${appId}\`
|
|
487
|
-
WHERE country IS NOT NULL
|
|
606
|
+
WHERE country IS NOT NULL AND ${LIVE_ROW}
|
|
488
607
|
GROUP BY country
|
|
489
608
|
ORDER BY count DESC
|
|
490
609
|
LIMIT ?`,
|
|
@@ -493,13 +612,14 @@ class DatabaseManager {
|
|
|
493
612
|
|
|
494
613
|
const [byDevice] = await this.pool.query(
|
|
495
614
|
`SELECT devicesize, COUNT(*) as count FROM \`${appId}\`
|
|
615
|
+
WHERE ${LIVE_ROW}
|
|
496
616
|
GROUP BY devicesize
|
|
497
617
|
ORDER BY count DESC`
|
|
498
618
|
);
|
|
499
619
|
|
|
500
620
|
const [recent] = await this.pool.query(
|
|
501
621
|
`SELECT COUNT(*) as count FROM \`${appId}\`
|
|
502
|
-
WHERE timestamp > DATE_SUB(NOW(), INTERVAL ? HOUR)`,
|
|
622
|
+
WHERE timestamp > DATE_SUB(NOW(), INTERVAL ? HOUR) AND ${LIVE_ROW}`,
|
|
503
623
|
[SERVER.DEFAULT_UNIQUE_VISITOR_WINDOW_HOURS]
|
|
504
624
|
);
|
|
505
625
|
|
|
@@ -522,13 +642,14 @@ class DatabaseManager {
|
|
|
522
642
|
const [views] = await this.pool.query(
|
|
523
643
|
`SELECT masked_ip, country, timestamp, devicesize
|
|
524
644
|
FROM \`${appId}\`
|
|
645
|
+
WHERE ${LIVE_ROW}
|
|
525
646
|
ORDER BY timestamp DESC
|
|
526
647
|
LIMIT ? OFFSET ?`,
|
|
527
648
|
[limit, offset]
|
|
528
649
|
);
|
|
529
650
|
|
|
530
651
|
const [total] = await this.pool.query(
|
|
531
|
-
`SELECT COUNT(*) as count FROM \`${appId}
|
|
652
|
+
`SELECT COUNT(*) as count FROM \`${appId}\` WHERE ${LIVE_ROW}`
|
|
532
653
|
);
|
|
533
654
|
|
|
534
655
|
return {
|
|
@@ -560,7 +681,7 @@ class DatabaseManager {
|
|
|
560
681
|
const [trends] = await this.pool.query(
|
|
561
682
|
`SELECT ${groupBy} as period, COUNT(*) as count
|
|
562
683
|
FROM \`${appId}\`
|
|
563
|
-
WHERE timestamp > DATE_SUB(NOW(), INTERVAL ? DAY)
|
|
684
|
+
WHERE timestamp > DATE_SUB(NOW(), INTERVAL ? DAY) AND ${LIVE_ROW}
|
|
564
685
|
GROUP BY period
|
|
565
686
|
ORDER BY period ASC`,
|
|
566
687
|
[days]
|
|
@@ -578,7 +699,7 @@ class DatabaseManager {
|
|
|
578
699
|
const [bySource] = await this.pool.query(
|
|
579
700
|
`SELECT source_type, COUNT(*) as count
|
|
580
701
|
FROM \`${appId}\`
|
|
581
|
-
WHERE source_type IS NOT NULL
|
|
702
|
+
WHERE source_type IS NOT NULL AND ${LIVE_ROW}
|
|
582
703
|
GROUP BY source_type
|
|
583
704
|
ORDER BY count DESC`
|
|
584
705
|
);
|
|
@@ -586,7 +707,7 @@ class DatabaseManager {
|
|
|
586
707
|
const [byDomain] = await this.pool.query(
|
|
587
708
|
`SELECT referrer_domain, COUNT(*) as count
|
|
588
709
|
FROM \`${appId}\`
|
|
589
|
-
WHERE referrer_domain IS NOT NULL
|
|
710
|
+
WHERE referrer_domain IS NOT NULL AND ${LIVE_ROW}
|
|
590
711
|
GROUP BY referrer_domain
|
|
591
712
|
ORDER BY count DESC
|
|
592
713
|
LIMIT ?`,
|
|
@@ -605,7 +726,7 @@ class DatabaseManager {
|
|
|
605
726
|
const [byBrowser] = await this.pool.query(
|
|
606
727
|
`SELECT browser, COUNT(*) as count
|
|
607
728
|
FROM \`${appId}\`
|
|
608
|
-
WHERE browser IS NOT NULL
|
|
729
|
+
WHERE browser IS NOT NULL AND ${LIVE_ROW}
|
|
609
730
|
GROUP BY browser
|
|
610
731
|
ORDER BY count DESC
|
|
611
732
|
LIMIT ?`,
|
|
@@ -615,7 +736,7 @@ class DatabaseManager {
|
|
|
615
736
|
const [byOS] = await this.pool.query(
|
|
616
737
|
`SELECT os, COUNT(*) as count
|
|
617
738
|
FROM \`${appId}\`
|
|
618
|
-
WHERE os IS NOT NULL
|
|
739
|
+
WHERE os IS NOT NULL AND ${LIVE_ROW}
|
|
619
740
|
GROUP BY os
|
|
620
741
|
ORDER BY count DESC
|
|
621
742
|
LIMIT ?`,
|
|
@@ -625,7 +746,7 @@ class DatabaseManager {
|
|
|
625
746
|
const [byDeviceType] = await this.pool.query(
|
|
626
747
|
`SELECT device_type, COUNT(*) as count
|
|
627
748
|
FROM \`${appId}\`
|
|
628
|
-
WHERE device_type IS NOT NULL
|
|
749
|
+
WHERE device_type IS NOT NULL AND ${LIVE_ROW}
|
|
629
750
|
GROUP BY device_type
|
|
630
751
|
ORDER BY count DESC`
|
|
631
752
|
);
|
|
@@ -642,7 +763,7 @@ class DatabaseManager {
|
|
|
642
763
|
const [pages] = await this.pool.query(
|
|
643
764
|
`SELECT page_path, page_title, COUNT(*) as views
|
|
644
765
|
FROM \`${appId}\`
|
|
645
|
-
WHERE page_path IS NOT NULL
|
|
766
|
+
WHERE page_path IS NOT NULL AND ${LIVE_ROW}
|
|
646
767
|
GROUP BY page_path, page_title
|
|
647
768
|
ORDER BY views DESC
|
|
648
769
|
LIMIT ?`,
|
|
@@ -662,7 +783,7 @@ class DatabaseManager {
|
|
|
662
783
|
const [events] = await this.pool.query(
|
|
663
784
|
`SELECT ${SESSION_COLUMNS}
|
|
664
785
|
FROM \`${appId}\`
|
|
665
|
-
WHERE session_id = ?
|
|
786
|
+
WHERE session_id = ? AND ${LIVE_ROW}
|
|
666
787
|
ORDER BY timestamp ASC`,
|
|
667
788
|
[sessionId]
|
|
668
789
|
);
|
|
@@ -702,3 +823,4 @@ module.exports = DatabaseManager;
|
|
|
702
823
|
module.exports.SESSION_COLUMNS = SESSION_COLUMNS;
|
|
703
824
|
module.exports.appTableDDL = appTableDDL;
|
|
704
825
|
module.exports.APP_REGISTRY_DDL = APP_REGISTRY_DDL;
|
|
826
|
+
module.exports.LIVE_ROW = LIVE_ROW;
|