blob: 3bff346859ac3698dbeb7f89ad63b231c148abae [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {HistoryEmbeddingsUserActions, QUERY_RESULT_MINIMUM_AGE} from 'chrome://resources/cr_components/history/constants.js';
import type {QueryResult, QueryState} from 'chrome://resources/cr_components/history/history.mojom-webui.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {CrLitElement} from 'chrome://resources/lit/v3_0/lit.rollup.js';
import {BrowserServiceImpl} from './browser_service.js';
import {RESULTS_PER_PAGE} from './constants.js';
// Converts a JS Date object to a human readable string in the format of
// YYYY-MM-DD for the query.
export function convertDateToQueryValue(date: Date) {
const fullYear = date.getFullYear();
const month = date.getMonth() + 1; /* Month is 0-indexed. */
const day = date.getDate();
function twoDigits(value: number): string {
return value >= 10 ? `${value}` : `0${value}`;
}
return `${fullYear}-${twoDigits(month)}-${twoDigits(day)}`;
}
declare global {
interface HTMLElementTagNameMap {
'history-query-manager': HistoryQueryManagerElement;
}
}
export class HistoryQueryManagerElement extends CrLitElement {
static get is() {
return 'history-query-manager';
}
static get template() {
return null;
}
static override get properties() {
return {
queryState: {
type: Object,
notify: true,
},
queryResult: {
type: Object,
},
};
}
accessor queryState: QueryState;
accessor queryResult: QueryResult;
private eventTracker_: EventTracker = new EventTracker();
/**
* When this is non-null, that means there's a QueryResult that's pending
* metrics logging since this debouncer timestamp. The debouncing is needed
* because queries are issued as the user types, and we want to skip logging
* these trivial queries the user typed through.
*/
private resultPendingMetricsTimestamp_: number|null = null;
constructor() {
super();
this.queryState = {
// Whether the most recent query was incremental.
incremental: false,
// A query is initiated by page load.
querying: true,
searchTerm: '',
after: '',
};
}
override connectedCallback() {
super.connectedCallback();
this.eventTracker_.add(
document, 'change-query', this.onChangeQuery_.bind(this));
this.eventTracker_.add(
document, 'query-history', this.onQueryHistory_.bind(this));
this.eventTracker_.add(document, 'visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flushDebouncedQueryResultMetric_();
}
});
}
override disconnectedCallback() {
super.disconnectedCallback();
this.flushDebouncedQueryResultMetric_();
this.eventTracker_.removeAll();
}
initialize() {
this.queryHistory_(false /* incremental */);
}
private queryHistory_(incremental: boolean) {
this.queryState = {
...this.queryState,
querying: true,
incremental: incremental,
};
let afterTimestamp;
if (loadTimeData.getBoolean('enableHistoryEmbeddings') &&
this.queryState.after) {
const afterDate = new Date(this.queryState.after);
afterDate.setHours(0, 0, 0, 0);
afterTimestamp = afterDate.getTime();
}
const browserService = BrowserServiceImpl.getInstance();
const promise = incremental ?
browserService.handler.queryHistoryContinuation() :
browserService.handler.queryHistory(
this.queryState.searchTerm, RESULTS_PER_PAGE,
afterTimestamp ? afterTimestamp : null);
// Ignore rejected (cancelled) queries.
promise.then((result) => this.onQueryResult_(result.results), () => {});
}
private onChangeQuery_(e: CustomEvent<{search: string, after: string}>) {
const changes = e.detail;
let needsUpdate = false;
if (changes.search !== null &&
changes.search !== this.queryState.searchTerm) {
this.queryState = {...this.queryState, searchTerm: changes.search};
this.searchTermChanged_();
needsUpdate = true;
}
if (loadTimeData.getBoolean('enableHistoryEmbeddings') &&
changes.after !== null && changes.after !== this.queryState.after &&
(Boolean(changes.after) || Boolean(this.queryState.after))) {
this.queryState = {...this.queryState, after: changes.after};
needsUpdate = true;
}
if (needsUpdate) {
this.queryHistory_(false);
}
}
private onQueryHistory_(e: CustomEvent<boolean>): boolean {
this.queryHistory_(e.detail);
return false;
}
/**
* @param results List of results with information about the query.
*/
private onQueryResult_(results: QueryResult) {
this.queryState = {...this.queryState, querying: false};
this.queryResult = {
...this.queryResult,
info: results.info,
value: results.value,
};
this.fire('query-finished', {result: this.queryResult});
}
private searchTermChanged_() {
this.flushDebouncedQueryResultMetric_();
// TODO(tsergeant): Ignore incremental searches in this metric.
if (this.queryState.searchTerm) {
BrowserServiceImpl.getInstance().recordAction('Search');
this.resultPendingMetricsTimestamp_ = performance.now();
}
}
/**
* Flushes any pending query result metric waiting to be logged.
*/
private flushDebouncedQueryResultMetric_() {
if (this.resultPendingMetricsTimestamp_ &&
(performance.now() - this.resultPendingMetricsTimestamp_) >=
QUERY_RESULT_MINIMUM_AGE) {
BrowserServiceImpl.getInstance().recordHistogram(
'History.Embeddings.UserActions',
HistoryEmbeddingsUserActions.NON_EMPTY_QUERY_HISTORY_SEARCH,
HistoryEmbeddingsUserActions.END);
}
// Clear this regardless if it was recorded or not, because we don't want
// to "try again" to record the same query.
this.resultPendingMetricsTimestamp_ = null;
}
}
customElements.define(
HistoryQueryManagerElement.is, HistoryQueryManagerElement);