mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add additional telemetry events
Summary: The new events capture usage of forms, widgets, access rules, and onboarding tours and tips. Test Plan: Manual. Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D4189
This commit is contained in:
@@ -2,7 +2,6 @@ import {makeT} from 'app/client/lib/localization';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight';
|
||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
||||
import {cssBlockedCursor, cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {buildFormulaTriggers} from 'app/client/ui/TriggerFormulas';
|
||||
@@ -359,7 +358,7 @@ export function buildFormulaConfig(
|
||||
dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
|
||||
testId("field-set-trigger")
|
||||
),
|
||||
GristTooltips.setTriggerFormula(),
|
||||
'setTriggerFormula',
|
||||
)),
|
||||
cssRow(textButton(
|
||||
t("Make into data column"),
|
||||
@@ -412,7 +411,7 @@ export function buildFormulaConfig(
|
||||
dom.prop("disabled", disableOtherActions),
|
||||
testId("field-set-trigger")
|
||||
),
|
||||
GristTooltips.setTriggerFormula()
|
||||
'setTriggerFormula'
|
||||
)),
|
||||
])
|
||||
])
|
||||
|
||||
@@ -4,7 +4,6 @@ import GridView from 'app/client/components/GridView';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {ColumnRec} from "app/client/models/entities/ColumnRec";
|
||||
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
|
||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {isNarrowScreen, testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {IconName} from "app/client/ui2018/IconList";
|
||||
@@ -136,7 +135,7 @@ function buildAddNewColumMenuSection(gridView: GridView, index?: number): DomEle
|
||||
},
|
||||
withInfoTooltip(
|
||||
t('Add formula column'),
|
||||
GristTooltips.formulaColumn(),
|
||||
'formulaColumn',
|
||||
{variant: 'hover'}
|
||||
),
|
||||
testId('new-columns-menu-add-formula'),
|
||||
@@ -385,7 +384,7 @@ function buildUUIDMenuItem(gridView: GridView, index?: number) {
|
||||
},
|
||||
withInfoTooltip(
|
||||
t('UUID'),
|
||||
GristTooltips.uuid(),
|
||||
'uuid',
|
||||
{variant: 'hover'}
|
||||
),
|
||||
testId('new-columns-menu-shortcuts-uuid'),
|
||||
@@ -680,7 +679,7 @@ function buildLookupSection(gridView: GridView, index?: number){
|
||||
menuSubHeader(
|
||||
withInfoTooltip(
|
||||
t('Lookups'),
|
||||
GristTooltips.lookups(),
|
||||
'lookups',
|
||||
{variant: 'hover'}
|
||||
),
|
||||
testId('new-columns-menu-lookups'),
|
||||
|
||||
@@ -38,7 +38,7 @@ export type Tooltip =
|
||||
| 'addColumnConditionalStyle'
|
||||
| 'uuid'
|
||||
| 'lookups'
|
||||
| 'formulaColumn'
|
||||
| 'formulaColumn';
|
||||
|
||||
export type TooltipContentFunc = (...domArgs: DomElementArg[]) => DomContents;
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface IOnBoardingMsg {
|
||||
// starting a new one.
|
||||
const tourSingleton = Holder.create<OnBoardingPopupsCtl>(null);
|
||||
|
||||
export function startOnBoarding(messages: IOnBoardingMsg[], onFinishCB: () => void) {
|
||||
export function startOnBoarding(messages: IOnBoardingMsg[], onFinishCB: (lastMessageIndex: number) => void) {
|
||||
const ctl = OnBoardingPopupsCtl.create(tourSingleton, messages, onFinishCB);
|
||||
ctl.start().catch(reportError);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
||||
private _overlay: HTMLElement;
|
||||
private _arrowEl = buildArrow();
|
||||
|
||||
constructor(private _messages: IOnBoardingMsg[], private _onFinishCB: () => void) {
|
||||
constructor(private _messages: IOnBoardingMsg[], private _onFinishCB: (lastMessageIndex: number) => void) {
|
||||
super();
|
||||
if (this._messages.length === 0) {
|
||||
throw new OnBoardingError('messages should not be an empty list');
|
||||
@@ -133,8 +133,8 @@ class OnBoardingPopupsCtl extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
private _finish() {
|
||||
this._onFinishCB();
|
||||
private _finish(lastMessageIndex: number) {
|
||||
this._onFinishCB(lastMessageIndex);
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
@@ -143,9 +143,9 @@ class OnBoardingPopupsCtl extends Disposable {
|
||||
const entry = this._messages[newIndex];
|
||||
if (!entry) {
|
||||
if (maybeClose) {
|
||||
this._finish(ctlIndex);
|
||||
// User finished the tour, close and restart from the beginning if they reopen
|
||||
ctlIndex = 0;
|
||||
this._finish();
|
||||
}
|
||||
return; // gone out of bounds, probably by keyboard shortcut
|
||||
}
|
||||
@@ -266,7 +266,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
||||
this._arrowEl,
|
||||
ContentWrapper(
|
||||
cssCloseButton(cssBigIcon('CrossBig'),
|
||||
dom.on('click', () => this._finish()),
|
||||
dom.on('click', () => this._finish(ctlIndex)),
|
||||
testId('close'),
|
||||
),
|
||||
cssTitle(this._messages[ctlIndex].title),
|
||||
@@ -275,7 +275,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
||||
testId('popup'),
|
||||
),
|
||||
dom.onKeyDown({
|
||||
Escape: () => this._finish(),
|
||||
Escape: () => this._finish(ctlIndex),
|
||||
ArrowLeft: () => this._move(-1),
|
||||
ArrowRight: () => this._move(+1),
|
||||
Enter: () => this._move(+1, true),
|
||||
|
||||
@@ -4,7 +4,6 @@ import {makeT} from 'app/client/lib/localization';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ColumnRec, TableRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {PERMITTED_CUSTOM_WIDGETS} from "app/client/models/features";
|
||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
||||
import {linkId, NoLink} from 'app/client/ui/selectBy';
|
||||
import {overflowTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {getWidgetTypes} from "app/client/ui/widgetTypesMap";
|
||||
@@ -394,7 +393,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
dom.update(cssSelect(this._value.link, this._selectByOptions!),
|
||||
testId('selectby'))
|
||||
),
|
||||
GristTooltips.selectBy(),
|
||||
'selectBy',
|
||||
{popupOptions: {attach: null}, domArgs: [
|
||||
this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
|
||||
popupOptions: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {createGroup} from 'app/client/components/commands';
|
||||
import {duplicatePage} from 'app/client/components/duplicatePage';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||
import {PageRec} from 'app/client/models/DocModel';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import MetaTableModel from 'app/client/models/MetaTableModel';
|
||||
@@ -83,6 +84,8 @@ function buildDomFromTable(pagesTable: MetaTableModel<PageRec>, activeDoc: Grist
|
||||
}
|
||||
|
||||
function removeView(activeDoc: GristDoc, viewId: number, pageName: string) {
|
||||
logTelemetryEvent('deletedPage', {full: {docIdDigest: activeDoc.docId()}});
|
||||
|
||||
const docData = activeDoc.docData;
|
||||
// Create a set with tables on other pages (but not on this one).
|
||||
const tablesOnOtherViews = new Set(activeDoc.docModel.viewSections.rowModels
|
||||
|
||||
@@ -25,6 +25,7 @@ import {domAsync} from 'app/client/lib/domAsync';
|
||||
import * as imports from 'app/client/lib/imports';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {createSessionObs, isBoolean, SessionObs} from 'app/client/lib/sessionObs';
|
||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {CustomSectionConfig} from 'app/client/ui/CustomSectionConfig';
|
||||
@@ -35,9 +36,9 @@ import {textarea} from 'app/client/ui/inputs';
|
||||
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||
import {PredefinedCustomSectionConfig} from "app/client/ui/PredefinedCustomSectionConfig";
|
||||
import {cssLabel} from 'app/client/ui/RightPanelStyles';
|
||||
import {linkId, selectBy} from 'app/client/ui/selectBy';
|
||||
import {linkId, NoLink, selectBy} from 'app/client/ui/selectBy';
|
||||
import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
|
||||
import {widgetTypesMap} from "app/client/ui/widgetTypesMap";
|
||||
import {getTelemetryWidgetTypeFromVS, widgetTypesMap} from "app/client/ui/widgetTypesMap";
|
||||
import {basicButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||
import {buttonSelect} from 'app/client/ui2018/buttonSelect';
|
||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
@@ -792,7 +793,16 @@ export class RightPanel extends Disposable {
|
||||
);
|
||||
});
|
||||
|
||||
link.onWrite((val) => this._gristDoc.saveLink(val));
|
||||
link.onWrite(async (val) => {
|
||||
const widgetType = getTelemetryWidgetTypeFromVS(activeSection);
|
||||
if (val !== NoLink) {
|
||||
logTelemetryEvent('linkedWidget', {full: {docIdDigest: this._gristDoc.docId(), widgetType}});
|
||||
} else {
|
||||
logTelemetryEvent('unlinkedWidget', {full: {docIdDigest: this._gristDoc.docId(), widgetType}});
|
||||
}
|
||||
|
||||
await this._gristDoc.saveLink(val);
|
||||
});
|
||||
return [
|
||||
this._disableIfReadonly(),
|
||||
cssLabel(t("DATA TABLE")),
|
||||
|
||||
@@ -3,7 +3,6 @@ import {loadUserManager} from 'app/client/lib/imports';
|
||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {DocInfo, DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {docUrl, getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
||||
import {downloadDocModal, makeCopy, replaceTrunkWithFork} from 'app/client/ui/MakeCopyMenu';
|
||||
import {sendToDrive} from 'app/client/ui/sendToDrive';
|
||||
import {hoverTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
@@ -255,7 +254,7 @@ function menuWorkOnCopy(pageModel: DocPageModel) {
|
||||
menuText(
|
||||
withInfoTooltip(
|
||||
t("Edit without affecting the original"),
|
||||
GristTooltips.workOnACopy(),
|
||||
'workOnACopy',
|
||||
{popupOptions: {attach: null}}
|
||||
)
|
||||
),
|
||||
|
||||
@@ -27,7 +27,6 @@ import {IEditableMember, IMemberSelectOption, IOrgMemberSelectOption,
|
||||
Resource} from 'app/client/models/UserManagerModel';
|
||||
import {UserManagerModel, UserManagerModelImpl} from 'app/client/models/UserManagerModel';
|
||||
import {getResourceParent, ResourceType} from 'app/client/models/UserManagerModel';
|
||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
||||
import {shadowScroll} from 'app/client/ui/shadowScroll';
|
||||
import {hoverTooltip, ITooltipControl, showTransientTooltip, withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {createUserImage} from 'app/client/ui/UserImage';
|
||||
@@ -187,7 +186,7 @@ function buildUserManagerModal(
|
||||
}),
|
||||
testId('um-open-access-rules'),
|
||||
),
|
||||
GristTooltips.openAccessRules(),
|
||||
'openAccessRules',
|
||||
{domArgs: [cssAccessLink.cls('')]},
|
||||
)
|
||||
: null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { logTelemetryEvent } from 'app/client/lib/telemetry';
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import { urlState } from 'app/client/models/gristUrlState';
|
||||
import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups";
|
||||
@@ -100,7 +101,15 @@ export function getOnBoardingMessages(): IOnBoardingMsg[] {
|
||||
|
||||
export function startWelcomeTour(onFinishCB: () => void) {
|
||||
commands.allCommands.fieldTabOpen.run();
|
||||
startOnBoarding(getOnBoardingMessages(), onFinishCB);
|
||||
const messages = getOnBoardingMessages();
|
||||
startOnBoarding(messages, (lastMessageIndex) => {
|
||||
logTelemetryEvent('viewedWelcomeTour', {
|
||||
full: {
|
||||
percentComplete: Math.floor(((lastMessageIndex + 1) / messages.length) * 100),
|
||||
},
|
||||
});
|
||||
onFinishCB();
|
||||
});
|
||||
}
|
||||
|
||||
const TopBarButtonIcon = styled(icon, `
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* - to be shown briefly, as a transient notification next to some action element.
|
||||
*/
|
||||
|
||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||
import {GristTooltips, Tooltip} from 'app/client/ui/GristTooltips';
|
||||
import {prepareForTransition} from 'app/client/ui/transitions';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@@ -312,6 +314,8 @@ export interface InfoTooltipOptions {
|
||||
variant?: InfoTooltipVariant;
|
||||
/** Only applicable to the `click` variant. */
|
||||
popupOptions?: IPopupOptions;
|
||||
/** Only applicable to the `click` variant. */
|
||||
onOpen?: () => void;
|
||||
}
|
||||
|
||||
export type InfoTooltipVariant = 'click' | 'hover';
|
||||
@@ -320,33 +324,42 @@ export type InfoTooltipVariant = 'click' | 'hover';
|
||||
* Renders an info icon that shows a tooltip with the specified `content`.
|
||||
*/
|
||||
export function infoTooltip(
|
||||
content: DomContents,
|
||||
tooltip: Tooltip,
|
||||
options: InfoTooltipOptions = {},
|
||||
...domArgs: DomElementArg[]
|
||||
) {
|
||||
const {variant = 'click'} = options;
|
||||
const content = GristTooltips[tooltip]();
|
||||
const onOpen = () => logTelemetryEvent('viewedTip', {full: {tipName: tooltip}});
|
||||
switch (variant) {
|
||||
case 'click': {
|
||||
const {popupOptions} = options;
|
||||
return buildClickableInfoTooltip(content, popupOptions, domArgs);
|
||||
return buildClickableInfoTooltip(content, {onOpen, popupOptions}, domArgs);
|
||||
}
|
||||
case 'hover': {
|
||||
return buildHoverableInfoTooltip(content, domArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClickableInfoTooltipOptions {
|
||||
popupOptions?: IPopupOptions;
|
||||
onOpen?: () => void;
|
||||
}
|
||||
|
||||
function buildClickableInfoTooltip(
|
||||
content: DomContents,
|
||||
popupOptions?: IPopupOptions,
|
||||
options: ClickableInfoTooltipOptions = {},
|
||||
...domArgs: DomElementArg[]
|
||||
) {
|
||||
const {onOpen, popupOptions} = options;
|
||||
return cssInfoTooltipButton('?',
|
||||
(elem) => {
|
||||
setPopupToCreateDom(
|
||||
elem,
|
||||
(ctl) => {
|
||||
onOpen?.();
|
||||
|
||||
return cssInfoTooltipPopup(
|
||||
cssInfoTooltipPopupCloseButton(
|
||||
icon('CrossSmall'),
|
||||
@@ -395,11 +408,13 @@ export interface WithInfoTooltipOptions {
|
||||
iconDomArgs?: DomElementArg[];
|
||||
/** Only applicable to the `click` variant. */
|
||||
popupOptions?: IPopupOptions;
|
||||
onOpen?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps `domContent` with a info tooltip icon that displays the provided
|
||||
* `tooltipContent` and returns the wrapped element.
|
||||
* Wraps `domContent` with a info tooltip icon that displays the specified
|
||||
* `tooltip` and returns the wrapped element. Tooltips are defined in
|
||||
* `app/client/ui/GristTooltips.ts`.
|
||||
*
|
||||
* The tooltip button is displayed to the right of `domContents`, and displays
|
||||
* a popup on click by default. The popup can be dismissed by clicking away from
|
||||
@@ -414,20 +429,17 @@ export interface WithInfoTooltipOptions {
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* withInfoTooltip(
|
||||
* dom('div', 'Hello World!'),
|
||||
* dom('p', 'This is some text to show inside the tooltip.'),
|
||||
* )
|
||||
* withInfoTooltip(dom('div', 'Hello World!'), 'selectBy')
|
||||
*/
|
||||
export function withInfoTooltip(
|
||||
domContents: DomContents,
|
||||
tooltipContent: DomContents,
|
||||
tooltip: Tooltip,
|
||||
options: WithInfoTooltipOptions = {},
|
||||
) {
|
||||
const {variant = 'click', domArgs, iconDomArgs, popupOptions} = options;
|
||||
return cssDomWithTooltip(
|
||||
domContents,
|
||||
infoTooltip(tooltipContent, {variant, popupOptions}, iconDomArgs),
|
||||
infoTooltip(tooltip, {variant, popupOptions}, iconDomArgs),
|
||||
...(domArgs ?? [])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// the list of widget types with their labels and icons
|
||||
import {IWidgetType} from "app/common/widgetTypes";
|
||||
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
|
||||
import {IPageWidget} from "app/client/ui/PageWidgetPicker";
|
||||
import {IconName} from "app/client/ui2018/IconList";
|
||||
import {IWidgetType} from "app/common/widgetTypes";
|
||||
|
||||
export const widgetTypesMap = new Map<IWidgetType, IWidgetTypeInfo>([
|
||||
['record', {label: 'Table', icon: 'TypeTable'}],
|
||||
@@ -22,3 +24,37 @@ export interface IWidgetTypeInfo {
|
||||
export function getWidgetTypes(sectionType: IWidgetType | null): IWidgetTypeInfo {
|
||||
return widgetTypesMap.get(sectionType || 'record') || widgetTypesMap.get('record')!;
|
||||
}
|
||||
|
||||
export interface GetTelemetryWidgetTypeOptions {
|
||||
/** Defaults to `false`. */
|
||||
isSummary?: boolean;
|
||||
/** Defaults to `false`. */
|
||||
isNewTable?: boolean;
|
||||
}
|
||||
|
||||
export function getTelemetryWidgetTypeFromVS(vs: ViewSectionRec) {
|
||||
return getTelemetryWidgetType(vs.widgetType.peek(), {
|
||||
isSummary: vs.table.peek().summarySourceTable.peek() !== 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function getTelemetryWidgetTypeFromPageWidget(widget: IPageWidget) {
|
||||
return getTelemetryWidgetType(widget.type, {
|
||||
isNewTable: widget.table === 'New Table',
|
||||
isSummary: widget.summarize,
|
||||
});
|
||||
}
|
||||
|
||||
function getTelemetryWidgetType(type: IWidgetType, options: GetTelemetryWidgetTypeOptions = {}) {
|
||||
let telemetryWidgetType: string | undefined = widgetTypesMap.get(type)?.label;
|
||||
if (!telemetryWidgetType) { return undefined; }
|
||||
|
||||
if (options.isNewTable) {
|
||||
telemetryWidgetType = 'New ' + telemetryWidgetType;
|
||||
}
|
||||
if (options.isSummary) {
|
||||
telemetryWidgetType += ' (Summary)';
|
||||
}
|
||||
|
||||
return telemetryWidgetType;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user