diff --git a/src/queries.ts b/src/queries.ts index 23bf7788..5a3162b8 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -116,12 +116,12 @@ export function canonicalEvents(params: DesktopQueryParams | AndroidQueryParams) return [ // Fetch window/app events - `events = flood(query_bucket("${bid_window}"));`, + `events = flood(query_bucket(find_bucket("${bid_window}")));`, // On Android, merge events to avoid overload of events isAndroidParams(params) ? 'events = merge_events_by_keys(events, ["app"]);' : '', // Fetch not-afk events isDesktopParams(params) - ? `not_afk = flood(query_bucket("${params.bid_afk}")); + ? `not_afk = flood(query_bucket(find_bucket("${params.bid_afk}"))); not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);` + (always_active_pattern_str ? `not_treat_as_afk = filter_keyvals_regex(events, "app", "${always_active_pattern_str}"); @@ -382,16 +382,15 @@ export function multideviceQuery(params: MultiQueryParams): string[] { "cat_events": cat_events, "active_events": not_afk, "duration": duration - }, + } };` ); } export function editorActivityQuery(editorbuckets: string[]): string[] { let q = ['events = [];']; - for (let editorbucket of editorbuckets) { - editorbucket = escape_doublequote(editorbucket); - q.push('events = concat(events, flood(query_bucket("' + editorbucket + '")));'); + for (const editorbucket of editorbuckets) { + q.push(`events = concat(events, flood(query_bucket("${escape_doublequote(editorbucket)}")));`); } q = q.concat([ 'files = sort_by_duration(merge_events_by_keys(events, ["file", "language"]));', @@ -408,15 +407,18 @@ export function editorActivityQuery(editorbuckets: string[]): string[] { // Returns a query that yields a single event with the duration set to // the sum of all non-afk time in the queried period -export function activityQuery(afkbucket: string): string[] { - afkbucket = escape_doublequote(afkbucket); - return [ - 'afkbucket = "' + afkbucket + '";', - 'not_afk = flood(query_bucket(afkbucket));', - 'not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);', - 'not_afk = merge_events_by_keys(not_afk, ["status"]);', - 'RETURN = not_afk;', - ]; +// TODO: Would ideally account for `filter_afk` and `always_active_pattern` +export function activityQuery(afkbuckets: string[]): string[] { + let q = ['not_afk = [];']; + for (const afkbucket of afkbuckets) { + q = q.concat([ + `not_afk_curr = query_bucket("${escape_doublequote(afkbucket)}");`, + `not_afk_curr = filter_keyvals(not_afk_curr, "status", ["not-afk"]);`, + `not_afk = union_no_overlap(not_afk, not_afk_curr);`, + ]); + } + q = q.concat(['not_afk = merge_events_by_keys(not_afk, ["status"]);', 'RETURN = not_afk;']); + return q; } // Equivalent function to activityQuery, but for Android (which doesn't have an afk bucket) diff --git a/src/stores/activity.ts b/src/stores/activity.ts index 95305090..eeaeb623 100644 --- a/src/stores/activity.ts +++ b/src/stores/activity.ts @@ -394,11 +394,30 @@ export const useActivityStore = defineStore('activity', { this.query_editor_completed(data[0]); }, - async query_active_history({ timeperiod }: QueryOptions) { + async query_active_history({ timeperiod, ...query_options }: QueryOptions) { + const settingsStore = useSettingsStore(); + const bucketsStore = useBucketsStore(); const periods = timeperiodStrsAroundTimeperiod(timeperiod).filter(tp_str => { return !_.includes(this.active.history, tp_str); }); - const data = await getClient().query(periods, queries.activityQuery(this.buckets.afk[0])); + let afk_buckets: string[] = []; + if (settingsStore.useMultidevice) { + // get all hostnames that qualify for the multidevice query + const hostnames = bucketsStore.hosts.filter( + // require that the host has afk buckets, + // and that the host is not a fakedata host, + // unless we're explicitly querying fakedata + host => + host && + bucketsStore.bucketsAFK(host).length > 0 && + (!host.startsWith('fakedata') || query_options.host.startsWith('fakedata')) + ); + // get all afk buckets for all hosts + afk_buckets = _.flatten(hostnames.map(bucketsStore.bucketsAFK)); + } else { + afk_buckets = [this.buckets.afk[0]]; + } + const data = await getClient().query(periods, queries.activityQuery(afk_buckets)); const active_history = _.zipObject( periods, _.map(data, pair => _.filter(pair, e => e.data.status == 'not-afk')) diff --git a/test/unit/__snapshots__/queries.test.node.js.snap b/test/unit/__snapshots__/queries.test.node.js.snap index 221c6b9d..1c0278d9 100644 --- a/test/unit/__snapshots__/queries.test.node.js.snap +++ b/test/unit/__snapshots__/queries.test.node.js.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`generate fullDesktopQuery 1`] = ` -"events = flood(query_bucket("")); -not_afk = flood(query_bucket("")); +"events = flood(query_bucket(find_bucket("aw-watcher-window_testhost"))); +not_afk = flood(query_bucket(find_bucket("aw-watcher-afk_testhost"))); not_afk = filter_keyvals(not_afk, "status", ["not-afk"]); not_treat_as_afk = filter_keyvals_regex(events, "app", "meow|nyaan|specials: \\w(\\\\)"); not_afk = period_union(not_afk, not_treat_as_afk); @@ -43,3 +43,12 @@ RETURN = { } };" `; + +exports[`generate fullDesktopQuery 2`] = ` +"not_afk = []; +not_afk_curr = query_bucket("aw-watcher-afk_testhost"); +not_afk_curr = filter_keyvals(not_afk_curr, "status", ["not-afk"]); +not_afk = union_no_overlap(not_afk, not_afk_curr); +not_afk = merge_events_by_keys(not_afk, ["status"]); +RETURN = not_afk;" +`; diff --git a/test/unit/queries.test.node.js b/test/unit/queries.test.node.js index c9530610..eb48f35c 100644 --- a/test/unit/queries.test.node.js +++ b/test/unit/queries.test.node.js @@ -1,28 +1,49 @@ const queries = require('~/queries'); -test('generate fullDesktopQuery', () => { - const bid_window = ''; - const bid_afk = ''; - const bid_browsers = []; - const filter_afk = true; - const categories = []; - const filter_categories = true; - const include_audible = true; - const always_active_pattern = /meow|nyaan|specials: \w(\\)/.toString().substring(1).slice(0, -1); - const query_lines = queries.fullDesktopQuery({ - bid_window, - bid_afk, - bid_browsers, - filter_afk, - categories, - filter_categories, - include_audible, - always_active_pattern, - }); +// test data +const hostname = 'testhost'; +const bid_window = 'aw-watcher-window_' + hostname; +const bid_afk = 'aw-watcher-afk_' + hostname; +const bid_browsers = []; +const filter_afk = true; +const always_active_pattern = /meow|nyaan|specials: \w(\\)/.toString().substring(1).slice(0, -1); +const queryParams = { + bid_window, + bid_afk, + bid_browsers, + filter_afk, + categories: [], + filter_categories: true, + include_audible: true, + always_active_pattern, +}; + +function expectBracketsClosed(query) { + // Checks that there are matching parens, brackets, braces, etc + // Doesn't actually check placement, just matching open/closed count. + + // parens + const openParens = query.match(/\(/g); + const closeParens = query.match(/\)/g); + expect(openParens && openParens.length).toEqual(closeParens && closeParens.length); + + // brackets + const openBrackets = query.match(/\[/g); + const closeBrackets = query.match(/\]/g); + expect(openBrackets && openBrackets.length).toEqual(closeBrackets && closeBrackets.length); - // join query lines into a single string - const query = query_lines.join('\n'); + // braces + const openBraces = query.match(/\{/g); + const closeBraces = query.match(/\}/g); + expect(openBraces && openBraces.length).toEqual(closeBraces && closeBraces.length); +} + +test('generate fullDesktopQuery', () => { + let query = queries.fullDesktopQuery(queryParams).join('\n'); + expect(query).toMatchSnapshot(); + expectBracketsClosed(query); - // check that query_str is well formatted + query = queries.activityQuery([bid_afk]).join('\n'); expect(query).toMatchSnapshot(); + expectBracketsClosed(query); });