Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit 8d2b9f2

Browse files
authored
dashboard: Analytics (#5827)
* dashboard: Analytics * dashboard: Analytics wording "Telemetry data" => "Telemetry" * Dashboard: Rename server /analytics PUT json payload * dashboard: Use date object instead of millisecond arithmetic To figure out if the user should be prompted again to opt in for analytics * dashboard: Link to analytics policy When asking user to re-consider opting in for analytics
1 parent 013ee4f commit 8d2b9f2

File tree

10 files changed

+202
-5
lines changed

10 files changed

+202
-5
lines changed

packages/dashboard/lib/DashboardServer.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import express, { Application, NextFunction, Request, Response } from "express";
22
import path from "path";
33
import getPort from "get-port";
44
import open from "open";
5+
import { v4 as uuid } from "uuid";
6+
import Config from "@truffle/config";
57
import {
68
dashboardProviderMessageType,
79
LogMessage,
810
logMessageType
911
} from "@truffle/dashboard-message-bus-common";
10-
1112
import { DashboardMessageBus } from "@truffle/dashboard-message-bus";
1213
import { DashboardMessageBusClient } from "@truffle/dashboard-message-bus-client";
1314
import cors from "cors";
@@ -94,6 +95,30 @@ export class DashboardServer {
9495
this.expressApp.post("/rpc", this.postRpc.bind(this));
9596
}
9697

98+
this.expressApp.get("/analytics", (_req, res) => {
99+
const userConfig = Config.getUserConfig();
100+
res.json({
101+
enableAnalytics: userConfig.get("enableAnalytics"),
102+
analyticsSet: userConfig.get("analyticsSet"),
103+
analyticsMessageDateTime: userConfig.get("analyticsMessageDateTime")
104+
});
105+
});
106+
107+
this.expressApp.put("/analytics", (req, _res) => {
108+
const { value } = req.body as { value: boolean };
109+
110+
const userConfig = Config.getUserConfig();
111+
112+
const uid = userConfig.get("uniqueId");
113+
if (!uid) userConfig.set("uniqueId", uuid());
114+
115+
userConfig.set({
116+
enableAnalytics: !!value,
117+
analyticsSet: true,
118+
analyticsMessageDateTime: Date.now()
119+
});
120+
});
121+
97122
this.expressApp.use(express.static(this.frontendPath));
98123
this.expressApp.get("*", (_req, res) => {
99124
res.sendFile("index.html", { root: this.frontendPath });

packages/dashboard/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@mantine/notifications": "^5.0.0",
3939
"@mantine/prism": "^5.0.0",
4040
"@truffle/codec": "^0.14.12",
41+
"@truffle/config": "^1.3.48",
4142
"@truffle/dashboard-message-bus": "^0.1.10",
4243
"@truffle/dashboard-message-bus-client": "^0.1.9",
4344
"@truffle/dashboard-message-bus-common": "^0.1.5",
@@ -56,6 +57,7 @@
5657
"react-dom": "^18.2.0",
5758
"react-feather": "^2.0.10",
5859
"react-router-dom": "^6.3.0",
60+
"uuid": "^9.0.0",
5961
"wagmi": "^0.6.3",
6062
"ws": "^7.2.0"
6163
},

packages/dashboard/src/components/composed/Layout.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1+
import { useEffect } from "react";
12
import { AppShell, createStyles } from "@mantine/core";
3+
import {
4+
showNotification,
5+
updateNotification,
6+
hideNotification
7+
} from "@mantine/notifications";
28
import { Outlet } from "react-router-dom";
39
import { useDash } from "src/hooks";
410
import Sidebar from "src/components/composed/Sidebar";
511
import Notice from "src/components/composed/Notice";
12+
import {
13+
analyticsNotifications,
14+
analyticsNotificationId
15+
} from "src/utils/notifications";
16+
17+
const ARBITRARY_ANALYTICS_NEXT_ASK_THRESHOLD_IN_DAYS = 365;
618

719
const useStyles = createStyles((_theme, _params, _getRef) => ({
820
main: {
@@ -13,10 +25,54 @@ const useStyles = createStyles((_theme, _params, _getRef) => ({
1325

1426
function Layout(): JSX.Element {
1527
const {
16-
state: { notice }
28+
state: { notice, analyticsConfig },
29+
operations: { updateAnalyticsConfig }
1730
} = useDash()!;
1831
const { classes } = useStyles();
1932

33+
// Analytics notifications
34+
useEffect(() => {
35+
// Don't ask if...
36+
if (
37+
// There's currently a fullscreen notice
38+
notice.show ||
39+
// Analytics is already enabled
40+
analyticsConfig.enableAnalytics ||
41+
// Analytics config info is not in state yet
42+
analyticsConfig.analyticsMessageDateTime === null
43+
)
44+
return;
45+
46+
const askAfter = new Date(analyticsConfig.analyticsMessageDateTime);
47+
askAfter.setDate(
48+
askAfter.getDate() + ARBITRARY_ANALYTICS_NEXT_ASK_THRESHOLD_IN_DAYS
49+
);
50+
51+
const shouldAsk = !analyticsConfig.analyticsSet;
52+
const shouldAskAgain = new Date() > askAfter;
53+
54+
const analyticsNotificationArgs = [
55+
() => {
56+
updateAnalyticsConfig(true);
57+
updateNotification(analyticsNotifications["thank"]());
58+
},
59+
() => {
60+
updateAnalyticsConfig(false);
61+
hideNotification(analyticsNotificationId);
62+
}
63+
];
64+
65+
if (shouldAsk) {
66+
showNotification(
67+
analyticsNotifications["ask"](...analyticsNotificationArgs)
68+
);
69+
} else if (shouldAskAgain) {
70+
showNotification(
71+
analyticsNotifications["ask-again"](...analyticsNotificationArgs)
72+
);
73+
}
74+
}, [notice, analyticsConfig, updateAnalyticsConfig]);
75+
2076
return (
2177
<AppShell
2278
navbar={<Sidebar />}

packages/dashboard/src/contexts/DashContext/DashContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface ContextValue {
1313
lifecycle: ReceivedMessageLifecycle<DashboardProviderMessage>
1414
) => any;
1515
toggleNotice: () => void;
16+
updateAnalyticsConfig: (value: boolean) => void;
1617
};
1718
dispatch?: React.Dispatch<Action>;
1819
}

packages/dashboard/src/contexts/DashContext/DashProvider.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ function DashProvider({ children }: DashProviderProps): JSX.Element {
150150
if (initCalled.current) return;
151151
initCalled.current = true;
152152

153+
const { busClient } = state;
154+
const { host, port } = busClient.options;
155+
153156
const initBusClient = async () => {
154-
const { busClient } = state;
155157
await busClient.ready();
156-
const { host, port } = busClient.options;
157158
console.debug(`Connected to message bus at ws://${host}:${port}`);
158159

159160
// Message bus client subscribes to and handles messages
@@ -221,9 +222,17 @@ function DashProvider({ children }: DashProviderProps): JSX.Element {
221222
});
222223
};
223224

225+
const initAnalytics = async () => {
226+
const res = await fetch(`http://${host}:${port}/analytics`);
227+
const data = await res.json();
228+
229+
dispatch({ type: "set-analytics-config", data });
230+
};
231+
224232
const init = async () => {
225233
await initDecoder();
226234
await initBusClient();
235+
await initAnalytics();
227236
};
228237

229238
init();
@@ -268,7 +277,16 @@ function DashProvider({ children }: DashProviderProps): JSX.Element {
268277
lifecycle: ReceivedMessageLifecycle<DashboardProviderMessage>
269278
) => void rejectMessage(lifecycle, "USER"),
270279
toggleNotice: () =>
271-
void dispatch({ type: "set-notice", data: { show: !state.notice.show } })
280+
void dispatch({ type: "set-notice", data: { show: !state.notice.show } }),
281+
updateAnalyticsConfig: async (value: boolean) => {
282+
const { host, port } = state.busClient.options;
283+
await fetch(`http://${host}:${port}/analytics`, {
284+
method: "PUT",
285+
headers: { "Content-Type": "application/json" },
286+
body: JSON.stringify({ value })
287+
});
288+
// No need to update state afterwards
289+
}
272290
};
273291

274292
return (

packages/dashboard/src/contexts/DashContext/state.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export const initialState: State = {
4646
notice: {
4747
show: false,
4848
type: "LOADING"
49+
},
50+
analyticsConfig: {
51+
enableAnalytics: null,
52+
analyticsSet: null,
53+
analyticsMessageDateTime: null
4954
}
5055
};
5156

@@ -58,6 +63,8 @@ export const reducer = (state: State, action: Action): State => {
5863
return { ...state, chainInfo: data };
5964
case "set-notice":
6065
return { ...state, notice: { ...state.notice, ...data } };
66+
case "set-analytics-config":
67+
return { ...state, analyticsConfig: data };
6168
case "handle-message":
6269
// Copy state,
6370
// modify it depending on message type,

packages/dashboard/src/contexts/DashContext/types/Action.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type ActionType =
66
| "set-decoder"
77
| "set-chain-info"
88
| "set-notice"
9+
| "set-analytics-config"
910
| "handle-message";
1011

1112
export interface BaseAction {
@@ -30,6 +31,11 @@ export interface SetNoticeAction extends BaseAction {
3031
data: Partial<State["notice"]>;
3132
}
3233

34+
export interface SetAnalyticsConfigAction extends BaseAction {
35+
type: "set-analytics-config";
36+
data: State["analyticsConfig"];
37+
}
38+
3339
export interface HandleMessageAction extends BaseAction {
3440
type: "handle-message";
3541
data: ReceivedMessageLifecycle<Message>;
@@ -39,4 +45,5 @@ export type Action =
3945
| SetDecoderAction
4046
| SetChainInfoAction
4147
| SetNoticeAction
48+
| SetAnalyticsConfigAction
4249
| HandleMessageAction;

packages/dashboard/src/contexts/DashContext/types/State.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,9 @@ export interface State {
2828
show: boolean;
2929
type: NoticeContent | null;
3030
};
31+
analyticsConfig: {
32+
enableAnalytics: boolean | null;
33+
analyticsSet: boolean | null;
34+
analyticsMessageDateTime: number | null;
35+
};
3136
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Stack, Group, Button, Text, Anchor } from "@mantine/core";
2+
import type { NotificationProps } from "@mantine/notifications";
3+
4+
type NotificationPropsWithId = NotificationProps & { id: string };
5+
6+
export const analyticsNotificationId = "analytics";
7+
8+
const baseNotification = {
9+
autoClose: false,
10+
disallowClose: true,
11+
id: analyticsNotificationId
12+
};
13+
14+
export const analyticsNotifications: Record<
15+
"ask" | "ask-again" | "thank",
16+
(enable?: () => void, disable?: () => void) => NotificationPropsWithId
17+
> = {
18+
"ask": (enable, disable) => ({
19+
...baseNotification,
20+
title: "Help improve Truffle",
21+
message: (
22+
<Stack>
23+
<Text>
24+
You can help us improve Truffle by allowing us to track how you use
25+
our tools. Would you like to share anonymous telemetry? View the
26+
Truffle Analytics policy&nbsp;
27+
<Anchor href="https://trufflesuite.com/analytics" target="_blank">
28+
here
29+
</Anchor>
30+
.
31+
</Text>
32+
<Group grow>
33+
<Button onClick={disable} color="gray" variant="light">
34+
Dismiss
35+
</Button>
36+
<Button onClick={enable} variant="outline">
37+
Enable
38+
</Button>
39+
</Group>
40+
</Stack>
41+
),
42+
color: "yellow"
43+
}),
44+
"ask-again": (enable, disable) => ({
45+
...baseNotification,
46+
title: "Telemetry is currently disabled",
47+
message: (
48+
<Stack>
49+
<Text>
50+
Would you consider enabling it? It helps us make Truffle a better
51+
experience for you. View the Truffle Analytics policy&nbsp;
52+
<Anchor href="https://trufflesuite.com/analytics" target="_blank">
53+
here
54+
</Anchor>
55+
.
56+
</Text>
57+
<Group grow>
58+
<Button onClick={disable} color="gray" variant="light">
59+
No
60+
</Button>
61+
<Button onClick={enable} variant="outline">
62+
Enable
63+
</Button>
64+
</Group>
65+
</Stack>
66+
),
67+
color: "yellow"
68+
}),
69+
"thank": () => ({
70+
...baseNotification,
71+
title: "Telemetry is now enabled",
72+
message: "Thank you!",
73+
autoClose: 1500
74+
})
75+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "src/utils/notifications/decode";
2+
export * from "src/utils/notifications/analytics";

0 commit comments

Comments
 (0)