mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Add telemetry for AI Assistant
Summary: Also fixes a few bugs with some telemetry events not being recorded. Test Plan: Manual. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3960
This commit is contained in:
parent
0040716006
commit
0a34292536
@ -16,6 +16,7 @@ import {
|
|||||||
import {createParser} from 'app/common/ValueParser';
|
import {createParser} from 'app/common/ValueParser';
|
||||||
import {Observable} from 'grainjs';
|
import {Observable} from 'grainjs';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
|
||||||
// Column behavior type, used primarily in the UI.
|
// Column behavior type, used primarily in the UI.
|
||||||
export type BEHAVIOR = "empty"|"formula"|"data";
|
export type BEHAVIOR = "empty"|"formula"|"data";
|
||||||
@ -165,7 +166,7 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
|
|||||||
const docId = urlState().state.get().doc ?? '';
|
const docId = urlState().state.get().doc ?? '';
|
||||||
// Changed key name from history to history-v2 when ChatHistory changed in incompatible way.
|
// Changed key name from history to history-v2 when ChatHistory changed in incompatible way.
|
||||||
const key = `formula-assistant-history-v2-${docId}-${this.table().tableId()}-${this.colId()}`;
|
const key = `formula-assistant-history-v2-${docId}-${this.table().tableId()}-${this.colId()}`;
|
||||||
return localStorageJsonObs(key, {messages: []} as ChatHistory);
|
return localStorageJsonObs(key, {messages: [], conversationId: uuidv4()} as ChatHistory);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,5 +218,6 @@ export interface ChatMessage {
|
|||||||
*/
|
*/
|
||||||
export interface ChatHistory {
|
export interface ChatHistory {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
|
conversationId?: string;
|
||||||
state?: AssistanceState;
|
state?: AssistanceState;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as commands from 'app/client/components/commands';
|
import * as commands from 'app/client/components/commands';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
|
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||||
import {makeT} from 'app/client/lib/localization';
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||||
import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
|
||||||
@ -10,7 +11,7 @@ import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
|
|||||||
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
||||||
import {createUserImage} from 'app/client/ui/UserImage';
|
import {createUserImage} from 'app/client/ui/UserImage';
|
||||||
import {FormulaEditor} from 'app/client/widgets/FormulaEditor';
|
import {FormulaEditor} from 'app/client/widgets/FormulaEditor';
|
||||||
import {AssistanceResponse, AssistanceState} from 'app/common/AssistancePrompts';
|
import {AssistanceResponse, AssistanceState, FormulaAssistanceContext} from 'app/common/AssistancePrompts';
|
||||||
import {basicButton, bigPrimaryButtonLink, primaryButton} from 'app/client/ui2018/buttons';
|
import {basicButton, bigPrimaryButtonLink, primaryButton} from 'app/client/ui2018/buttons';
|
||||||
import {theme, vars} from 'app/client/ui2018/cssVars';
|
import {theme, vars} from 'app/client/ui2018/cssVars';
|
||||||
import {autoGrow} from 'app/client/ui/forms';
|
import {autoGrow} from 'app/client/ui/forms';
|
||||||
@ -19,12 +20,14 @@ import {cssLink} from 'app/client/ui2018/links';
|
|||||||
import {commonUrls} from 'app/common/gristUrls';
|
import {commonUrls} from 'app/common/gristUrls';
|
||||||
import {movable} from 'app/client/lib/popupUtils';
|
import {movable} from 'app/client/lib/popupUtils';
|
||||||
import {loadingDots} from 'app/client/ui2018/loaders';
|
import {loadingDots} from 'app/client/ui2018/loaders';
|
||||||
import {menu, menuCssClass, menuItem} from "app/client/ui2018/menus";
|
import {menu, menuCssClass, menuItem} from 'app/client/ui2018/menus';
|
||||||
|
import {TelemetryEvent, TelemetryMetadata} from 'app/common/Telemetry';
|
||||||
import {Computed, Disposable, dom, DomElementArg, makeTestId,
|
import {Computed, Disposable, dom, DomElementArg, makeTestId,
|
||||||
MutableObsArray, obsArray, Observable, styled} from 'grainjs';
|
MutableObsArray, obsArray, Observable, styled} from 'grainjs';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import noop from 'lodash/noop';
|
import noop from 'lodash/noop';
|
||||||
import {marked} from 'marked';
|
import {marked} from 'marked';
|
||||||
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
|
||||||
const t = makeT('FormulaEditor');
|
const t = makeT('FormulaEditor');
|
||||||
const testId = makeTestId('test-formula-editor-');
|
const testId = makeTestId('test-formula-editor-');
|
||||||
@ -67,6 +70,8 @@ export class FormulaAssistant extends Disposable {
|
|||||||
private _chatPanelBody: HTMLElement;
|
private _chatPanelBody: HTMLElement;
|
||||||
/** Client height of the chat panel body element. */
|
/** Client height of the chat panel body element. */
|
||||||
private _chatPanelBodyClientHeight = Observable.create<number>(this, 0);
|
private _chatPanelBodyClientHeight = Observable.create<number>(this, 0);
|
||||||
|
/** Set to true once the panel has been expanded (including by default). */
|
||||||
|
private _hasExpanded = false;
|
||||||
/**
|
/**
|
||||||
* Last known height of the chat panel.
|
* Last known height of the chat panel.
|
||||||
*
|
*
|
||||||
@ -108,6 +113,7 @@ export class FormulaAssistant extends Disposable {
|
|||||||
this._chat = ChatHistory.create(this, {
|
this._chat = ChatHistory.create(this, {
|
||||||
...this._options,
|
...this._options,
|
||||||
apply: this._apply.bind(this),
|
apply: this._apply.bind(this),
|
||||||
|
logTelemetryEvent: this._logTelemetryEvent.bind(this),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.autoDispose(commands.createGroup({
|
this.autoDispose(commands.createGroup({
|
||||||
@ -143,6 +149,15 @@ export class FormulaAssistant extends Disposable {
|
|||||||
|
|
||||||
this._triggerFinalize = bundleInfo.triggerFinalize;
|
this._triggerFinalize = bundleInfo.triggerFinalize;
|
||||||
this.onDispose(() => {
|
this.onDispose(() => {
|
||||||
|
if (this._hasExpanded) {
|
||||||
|
this._logTelemetryEvent('assistantClose', false, {
|
||||||
|
suggestionApplied: this._chat.conversationSuggestedFormulas.get()
|
||||||
|
.includes(this._options.column.formula.peek()),
|
||||||
|
conversationLength: this._chat.conversationLength.get(),
|
||||||
|
conversationHistoryLength: this._chat.conversationHistoryLength.get(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// This will be noop if already called.
|
// This will be noop if already called.
|
||||||
this._triggerFinalize();
|
this._triggerFinalize();
|
||||||
});
|
});
|
||||||
@ -178,6 +193,11 @@ export class FormulaAssistant extends Disposable {
|
|||||||
this._chatPanelBody.style.setProperty('height', '999px');
|
this._chatPanelBody.style.setProperty('height', '999px');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._assistantEnabled.get() && this._assistantExpanded.get()) {
|
||||||
|
this._logTelemetryEvent('assistantOpen', true);
|
||||||
|
this._hasExpanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
return this._domElement;
|
return this._domElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +218,21 @@ export class FormulaAssistant extends Disposable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _logTelemetryEvent(event: TelemetryEvent, includeContext = false, metadata: TelemetryMetadata = {}) {
|
||||||
|
logTelemetryEvent(event, {
|
||||||
|
full: {
|
||||||
|
docIdDigest: this._gristDoc.docId,
|
||||||
|
conversationId: this._chat.conversationId.get(),
|
||||||
|
...(!includeContext ? {} : {context: {
|
||||||
|
type: 'formula',
|
||||||
|
tableId: this._options.column.table.peek().tableId.peek(),
|
||||||
|
colId: this._options.column.colId.peek(),
|
||||||
|
} as FormulaAssistanceContext}),
|
||||||
|
...metadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _buildChatPanelHeader() {
|
private _buildChatPanelHeader() {
|
||||||
return cssChatPanelHeader(
|
return cssChatPanelHeader(
|
||||||
cssChatPanelHeaderTitle(
|
cssChatPanelHeaderTitle(
|
||||||
@ -266,6 +301,12 @@ export class FormulaAssistant extends Disposable {
|
|||||||
* Save button handler. We just store the action and wait for the bundler to finalize.
|
* Save button handler. We just store the action and wait for the bundler to finalize.
|
||||||
*/
|
*/
|
||||||
private _saveOrClose() {
|
private _saveOrClose() {
|
||||||
|
if (this._hasExpanded) {
|
||||||
|
this._logTelemetryEvent('assistantSave', true, {
|
||||||
|
newFormula: this._options.column.formula.peek(),
|
||||||
|
oldFormula: this._options.editor.getTextValue(),
|
||||||
|
});
|
||||||
|
}
|
||||||
this._action = 'save';
|
this._action = 'save';
|
||||||
this._triggerFinalize();
|
this._triggerFinalize();
|
||||||
}
|
}
|
||||||
@ -274,6 +315,9 @@ export class FormulaAssistant extends Disposable {
|
|||||||
* Cancel button handler.
|
* Cancel button handler.
|
||||||
*/
|
*/
|
||||||
private _cancel() {
|
private _cancel() {
|
||||||
|
if (this._hasExpanded) {
|
||||||
|
this._logTelemetryEvent('assistantCancel', true);
|
||||||
|
}
|
||||||
this._action = 'cancel';
|
this._action = 'cancel';
|
||||||
this._triggerFinalize();
|
this._triggerFinalize();
|
||||||
}
|
}
|
||||||
@ -374,6 +418,11 @@ export class FormulaAssistant extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _expandChatPanel() {
|
private _expandChatPanel() {
|
||||||
|
if (!this._hasExpanded) {
|
||||||
|
this._logTelemetryEvent('assistantOpen', true);
|
||||||
|
this._hasExpanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
this._assistantExpanded.set(true);
|
this._assistantExpanded.set(true);
|
||||||
const editor = this._options.editor.getDom();
|
const editor = this._options.editor.getDom();
|
||||||
let availableSpace = editor.clientHeight - MIN_FORMULA_EDITOR_HEIGHT_PX
|
let availableSpace = editor.clientHeight - MIN_FORMULA_EDITOR_HEIGHT_PX
|
||||||
@ -565,13 +614,17 @@ export class FormulaAssistant extends Disposable {
|
|||||||
// Destruct options.
|
// Destruct options.
|
||||||
const {column, gristDoc} = this._options;
|
const {column, gristDoc} = this._options;
|
||||||
// Get the state of the chat from the column.
|
// Get the state of the chat from the column.
|
||||||
|
const conversationId = this._chat.conversationId.get();
|
||||||
const prevState = column.chatHistory.peek().get().state;
|
const prevState = column.chatHistory.peek().get().state;
|
||||||
// Send the message back to the AI with previous state and a mark that we want to regenerate.
|
// Send the message back to the AI with previous state and a mark that we want to regenerate.
|
||||||
// We can't modify the state here as we treat it as a black box, so we only removed last message
|
// We can't modify the state here as we treat it as a black box, so we only removed last message
|
||||||
// from ai from the chat, we grabbed last question and we are sending it back to the AI with a
|
// from ai from the chat, we grabbed last question and we are sending it back to the AI with a
|
||||||
// flag that it should clear last response and regenerate it.
|
// flag that it should clear last response and regenerate it.
|
||||||
const {reply, suggestedActions, state} = await askAI(gristDoc, {
|
const {reply, suggestedActions, state} = await askAI(gristDoc, {
|
||||||
column, description, state: prevState,
|
conversationId,
|
||||||
|
column,
|
||||||
|
description,
|
||||||
|
state: prevState,
|
||||||
regenerate,
|
regenerate,
|
||||||
});
|
});
|
||||||
console.debug('suggestedActions', {suggestedActions, reply, state});
|
console.debug('suggestedActions', {suggestedActions, reply, state});
|
||||||
@ -639,8 +692,12 @@ export class FormulaAssistant extends Disposable {
|
|||||||
* sending messages to the AI.
|
* sending messages to the AI.
|
||||||
*/
|
*/
|
||||||
class ChatHistory extends Disposable {
|
class ChatHistory extends Disposable {
|
||||||
public history: MutableObsArray<ChatMessage>;
|
public conversationId: Observable<string>;
|
||||||
public length: Computed<number>;
|
public conversation: MutableObsArray<ChatMessage>;
|
||||||
|
public conversationHistory: MutableObsArray<ChatMessage>;
|
||||||
|
public conversationLength: Computed<number>;
|
||||||
|
public conversationHistoryLength: Computed<number>;
|
||||||
|
public conversationSuggestedFormulas: Computed<string[]>;
|
||||||
public lastSuggestedFormula: Computed<string|null>;
|
public lastSuggestedFormula: Computed<string|null>;
|
||||||
|
|
||||||
private _element: HTMLElement;
|
private _element: HTMLElement;
|
||||||
@ -649,31 +706,56 @@ class ChatHistory extends Disposable {
|
|||||||
column: ColumnRec,
|
column: ColumnRec,
|
||||||
gristDoc: GristDoc,
|
gristDoc: GristDoc,
|
||||||
apply: (formula: string) => void,
|
apply: (formula: string) => void,
|
||||||
|
logTelemetryEvent: (event: TelemetryEvent, includeContext?: boolean, metadata?: TelemetryMetadata) => void,
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
const column = this._options.column;
|
const column = this._options.column;
|
||||||
|
let conversationId = column.chatHistory.peek().get().conversationId;
|
||||||
|
if (!conversationId) {
|
||||||
|
conversationId = uuidv4();
|
||||||
|
const chatHistory = column.chatHistory.peek();
|
||||||
|
chatHistory.set({...chatHistory.get(), conversationId});
|
||||||
|
}
|
||||||
|
this.conversationId = Observable.create(this, conversationId);
|
||||||
|
this.autoDispose(this.conversationId.addListener((newConversationId) => {
|
||||||
|
// If a new conversation id was generated (e.g. on Clear Conversation), save it
|
||||||
|
// to the column's history.
|
||||||
|
const chatHistory = column.chatHistory.peek();
|
||||||
|
chatHistory.set({...chatHistory.get(), conversationId: newConversationId});
|
||||||
|
}));
|
||||||
|
|
||||||
// Create observable array of messages that is connected to the column's chatHistory.
|
// Create observable array of messages that is connected to the column's chatHistory.
|
||||||
this.history = this.autoDispose(obsArray(column.chatHistory.peek().get().messages));
|
this.conversationHistory = this.autoDispose(obsArray(column.chatHistory.peek().get().messages));
|
||||||
this.autoDispose(this.history.addListener((cur) => {
|
this.autoDispose(this.conversationHistory.addListener((cur) => {
|
||||||
const chatHistory = column.chatHistory.peek();
|
const chatHistory = column.chatHistory.peek();
|
||||||
chatHistory.set({...chatHistory.get(), messages: [...cur]});
|
chatHistory.set({...chatHistory.get(), messages: [...cur]});
|
||||||
}));
|
}));
|
||||||
this.length = Computed.create(this, use => use(this.history).length); // ??
|
this.conversation = this.autoDispose(obsArray());
|
||||||
|
|
||||||
|
this.conversationHistoryLength = Computed.create(this, use => use(this.conversationHistory).length);
|
||||||
|
this.conversationLength = Computed.create(this, use => use(this.conversation).length);
|
||||||
|
|
||||||
|
this.conversationSuggestedFormulas = Computed.create(this, use => {
|
||||||
|
return use(this.conversation)
|
||||||
|
.map(({formula}) => formula)
|
||||||
|
.filter((formula): formula is string => Boolean(formula));
|
||||||
|
});
|
||||||
this.lastSuggestedFormula = Computed.create(this, use => {
|
this.lastSuggestedFormula = Computed.create(this, use => {
|
||||||
return [...use(this.history)].reverse().find(entry => entry.formula)?.formula ?? null;
|
return [...use(this.conversationHistory)].reverse().find(({formula}) => formula)?.formula ?? null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public thinking(on = true) {
|
public thinking(on = true) {
|
||||||
if (!on) {
|
if (!on) {
|
||||||
// Find all index of all thinking messages.
|
// Find all index of all thinking messages.
|
||||||
const messages = [...this.history.get()].filter(m => m.message === '...');
|
const messages = [...this.conversationHistory.get()].filter(m => m.message === '...');
|
||||||
// Remove all thinking messages.
|
// Remove all thinking messages.
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
this.history.splice(this.history.get().indexOf(message), 1);
|
this.conversationHistory.splice(this.conversationHistory.get().indexOf(message), 1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.history.push({
|
this.conversationHistory.push({
|
||||||
message: '...',
|
message: '...',
|
||||||
sender: 'ai',
|
sender: 'ai',
|
||||||
});
|
});
|
||||||
@ -688,20 +770,21 @@ class ChatHistory extends Disposable {
|
|||||||
public addResponse(message: ChatMessage) {
|
public addResponse(message: ChatMessage) {
|
||||||
// Clear any thinking from messages.
|
// Clear any thinking from messages.
|
||||||
this.thinking(false);
|
this.thinking(false);
|
||||||
this.history.push({...message, sender: 'ai'});
|
const entry: ChatMessage = {...message, sender: 'ai'};
|
||||||
|
this.conversationHistory.push(entry);
|
||||||
|
this.conversation.push(entry);
|
||||||
this.scrollDown();
|
this.scrollDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public addQuestion(message: string) {
|
public addQuestion(message: string) {
|
||||||
this.thinking(false);
|
this.thinking(false);
|
||||||
this.history.push({
|
const entry: ChatMessage = {message, sender: 'user'};
|
||||||
message,
|
this.conversationHistory.push(entry);
|
||||||
sender: 'user',
|
this.conversation.push(entry);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public lastQuestion() {
|
public lastQuestion() {
|
||||||
const list = this.history.get();
|
const list = this.conversationHistory.get();
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -713,14 +796,16 @@ class ChatHistory extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public removeLastResponse() {
|
public removeLastResponse() {
|
||||||
const lastMessage = this.history.get()[this.history.get().length - 1];
|
const lastMessage = this.conversationHistory.get()[this.conversationHistory.get().length - 1];
|
||||||
if (lastMessage?.sender === 'ai') {
|
if (lastMessage?.sender === 'ai') {
|
||||||
this.history.pop();
|
this.conversationHistory.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear() {
|
public clear() {
|
||||||
this.history.set([]);
|
this._options.logTelemetryEvent('assistantClearConversation', true);
|
||||||
|
this.conversationId.set(uuidv4());
|
||||||
|
this.conversationHistory.set([]);
|
||||||
const {column} = this._options;
|
const {column} = this._options;
|
||||||
// Get the state of the chat from the column.
|
// Get the state of the chat from the column.
|
||||||
const prevState = column.chatHistory.peek().get();
|
const prevState = column.chatHistory.peek().get();
|
||||||
@ -737,7 +822,7 @@ class ChatHistory extends Disposable {
|
|||||||
public buildDom() {
|
public buildDom() {
|
||||||
return this._element = cssHistory(
|
return this._element = cssHistory(
|
||||||
this._buildIntroMessage(),
|
this._buildIntroMessage(),
|
||||||
dom.forEach(this.history, entry => {
|
dom.forEach(this.conversationHistory, entry => {
|
||||||
if (entry.sender === 'user') {
|
if (entry.sender === 'user') {
|
||||||
return cssMessage(
|
return cssMessage(
|
||||||
dom('span',
|
dom('span',
|
||||||
@ -857,13 +942,15 @@ class ChatHistory extends Disposable {
|
|||||||
async function askAI(grist: GristDoc, options: {
|
async function askAI(grist: GristDoc, options: {
|
||||||
column: ColumnRec,
|
column: ColumnRec,
|
||||||
description: string,
|
description: string,
|
||||||
|
conversationId: string,
|
||||||
regenerate?: boolean,
|
regenerate?: boolean,
|
||||||
state?: AssistanceState
|
state?: AssistanceState
|
||||||
}): Promise<AssistanceResponse> {
|
}): Promise<AssistanceResponse> {
|
||||||
const {column, description, state, regenerate} = options;
|
const {column, description, conversationId, state, regenerate} = options;
|
||||||
const tableId = column.table.peek().tableId.peek();
|
const tableId = column.table.peek().tableId.peek();
|
||||||
const colId = column.colId.peek();
|
const colId = column.colId.peek();
|
||||||
const result = await grist.docApi.getAssistance({
|
const result = await grist.docApi.getAssistance({
|
||||||
|
conversationId,
|
||||||
context: {type: 'formula', tableId, colId},
|
context: {type: 'formula', tableId, colId},
|
||||||
text: description,
|
text: description,
|
||||||
state,
|
state,
|
||||||
|
@ -35,6 +35,7 @@ export type AssistanceContext = FormulaAssistanceContext;
|
|||||||
* A request for assistance.
|
* A request for assistance.
|
||||||
*/
|
*/
|
||||||
export interface AssistanceRequest {
|
export interface AssistanceRequest {
|
||||||
|
conversationId: string;
|
||||||
context: AssistanceContext;
|
context: AssistanceContext;
|
||||||
state?: AssistanceState;
|
state?: AssistanceState;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -25,6 +25,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
apiUsage: {
|
apiUsage: {
|
||||||
description: 'Triggered when an HTTP request with an API key is made.',
|
description: 'Triggered when an HTTP request with an API key is made.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
method: {
|
method: {
|
||||||
description: 'The HTTP request method (e.g. GET, POST, PUT).',
|
description: 'The HTTP request method (e.g. GET, POST, PUT).',
|
||||||
@ -40,9 +41,254 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
assistantOpen: {
|
||||||
|
description: 'Triggered when the AI Assistant is first opened.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'short',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
description: 'A random identifier for the current conversation with the assistant.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistantSend: {
|
||||||
|
description: 'Triggered when a message is sent to the AI Assistant.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'short',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
siteId: {
|
||||||
|
description: 'The id of the site.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
siteType: {
|
||||||
|
description: 'The type of the site.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
description: 'The document access level of the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
description: 'A random identifier for the current conversation with the assistant.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
prompt: {
|
||||||
|
description: 'The role ("user" or "system"), content, and index of the message sent to the AI Assistant.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistantReceive: {
|
||||||
|
description: 'Triggered when a message is received from the AI Assistant is received.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'short',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
siteId: {
|
||||||
|
description: 'The id of the site.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
siteType: {
|
||||||
|
description: 'The type of the site.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
description: 'The document access level of the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
description: 'A random identifier for the current conversation with the assistant.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
description: 'The content and index of the message received from the AI Assistant.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
suggestedFormula: {
|
||||||
|
description: 'The formula suggested by the AI Assistant, if present.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistantSave: {
|
||||||
|
description: 'Triggered when changes in the expanded formula editor are saved after the AI Assistant ' +
|
||||||
|
'was opened.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'short',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
description: 'A random identifier for the current conversation with the assistant.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
newFormula: {
|
||||||
|
description: 'The formula that was saved.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
oldFormula: {
|
||||||
|
description: 'The formula that was overwritten.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistantCancel: {
|
||||||
|
description: 'Triggered when changes in the expanded formula editor are discarded after the AI Assistant ' +
|
||||||
|
'was opened.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'short',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
description: 'A random identifier for the current conversation with the assistant.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistantClearConversation: {
|
||||||
|
description: 'Triggered when a conversation in the AI Assistant is cleared.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'short',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
description: 'A random identifier for the current conversation with the assistant.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
description: 'The type of assistant (e.g. "formula"), table id, and column id.',
|
||||||
|
dataType: 'object',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assistantClose: {
|
||||||
|
description: 'Triggered when a formula is saved or discarded after the AI Assistant was opened.',
|
||||||
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
|
metadataContracts: {
|
||||||
|
docIdDigest: {
|
||||||
|
description: 'A hash of the doc id.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
description: 'A random identifier for the current conversation with the assistant.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
suggestionApplied: {
|
||||||
|
description: 'True if a suggested formula from one of the received messages was applied.',
|
||||||
|
dataType: 'boolean',
|
||||||
|
},
|
||||||
|
conversationLength: {
|
||||||
|
description: 'The number of messages sent and received since opening the AI Assistant.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
conversationHistoryLength: {
|
||||||
|
description: "The number of messages in the conversation's history. May be less than conversationLength "
|
||||||
|
+ "if the conversation history was cleared in the same session.",
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
beaconOpen: {
|
beaconOpen: {
|
||||||
description: 'Triggered when HelpScout Beacon is opened.',
|
description: 'Triggered when HelpScout Beacon is opened.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
userId: {
|
userId: {
|
||||||
description: 'The id of the user that triggered this event.',
|
description: 'The id of the user that triggered this event.',
|
||||||
@ -57,6 +303,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
beaconArticleViewed: {
|
beaconArticleViewed: {
|
||||||
description: 'Triggered when an article is opened in HelpScout Beacon.',
|
description: 'Triggered when an article is opened in HelpScout Beacon.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
articleId: {
|
articleId: {
|
||||||
description: 'The id of the article.',
|
description: 'The id of the article.',
|
||||||
@ -75,6 +322,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
beaconEmailSent: {
|
beaconEmailSent: {
|
||||||
description: 'Triggered when an email is sent in HelpScout Beacon.',
|
description: 'Triggered when an email is sent in HelpScout Beacon.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
userId: {
|
userId: {
|
||||||
description: 'The id of the user that triggered this event.',
|
description: 'The id of the user that triggered this event.',
|
||||||
@ -89,6 +337,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
beaconSearch: {
|
beaconSearch: {
|
||||||
description: 'Triggered when a search is made in HelpScout Beacon.',
|
description: 'Triggered when a search is made in HelpScout Beacon.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
searchQuery: {
|
searchQuery: {
|
||||||
description: 'The search query.',
|
description: 'The search query.',
|
||||||
@ -107,6 +356,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
documentForked: {
|
documentForked: {
|
||||||
description: 'Triggered when a document is forked.',
|
description: 'Triggered when a document is forked.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
docIdDigest: {
|
docIdDigest: {
|
||||||
description: 'A hash of the doc id.',
|
description: 'A hash of the doc id.',
|
||||||
@ -161,6 +411,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
documentOpened: {
|
documentOpened: {
|
||||||
description: 'Triggered when a public document or template is opened.',
|
description: 'Triggered when a public document or template is opened.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
docIdDigest: {
|
docIdDigest: {
|
||||||
description: 'A hash of the doc id.',
|
description: 'A hash of the doc id.',
|
||||||
@ -211,6 +462,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
documentUsage: {
|
documentUsage: {
|
||||||
description: 'Triggered on doc open and close, as well as hourly while a document is open.',
|
description: 'Triggered on doc open and close, as well as hourly while a document is open.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
docIdDigest: {
|
docIdDigest: {
|
||||||
description: 'A hash of the doc id.',
|
description: 'A hash of the doc id.',
|
||||||
@ -346,6 +598,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
processMonitor: {
|
processMonitor: {
|
||||||
description: 'Triggered every 5 seconds.',
|
description: 'Triggered every 5 seconds.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
heapUsedMB: {
|
heapUsedMB: {
|
||||||
description: 'Size of JS heap in use, in MiB.',
|
description: 'Size of JS heap in use, in MiB.',
|
||||||
@ -368,6 +621,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
sendingWebhooks: {
|
sendingWebhooks: {
|
||||||
description: 'Triggered when sending webhooks.',
|
description: 'Triggered when sending webhooks.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
numEvents: {
|
numEvents: {
|
||||||
description: 'The number of events in the batch of webhooks being sent.',
|
description: 'The number of events in the batch of webhooks being sent.',
|
||||||
@ -406,6 +660,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
signupFirstVisit: {
|
signupFirstVisit: {
|
||||||
description: 'Triggered when a new user first opens the Grist app',
|
description: 'Triggered when a new user first opens the Grist app',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
siteId: {
|
siteId: {
|
||||||
description: 'The site id of first visit after signup.',
|
description: 'The site id of first visit after signup.',
|
||||||
@ -429,6 +684,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
description: 'Triggered after a user successfully verifies their account during sign-up. '
|
description: 'Triggered after a user successfully verifies their account during sign-up. '
|
||||||
+ 'Not triggered in grist-core.',
|
+ 'Not triggered in grist-core.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
isAnonymousTemplateSignup: {
|
isAnonymousTemplateSignup: {
|
||||||
description: 'Whether the user viewed any templates before signing up.',
|
description: 'Whether the user viewed any templates before signing up.',
|
||||||
@ -443,6 +699,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
siteMembership: {
|
siteMembership: {
|
||||||
description: 'Triggered daily.',
|
description: 'Triggered daily.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
siteId: {
|
siteId: {
|
||||||
description: 'The site id.',
|
description: 'The site id.',
|
||||||
@ -469,6 +726,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
siteUsage: {
|
siteUsage: {
|
||||||
description: 'Triggered daily.',
|
description: 'Triggered daily.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
siteId: {
|
siteId: {
|
||||||
description: 'The site id.',
|
description: 'The site id.',
|
||||||
@ -508,6 +766,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
tutorialProgressChanged: {
|
tutorialProgressChanged: {
|
||||||
description: 'Triggered on changes to tutorial progress.',
|
description: 'Triggered on changes to tutorial progress.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
tutorialForkIdDigest: {
|
tutorialForkIdDigest: {
|
||||||
description: 'A hash of the tutorial fork id.',
|
description: 'A hash of the tutorial fork id.',
|
||||||
@ -529,11 +788,20 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
description: 'Percentage of tutorial completion.',
|
description: 'Percentage of tutorial completion.',
|
||||||
dataType: 'number',
|
dataType: 'number',
|
||||||
},
|
},
|
||||||
|
userId: {
|
||||||
|
description: 'The id of the user that triggered this event.',
|
||||||
|
dataType: 'number',
|
||||||
|
},
|
||||||
|
altSessionId: {
|
||||||
|
description: 'A random, session-based identifier for the user that triggered this event.',
|
||||||
|
dataType: 'string',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tutorialRestarted: {
|
tutorialRestarted: {
|
||||||
description: 'Triggered when a tutorial is restarted.',
|
description: 'Triggered when a tutorial is restarted.',
|
||||||
minimumTelemetryLevel: Level.full,
|
minimumTelemetryLevel: Level.full,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
tutorialForkIdDigest: {
|
tutorialForkIdDigest: {
|
||||||
description: 'A hash of the tutorial fork id.',
|
description: 'A hash of the tutorial fork id.',
|
||||||
@ -572,6 +840,7 @@ export const TelemetryContracts: TelemetryContracts = {
|
|||||||
watchedVideoTour: {
|
watchedVideoTour: {
|
||||||
description: 'Triggered when the video tour is closed.',
|
description: 'Triggered when the video tour is closed.',
|
||||||
minimumTelemetryLevel: Level.limited,
|
minimumTelemetryLevel: Level.limited,
|
||||||
|
retentionPeriod: 'indefinitely',
|
||||||
metadataContracts: {
|
metadataContracts: {
|
||||||
watchTimeSeconds: {
|
watchTimeSeconds: {
|
||||||
description: 'The number of seconds elapsed in the video player.',
|
description: 'The number of seconds elapsed in the video player.',
|
||||||
@ -595,6 +864,13 @@ type TelemetryContracts = Record<TelemetryEvent, TelemetryEventContract>;
|
|||||||
|
|
||||||
export const TelemetryEvents = StringUnion(
|
export const TelemetryEvents = StringUnion(
|
||||||
'apiUsage',
|
'apiUsage',
|
||||||
|
'assistantOpen',
|
||||||
|
'assistantSend',
|
||||||
|
'assistantReceive',
|
||||||
|
'assistantSave',
|
||||||
|
'assistantCancel',
|
||||||
|
'assistantClearConversation',
|
||||||
|
'assistantClose',
|
||||||
'beaconOpen',
|
'beaconOpen',
|
||||||
'beaconArticleViewed',
|
'beaconArticleViewed',
|
||||||
'beaconEmailSent',
|
'beaconEmailSent',
|
||||||
@ -617,12 +893,15 @@ export type TelemetryEvent = typeof TelemetryEvents.type;
|
|||||||
interface TelemetryEventContract {
|
interface TelemetryEventContract {
|
||||||
description: string;
|
description: string;
|
||||||
minimumTelemetryLevel: Level;
|
minimumTelemetryLevel: Level;
|
||||||
|
retentionPeriod: TelemetryRetentionPeriod;
|
||||||
metadataContracts?: Record<string, MetadataContract>;
|
metadataContracts?: Record<string, MetadataContract>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TelemetryRetentionPeriod = 'short' | 'indefinitely';
|
||||||
|
|
||||||
interface MetadataContract {
|
interface MetadataContract {
|
||||||
description: string;
|
description: string;
|
||||||
dataType: 'boolean' | 'number' | 'string' | 'string[]' | 'date';
|
dataType: 'boolean' | 'number' | 'string' | 'string[]' | 'date' | 'object';
|
||||||
minimumTelemetryLevel?: Level;
|
minimumTelemetryLevel?: Level;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,9 +922,6 @@ export type TelemetryMetadata = Record<string, any>;
|
|||||||
*/
|
*/
|
||||||
export const TELEMETRY_TEMPLATE_SIGNUP_COOKIE_NAME = 'gr_template_signup_trk';
|
export const TELEMETRY_TEMPLATE_SIGNUP_COOKIE_NAME = 'gr_template_signup_trk';
|
||||||
|
|
||||||
// A set of metadata keys that are always allowed when logging.
|
|
||||||
const ALLOWED_METADATA_KEYS = new Set(['eventSource', 'installationId']);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a function that accepts a telemetry event and metadata, and performs various
|
* Returns a function that accepts a telemetry event and metadata, and performs various
|
||||||
* checks on it based on a set of contracts and the `telemetryLevel`.
|
* checks on it based on a set of contracts and the `telemetryLevel`.
|
||||||
@ -670,8 +946,6 @@ export function buildTelemetryEventChecker(telemetryLevel: TelemetryLevel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(metadata ?? {})) {
|
for (const [key, value] of Object.entries(metadata ?? {})) {
|
||||||
if (ALLOWED_METADATA_KEYS.has(key)) { continue; }
|
|
||||||
|
|
||||||
const metadataContract = eventContract.metadataContracts?.[key];
|
const metadataContract = eventContract.metadataContracts?.[key];
|
||||||
if (!metadataContract) {
|
if (!metadataContract) {
|
||||||
throw new Error(`Unknown metadata for telemetry event ${event}: ${key}`);
|
throw new Error(`Unknown metadata for telemetry event ${event}: ${key}`);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import {AssistanceMessage, AssistanceRequest, AssistanceResponse} from 'app/common/AssistancePrompts';
|
import {AssistanceMessage, AssistanceRequest, AssistanceResponse} from 'app/common/AssistancePrompts';
|
||||||
import {delay} from 'app/common/delay';
|
import {delay} from 'app/common/delay';
|
||||||
import {DocAction} from 'app/common/DocActions';
|
import {DocAction} from 'app/common/DocActions';
|
||||||
|
import {ActiveDoc} from 'app/server/lib/ActiveDoc';
|
||||||
import {OptDocSession} from 'app/server/lib/DocSession';
|
import {OptDocSession} from 'app/server/lib/DocSession';
|
||||||
import log from 'app/server/lib/log';
|
import log from 'app/server/lib/log';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
@ -17,7 +18,7 @@ export const DEPS = { fetch, delayTime: 1000 };
|
|||||||
* An assistant can help a user do things with their document,
|
* An assistant can help a user do things with their document,
|
||||||
* by interfacing with an external LLM endpoint.
|
* by interfacing with an external LLM endpoint.
|
||||||
*/
|
*/
|
||||||
export interface Assistant {
|
interface Assistant {
|
||||||
apply(session: OptDocSession, doc: AssistanceDoc, request: AssistanceRequest): Promise<AssistanceResponse>;
|
apply(session: OptDocSession, doc: AssistanceDoc, request: AssistanceRequest): Promise<AssistanceResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ export interface Assistant {
|
|||||||
* Document-related methods for use in the implementation of assistants.
|
* Document-related methods for use in the implementation of assistants.
|
||||||
* Somewhat ad-hoc currently.
|
* Somewhat ad-hoc currently.
|
||||||
*/
|
*/
|
||||||
export interface AssistanceDoc {
|
interface AssistanceDoc extends ActiveDoc {
|
||||||
/**
|
/**
|
||||||
* Generate a particular prompt coded in the data engine for some reason.
|
* Generate a particular prompt coded in the data engine for some reason.
|
||||||
* It makes python code for some tables, and starts a function body with
|
* It makes python code for some tables, and starts a function body with
|
||||||
@ -117,10 +118,11 @@ export class OpenAIAssistant implements Assistant {
|
|||||||
public async apply(
|
public async apply(
|
||||||
optSession: OptDocSession, doc: AssistanceDoc, request: AssistanceRequest): Promise<AssistanceResponse> {
|
optSession: OptDocSession, doc: AssistanceDoc, request: AssistanceRequest): Promise<AssistanceResponse> {
|
||||||
const messages = request.state?.messages || [];
|
const messages = request.state?.messages || [];
|
||||||
|
const newMessages = [];
|
||||||
const chatMode = this._chatMode;
|
const chatMode = this._chatMode;
|
||||||
if (chatMode) {
|
if (chatMode) {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
messages.push({
|
newMessages.push({
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: 'You are a helpful assistant for a user of software called Grist. ' +
|
content: 'You are a helpful assistant for a user of software called Grist. ' +
|
||||||
'Below are one or more Python classes. ' +
|
'Below are one or more Python classes. ' +
|
||||||
@ -141,7 +143,7 @@ export class OpenAIAssistant implements Assistant {
|
|||||||
await makeSchemaPromptV1(optSession, doc, request) +
|
await makeSchemaPromptV1(optSession, doc, request) +
|
||||||
'\n```',
|
'\n```',
|
||||||
});
|
});
|
||||||
messages.push({
|
newMessages.push({
|
||||||
role: 'user', content: request.text,
|
role: 'user', content: request.text,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -150,21 +152,49 @@ export class OpenAIAssistant implements Assistant {
|
|||||||
messages.pop();
|
messages.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
messages.push({
|
newMessages.push({
|
||||||
role: 'user', content: request.text,
|
role: 'user', content: request.text,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
messages.length = 0;
|
messages.length = 0;
|
||||||
messages.push({
|
newMessages.push({
|
||||||
role: 'user', content: await makeSchemaPromptV1(optSession, doc, request),
|
role: 'user', content: await makeSchemaPromptV1(optSession, doc, request),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
messages.push(...newMessages);
|
||||||
|
|
||||||
|
const newMessagesStartIndex = messages.length - newMessages.length;
|
||||||
|
for (const [index, {role, content}] of newMessages.entries()) {
|
||||||
|
doc.logTelemetryEvent(optSession, 'assistantSend', {
|
||||||
|
full: {
|
||||||
|
conversationId: request.conversationId,
|
||||||
|
context: request.context,
|
||||||
|
prompt: {
|
||||||
|
index: newMessagesStartIndex + index,
|
||||||
|
role,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const completion: string = await this._getCompletion(messages);
|
const completion: string = await this._getCompletion(messages);
|
||||||
const response = await completionToResponse(doc, request, completion, completion);
|
const response = await completionToResponse(doc, request, completion, completion);
|
||||||
if (chatMode) {
|
if (chatMode) {
|
||||||
response.state = {messages};
|
response.state = {messages};
|
||||||
}
|
}
|
||||||
|
doc.logTelemetryEvent(optSession, 'assistantReceive', {
|
||||||
|
full: {
|
||||||
|
conversationId: request.conversationId,
|
||||||
|
context: request.context,
|
||||||
|
message: {
|
||||||
|
index: messages.length - 1,
|
||||||
|
content: completion,
|
||||||
|
},
|
||||||
|
suggestedFormula: (response.suggestedActions[0]?.[3] as any)?.formula,
|
||||||
|
},
|
||||||
|
});
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,7 +337,7 @@ export class HuggingFaceAssistant implements Assistant {
|
|||||||
/**
|
/**
|
||||||
* Test assistant that mimics ChatGPT and just returns the input.
|
* Test assistant that mimics ChatGPT and just returns the input.
|
||||||
*/
|
*/
|
||||||
export class EchoAssistant implements Assistant {
|
class EchoAssistant implements Assistant {
|
||||||
public async apply(sess: OptDocSession, doc: AssistanceDoc, request: AssistanceRequest): Promise<AssistanceResponse> {
|
public async apply(sess: OptDocSession, doc: AssistanceDoc, request: AssistanceRequest): Promise<AssistanceResponse> {
|
||||||
if (request.text === "ERROR") {
|
if (request.text === "ERROR") {
|
||||||
throw new Error(`ERROR`);
|
throw new Error(`ERROR`);
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
TelemetryLevels,
|
TelemetryLevels,
|
||||||
TelemetryMetadata,
|
TelemetryMetadata,
|
||||||
TelemetryMetadataByLevel,
|
TelemetryMetadataByLevel,
|
||||||
|
TelemetryRetentionPeriod,
|
||||||
} from 'app/common/Telemetry';
|
} from 'app/common/Telemetry';
|
||||||
import {TelemetryPrefsWithSources} from 'app/common/InstallAPI';
|
import {TelemetryPrefsWithSources} from 'app/common/InstallAPI';
|
||||||
import {Activation} from 'app/gen-server/entity/Activation';
|
import {Activation} from 'app/gen-server/entity/Activation';
|
||||||
@ -44,19 +45,15 @@ const MAX_PENDING_FORWARD_EVENT_REQUESTS = 25;
|
|||||||
*/
|
*/
|
||||||
export class Telemetry implements ITelemetry {
|
export class Telemetry implements ITelemetry {
|
||||||
private _activation: Activation | undefined;
|
private _activation: Activation | undefined;
|
||||||
private readonly _deploymentType = this._gristServer.getDeploymentType();
|
|
||||||
|
|
||||||
private _telemetryPrefs: TelemetryPrefsWithSources | undefined;
|
private _telemetryPrefs: TelemetryPrefsWithSources | undefined;
|
||||||
|
private readonly _deploymentType = this._gristServer.getDeploymentType();
|
||||||
private readonly _shouldForwardTelemetryEvents = this._deploymentType !== 'saas';
|
private readonly _shouldForwardTelemetryEvents = this._deploymentType !== 'saas';
|
||||||
private readonly _forwardTelemetryEventsUrl = process.env.GRIST_TELEMETRY_URL ||
|
private readonly _forwardTelemetryEventsUrl = process.env.GRIST_TELEMETRY_URL ||
|
||||||
'https://telemetry.getgrist.com/api/telemetry';
|
'https://telemetry.getgrist.com/api/telemetry';
|
||||||
private _numPendingForwardEventRequests = 0;
|
private _numPendingForwardEventRequests = 0;
|
||||||
|
|
||||||
|
|
||||||
private readonly _logger = new LogMethods('Telemetry ', () => ({}));
|
private readonly _logger = new LogMethods('Telemetry ', () => ({}));
|
||||||
private readonly _telemetryLogger = new LogMethods('Telemetry ', () => ({
|
private readonly _telemetryLogger = new LogMethods<string>('Telemetry ', (eventType) => ({
|
||||||
eventType: 'telemetry',
|
eventType,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
private _checkTelemetryEvent: TelemetryEventChecker | undefined;
|
private _checkTelemetryEvent: TelemetryEventChecker | undefined;
|
||||||
@ -153,21 +150,17 @@ export class Telemetry implements ITelemetry {
|
|||||||
*/
|
*/
|
||||||
app.post('/api/telemetry', expressWrap(async (req, resp) => {
|
app.post('/api/telemetry', expressWrap(async (req, resp) => {
|
||||||
const mreq = req as RequestWithLogin;
|
const mreq = req as RequestWithLogin;
|
||||||
const event = stringParam(req.body.event, 'event', TelemetryEvents.values);
|
const event = stringParam(req.body.event, 'event', TelemetryEvents.values) as TelemetryEvent;
|
||||||
if ('eventSource' in req.body.metadata) {
|
if ('eventSource' in req.body.metadata) {
|
||||||
this._telemetryLogger.rawLog('info', null, event, {
|
this._telemetryLogger.rawLog('info', getEventType(event), event, {
|
||||||
eventName: event,
|
|
||||||
...(removeNullishKeys(req.body.metadata)),
|
...(removeNullishKeys(req.body.metadata)),
|
||||||
|
eventName: event,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
this._assertTelemetryIsReady();
|
this._assertTelemetryIsReady();
|
||||||
await this.logEvent(event as TelemetryEvent, merge(
|
await this.logEvent(event, merge(
|
||||||
{
|
{
|
||||||
limited: {
|
|
||||||
eventSource: `grist-${this._deploymentType}`,
|
|
||||||
...(this._deploymentType !== 'saas' ? {installationId: this._activation!.id} : {}),
|
|
||||||
},
|
|
||||||
full: {
|
full: {
|
||||||
userId: mreq.userId,
|
userId: mreq.userId,
|
||||||
altSessionId: mreq.altSessionId,
|
altSessionId: mreq.altSessionId,
|
||||||
@ -219,10 +212,11 @@ export class Telemetry implements ITelemetry {
|
|||||||
event: TelemetryEvent,
|
event: TelemetryEvent,
|
||||||
metadata?: TelemetryMetadata
|
metadata?: TelemetryMetadata
|
||||||
) {
|
) {
|
||||||
this._telemetryLogger.rawLog('info', null, event, {
|
this._telemetryLogger.rawLog('info', getEventType(event), event, {
|
||||||
|
...metadata,
|
||||||
eventName: event,
|
eventName: event,
|
||||||
eventSource: `grist-${this._deploymentType}`,
|
eventSource: `grist-${this._deploymentType}`,
|
||||||
...metadata,
|
installationId: this._activation!.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +232,15 @@ export class Telemetry implements ITelemetry {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this._numPendingForwardEventRequests += 1;
|
this._numPendingForwardEventRequests += 1;
|
||||||
await this._doForwardEvent(JSON.stringify({event, metadata}));
|
await this._doForwardEvent(JSON.stringify({
|
||||||
|
event,
|
||||||
|
metadata: {
|
||||||
|
...metadata,
|
||||||
|
eventName: event,
|
||||||
|
eventSource: `grist-${this._deploymentType}`,
|
||||||
|
installationId: this._activation!.id,
|
||||||
|
}
|
||||||
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._logger.error(undefined, `failed to forward telemetry event ${event}`, e);
|
this._logger.error(undefined, `failed to forward telemetry event ${event}`, e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -342,3 +344,15 @@ export function hashDigestKeys(metadata: TelemetryMetadata): TelemetryMetadata {
|
|||||||
});
|
});
|
||||||
return filteredMetadata;
|
return filteredMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TelemetryEventType = 'telemetry' | 'telemetry-short-retention';
|
||||||
|
|
||||||
|
const EventTypeByRetentionPeriod: Record<TelemetryRetentionPeriod, TelemetryEventType> = {
|
||||||
|
indefinitely: 'telemetry',
|
||||||
|
short: 'telemetry-short-retention',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEventType(event: TelemetryEvent) {
|
||||||
|
const {retentionPeriod} = TelemetryContracts[event];
|
||||||
|
return EventTypeByRetentionPeriod[retentionPeriod];
|
||||||
|
}
|
||||||
|
@ -164,6 +164,7 @@ where c.colId = ? and t.tableId = ?
|
|||||||
formula = colInfo?.formula;
|
formula = colInfo?.formula;
|
||||||
|
|
||||||
const result = await sendForCompletion(session, activeDoc, {
|
const result = await sendForCompletion(session, activeDoc, {
|
||||||
|
conversationId: 'conversationId',
|
||||||
context: {type: 'formula', tableId, colId},
|
context: {type: 'formula', tableId, colId},
|
||||||
state: history,
|
state: history,
|
||||||
text: followUp || description,
|
text: followUp || description,
|
||||||
|
@ -111,6 +111,7 @@ describe('Telemetry', function() {
|
|||||||
eventSource: `grist-${deploymentType}`,
|
eventSource: `grist-${deploymentType}`,
|
||||||
docIdDigest: 'dige:Vq9L3nCkeufQ8euzDkXtM2Fl1cnsALqakjEeM6QlbXQ=',
|
docIdDigest: 'dige:Vq9L3nCkeufQ8euzDkXtM2Fl1cnsALqakjEeM6QlbXQ=',
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
|
installationId,
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -133,6 +134,7 @@ describe('Telemetry', function() {
|
|||||||
docIdDigest: 'dige:Vq9L3nCkeufQ8euzDkXtM2Fl1cnsALqakjEeM6QlbXQ=',
|
docIdDigest: 'dige:Vq9L3nCkeufQ8euzDkXtM2Fl1cnsALqakjEeM6QlbXQ=',
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
userId: 1,
|
userId: 1,
|
||||||
|
installationId,
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -239,6 +241,7 @@ describe('Telemetry', function() {
|
|||||||
eventName: 'watchedVideoTour',
|
eventName: 'watchedVideoTour',
|
||||||
eventSource: `grist-${deploymentType}`,
|
eventSource: `grist-${deploymentType}`,
|
||||||
watchTimeSeconds: 30,
|
watchTimeSeconds: 30,
|
||||||
|
installationId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
assert.containsAllKeys(metadata, [
|
assert.containsAllKeys(metadata, [
|
||||||
@ -298,14 +301,10 @@ describe('Telemetry', function() {
|
|||||||
assert.equal(event, 'watchedVideoTour');
|
assert.equal(event, 'watchedVideoTour');
|
||||||
if (telemetryLevel === 'limited') {
|
if (telemetryLevel === 'limited') {
|
||||||
assert.deepEqual(metadata, {
|
assert.deepEqual(metadata, {
|
||||||
eventSource: `grist-${deploymentType}`,
|
|
||||||
installationId,
|
|
||||||
watchTimeSeconds: 30,
|
watchTimeSeconds: 30,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
assert.containsAllKeys(metadata, [
|
assert.containsAllKeys(metadata, [
|
||||||
'eventSource',
|
|
||||||
'installationId',
|
|
||||||
'watchTimeSeconds',
|
'watchTimeSeconds',
|
||||||
'userId',
|
'userId',
|
||||||
'altSessionId',
|
'altSessionId',
|
||||||
|
Loading…
Reference in New Issue
Block a user