mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add Support Grist page and nudge
Summary: Adds a new Support Grist page (accessible only in grist-core), containing options to opt in to telemetry and sponsor Grist Labs on GitHub. A nudge is also shown in the doc menu, which can be collapsed or permanently dismissed. Test Plan: Browser and server tests. Reviewers: paulfitz, dsagal Reviewed By: paulfitz Subscribers: jarek, dsagal Differential Revision: https://phab.getgrist.com/D3926
This commit is contained in:
10
app/common/Install.ts
Normal file
10
app/common/Install.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {TelemetryLevel} from 'app/common/Telemetry';
|
||||
|
||||
export interface InstallPrefs {
|
||||
telemetry?: TelemetryPrefs;
|
||||
}
|
||||
|
||||
export interface TelemetryPrefs {
|
||||
/** Defaults to "off". */
|
||||
telemetryLevel?: TelemetryLevel;
|
||||
}
|
||||
51
app/common/InstallAPI.ts
Normal file
51
app/common/InstallAPI.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import {BaseAPI, IOptions} from 'app/common/BaseAPI';
|
||||
import {InstallPrefs} from 'app/common/Install';
|
||||
import {TelemetryLevel} from 'app/common/Telemetry';
|
||||
import {addCurrentOrgToPath} from 'app/common/urlUtils';
|
||||
|
||||
export const installPropertyKeys = ['prefs'];
|
||||
|
||||
export interface InstallProperties {
|
||||
prefs: InstallPrefs;
|
||||
}
|
||||
|
||||
export interface InstallPrefsWithSources {
|
||||
telemetry: {
|
||||
telemetryLevel: PrefWithSource<TelemetryLevel>;
|
||||
},
|
||||
}
|
||||
|
||||
export type TelemetryPrefsWithSources = InstallPrefsWithSources['telemetry'];
|
||||
|
||||
export interface PrefWithSource<T> {
|
||||
value: T;
|
||||
source: PrefSource;
|
||||
}
|
||||
|
||||
export type PrefSource = 'environment-variable' | 'preferences';
|
||||
|
||||
export interface InstallAPI {
|
||||
getInstallPrefs(): Promise<InstallPrefsWithSources>;
|
||||
updateInstallPrefs(prefs: Partial<InstallPrefs>): Promise<void>;
|
||||
}
|
||||
|
||||
export class InstallAPIImpl extends BaseAPI implements InstallAPI {
|
||||
constructor(private _homeUrl: string, options: IOptions = {}) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
public async getInstallPrefs(): Promise<InstallPrefsWithSources> {
|
||||
return this.requestJson(`${this._url}/api/install/prefs`, {method: 'GET'});
|
||||
}
|
||||
|
||||
public async updateInstallPrefs(prefs: Partial<InstallPrefs>): Promise<void> {
|
||||
await this.request(`${this._url}/api/install/prefs`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({...prefs}),
|
||||
});
|
||||
}
|
||||
|
||||
private get _url(): string {
|
||||
return addCurrentOrgToPath(this._homeUrl);
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,7 @@ export const DismissedPopup = StringUnion(
|
||||
'tutorialFirstCard', // first card of the tutorial,
|
||||
'formulaHelpInfo', // formula help info shown in the popup editor,
|
||||
'formulaAssistantInfo', // formula assistant info shown in the popup editor,
|
||||
'supportGrist', // nudge to opt in to telemetry,
|
||||
);
|
||||
export type DismissedPopup = typeof DismissedPopup.type;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {StringUnion} from 'app/common/StringUnion';
|
||||
import pickBy = require('lodash/pickBy');
|
||||
|
||||
/**
|
||||
* Telemetry levels, in increasing order of data collected.
|
||||
@@ -720,36 +719,3 @@ export function buildTelemetryEventChecker(telemetryLevel: TelemetryLevel) {
|
||||
}
|
||||
|
||||
export type TelemetryEventChecker = (event: TelemetryEvent, metadata?: TelemetryMetadata) => void;
|
||||
|
||||
/**
|
||||
* Returns a new, filtered metadata object.
|
||||
*
|
||||
* Metadata in groups that don't meet `telemetryLevel` are removed from the
|
||||
* returned object, and the returned object is flattened.
|
||||
*
|
||||
* Returns undefined if `metadata` is undefined.
|
||||
*/
|
||||
export function filterMetadata(
|
||||
metadata: TelemetryMetadataByLevel | undefined,
|
||||
telemetryLevel: TelemetryLevel
|
||||
): TelemetryMetadata | undefined {
|
||||
if (!metadata) { return; }
|
||||
|
||||
let filteredMetadata = {};
|
||||
for (const level of ['limited', 'full'] as const) {
|
||||
if (Level[telemetryLevel] < Level[level]) { break; }
|
||||
|
||||
filteredMetadata = {...filteredMetadata, ...metadata[level]};
|
||||
}
|
||||
|
||||
filteredMetadata = removeNullishKeys(filteredMetadata);
|
||||
|
||||
return removeNullishKeys(filteredMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of `object` with all null and undefined keys removed.
|
||||
*/
|
||||
export function removeNullishKeys(object: Record<string, any>) {
|
||||
return pickBy(object, value => value !== null && value !== undefined);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ export type ActivationPage = typeof ActivationPage.type;
|
||||
export const LoginPage = StringUnion('signup', 'login', 'verified', 'forgot-password');
|
||||
export type LoginPage = typeof LoginPage.type;
|
||||
|
||||
export const SupportGristPage = StringUnion('support-grist');
|
||||
export type SupportGristPage = typeof SupportGristPage.type;
|
||||
|
||||
// Overall UI style. "full" is normal, "light" is a single page focused, panels hidden experience.
|
||||
export const InterfaceStyle = StringUnion('light', 'full');
|
||||
export type InterfaceStyle = typeof InterfaceStyle.type;
|
||||
@@ -72,6 +75,7 @@ export const commonUrls = {
|
||||
helpTriggerFormulas: "https://support.getgrist.com/formulas/#trigger-formulas",
|
||||
helpTryingOutChanges: "https://support.getgrist.com/copying-docs/#trying-out-changes",
|
||||
helpCustomWidgets: "https://support.getgrist.com/widget-custom",
|
||||
helpTelemetryLimited: "https://support.getgrist.com/telemetry-limited",
|
||||
plans: "https://www.getgrist.com/pricing",
|
||||
sproutsProgram: "https://www.getgrist.com/sprouts-program",
|
||||
contact: "https://www.getgrist.com/contact",
|
||||
@@ -83,6 +87,8 @@ export const commonUrls = {
|
||||
basicTutorialImage: 'https://www.getgrist.com/wp-content/uploads/2021/08/lightweight-crm.png',
|
||||
gristLabsCustomWidgets: 'https://gristlabs.github.io/grist-widget/',
|
||||
gristLabsWidgetRepository: 'https://github.com/gristlabs/grist-widget/releases/download/latest/manifest.json',
|
||||
githubGristCore: 'https://github.com/gristlabs/grist-core',
|
||||
githubSponsorGristLabs: 'https://github.com/sponsors/gristlabs',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -103,6 +109,7 @@ export interface IGristUrlState {
|
||||
activation?: ActivationPage;
|
||||
login?: LoginPage;
|
||||
welcome?: WelcomePage;
|
||||
supportGrist?: SupportGristPage;
|
||||
welcomeTour?: boolean;
|
||||
docTour?: boolean;
|
||||
manageUsers?: boolean;
|
||||
@@ -258,6 +265,8 @@ export function encodeUrl(gristConfig: Partial<GristLoadConfig>,
|
||||
parts.push(`welcome/${state.welcome}`);
|
||||
}
|
||||
|
||||
if (state.supportGrist) { parts.push(state.supportGrist); }
|
||||
|
||||
const queryParams = pickBy(state.params, (v, k) => k !== 'linkParameters') as {[key: string]: string};
|
||||
for (const [k, v] of Object.entries(state.params?.linkParameters || {})) {
|
||||
queryParams[`${k}_`] = v;
|
||||
@@ -320,7 +329,7 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
|
||||
// the minimum length of a urlId prefix is longer than the maximum length
|
||||
// of any of the valid keys in the url.
|
||||
for (const key of map.keys()) {
|
||||
if (key.length >= MIN_URLID_PREFIX_LENGTH && !LoginPage.guard(key)) {
|
||||
if (key.length >= MIN_URLID_PREFIX_LENGTH && !LoginPage.guard(key) && !SupportGristPage.guard(key)) {
|
||||
map.set('doc', key);
|
||||
map.set('slug', map.get(key)!);
|
||||
map.delete(key);
|
||||
@@ -358,6 +367,9 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
|
||||
state.activation = ActivationPage.parse(map.get('activation')) || 'activation';
|
||||
}
|
||||
if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')); }
|
||||
if (map.has('support-grist')) {
|
||||
state.supportGrist = SupportGristPage.parse(map.get('support-grist')) || 'support-grist';
|
||||
}
|
||||
if (sp.has('planType')) { state.params!.planType = sp.get('planType')!; }
|
||||
if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; }
|
||||
if (sp.has('billingTask')) {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import {createHash} from 'crypto';
|
||||
|
||||
/**
|
||||
* Returns a hash of `id` prefixed with the first 4 characters of `id`.
|
||||
*
|
||||
* Useful for situations where potentially sensitive identifiers are logged, such as
|
||||
* doc ids (like those that have public link sharing enabled). The first 4 characters
|
||||
* are included to assist with troubleshooting.
|
||||
*/
|
||||
export function hashId(id: string): string {
|
||||
return `${id.slice(0, 4)}:${createHash('sha256').update(id.slice(4)).digest('base64')}`;
|
||||
}
|
||||
Reference in New Issue
Block a user