coveo-search-ui
Version:
Coveo JavaScript Search Framework
220 lines (187 loc) • 7.65 kB
text/typescript
import { first } from 'underscore';
import { Assert } from '../misc/Assert';
import { Logger } from '../misc/Logger';
import { IAPIAnalyticsSearchEventsResponse } from '../rest/APIAnalyticsSearchEventsResponse';
import { IClickEvent } from '../rest/ClickEvent';
import { IEndpointCallerOptions, IErrorResponse, ISuccessResponse } from '../rest/EndpointCaller';
import { AnalyticsEndpointCaller } from '../rest/AnalyticsEndpointCaller';
import { IStringMap } from '../rest/GenericParam';
import { ISearchEvent } from '../rest/SearchEvent';
import { UrlUtils, IUrlNormalizedParts } from '../utils/UrlUtils';
import { Utils } from '../utils/Utils';
import { AccessToken } from './AccessToken';
import { IAPIAnalyticsEventResponse } from './APIAnalyticsEventResponse';
import { IAPIAnalyticsVisitResponseRest } from './APIAnalyticsVisitResponse';
import { ICustomEvent } from './CustomEvent';
import { ITopQueries } from './TopQueries';
import { SearchEndpoint } from '../rest/SearchEndpoint';
import { AnalyticsInformation } from '../ui/Analytics/AnalyticsInformation';
export interface IAnalyticsEndpointOptions {
accessToken: AccessToken;
serviceUrl: string;
organization: string;
}
export class AnalyticsEndpoint {
logger: Logger;
static DEFAULT_ANALYTICS_URI = 'https://analytics.cloud.coveo.com/rest/ua';
static DEFAULT_ANALYTICS_VERSION = 'v15';
static CUSTOM_ANALYTICS_VERSION = undefined;
static pendingRequest: Promise<any>;
private visitId: string;
private organization: string;
public endpointCaller: AnalyticsEndpointCaller;
constructor(public options: IAnalyticsEndpointOptions) {
this.logger = new Logger(this);
const endpointCallerOptions: IEndpointCallerOptions = {
accessToken: this.options.accessToken.token
};
this.endpointCaller = new AnalyticsEndpointCaller(endpointCallerOptions);
this.organization = options.organization;
}
public static getURLFromSearchEndpoint(endpoint: SearchEndpoint): string {
if (!endpoint || !endpoint.options || !endpoint.options.restUri) {
return this.DEFAULT_ANALYTICS_URI;
}
const [basePlatform] = endpoint.options.restUri.replace(/^(https?:\/\/)platform/, '$1analytics').split('/rest');
return basePlatform + '/rest/ua';
}
public getCurrentVisitId(): string {
return this.visitId;
}
public getCurrentVisitIdPromise(): Promise<string> {
return new Promise((resolve, reject) => {
if (this.getCurrentVisitId()) {
resolve(this.getCurrentVisitId());
} else {
const url = this.buildAnalyticsUrl('/analytics/visit');
this.getFromService<IAPIAnalyticsVisitResponseRest>(url, {})
.then((response: IAPIAnalyticsVisitResponseRest) => {
this.visitId = response.id;
resolve(this.visitId);
})
.catch((response: IErrorResponse) => {
reject(response);
});
}
});
}
public sendSearchEvents(searchEvents: ISearchEvent[]): Promise<IAPIAnalyticsSearchEventsResponse> {
if (searchEvents.length > 0) {
this.logger.info('Logging analytics search events', searchEvents);
return this.sendToService(searchEvents, 'searches', 'searchEvents');
}
}
public sendDocumentViewEvent(documentViewEvent: IClickEvent): Promise<IAPIAnalyticsEventResponse> {
Assert.exists(documentViewEvent);
this.logger.info('Logging analytics document view', documentViewEvent);
return this.sendToService(documentViewEvent, 'click', 'clickEvent');
}
public sendCustomEvent(customEvent: ICustomEvent) {
Assert.exists(customEvent);
this.logger.info('Logging analytics custom event', customEvent);
return this.sendToService(customEvent, 'custom', 'customEvent');
}
public getTopQueries(params: ITopQueries): Promise<string[]> {
const url = this.buildAnalyticsUrl('/stats/topQueries');
return this.getFromService<string[]>(url, params);
}
public clearCookies() {
new AnalyticsInformation().clear();
}
private async sendToService(data: Record<string, any>, path: string, paramName: string): Promise<any> {
// We use pendingRequest because we don't want to have 2 request to analytics at the same time.
// Otherwise the cookie visitId won't be set correctly.
if (AnalyticsEndpoint.pendingRequest != null) {
await AnalyticsEndpoint.pendingRequest;
}
const url = this.getURL(path);
const request = this.executeRequest(url, data);
try {
const results = await request;
AnalyticsEndpoint.pendingRequest = null;
this.handleAnalyticsEventResponse(results.data);
return results.data;
} catch (error) {
AnalyticsEndpoint.pendingRequest = null;
if (this.isAnalyticsTokenExpired(error)) {
const successfullyRenewed = await this.options.accessToken.doRenew();
if (successfullyRenewed) {
return this.sendToService(data, path, paramName);
}
}
throw error;
}
}
private isAnalyticsTokenExpired(error: IErrorResponse) {
return error != null && error.statusCode === 400 && error.data && error.data.type === 'InvalidToken';
}
private executeRequest(
urlNormalized: IUrlNormalizedParts,
data: Record<string, any>
): Promise<ISuccessResponse<IAPIAnalyticsEventResponse>> {
const request = this.endpointCaller.call<IAPIAnalyticsEventResponse>({
errorsAsSuccess: false,
method: 'POST',
queryString: urlNormalized.queryNormalized,
requestData: data,
url: urlNormalized.path,
responseType: 'text',
requestDataType: 'application/json'
});
if (request) {
AnalyticsEndpoint.pendingRequest = request;
return request;
}
// In some case, (eg: using navigator.sendBeacon), there won't be any response to read from the service
// In those case, send back an empty object upstream.
return Promise.resolve({
data: {
visitId: '',
visitorId: ''
},
duration: 0
});
}
private getURL(path: string): IUrlNormalizedParts {
const versionToCall = AnalyticsEndpoint.CUSTOM_ANALYTICS_VERSION || AnalyticsEndpoint.DEFAULT_ANALYTICS_VERSION;
const urlNormalized = UrlUtils.normalizeAsParts({
paths: [this.options.serviceUrl, versionToCall, '/analytics/', path],
query: {
org: this.organization
}
});
return urlNormalized;
}
private getFromService<T>(url: string, params: IStringMap<string>): Promise<T> {
const paramsToSend = { ...params, access_token: this.options.accessToken.token };
return this.endpointCaller
.call<T>({
errorsAsSuccess: false,
method: 'GET',
queryString: this.options.organization ? ['org=' + Utils.safeEncodeURIComponent(this.options.organization)] : [],
requestData: paramsToSend,
responseType: 'json',
url: url
})
.then((res: ISuccessResponse<T>) => {
return res.data;
});
}
private handleAnalyticsEventResponse(response: IAPIAnalyticsEventResponse | IAPIAnalyticsSearchEventsResponse) {
let visitId: string;
if (response['visitId']) {
visitId = response['visitId'];
} else if (response['searchEventResponses']) {
visitId = (<IAPIAnalyticsEventResponse>first(response['searchEventResponses'])).visitId;
}
if (visitId) {
this.visitId = visitId;
}
return response;
}
private buildAnalyticsUrl(path: string) {
return UrlUtils.normalizeAsString({
paths: [this.options.serviceUrl, AnalyticsEndpoint.CUSTOM_ANALYTICS_VERSION || AnalyticsEndpoint.DEFAULT_ANALYTICS_VERSION, path]
});
}
}