Skip to content

Commit 42eed02

Browse files
SgtPookiwhizzzkidlidel
authored
feat: add telemetry to companion (#1117)
* feat: add types for state * tmp * feat: ipfs-companion tracks views and sessions * Update add-on/src/lib/telemetry.js Co-authored-by: Nishant Arora <[email protected]> * Update add-on/src/lib/telemetry.js Co-authored-by: Nishant Arora <[email protected]> * Update add-on/src/lib/telemetry.js Co-authored-by: Nishant Arora <[email protected]> * Update add-on/_locales/en/messages.json Co-authored-by: Marcin Rataj <[email protected]> * Update add-on/_locales/en/messages.json Co-authored-by: Marcin Rataj <[email protected]> * Update add-on/_locales/en/messages.json Co-authored-by: Marcin Rataj <[email protected]> * Update add-on/_locales/en/messages.json Co-authored-by: Marcin Rataj <[email protected]> * chore: fix options and state typings * chore: use debug logger * fix(lint): remove unused method * fix(lint): run 'npm run fix:lint' * chore: build and lint success * chore(types): fix type errors * chore: add docs/telemetry/COLLECTED_DATA.md see ipfs-shipyard/ignite-metrics#35 * chore: update old metric group names in logConsent * chore: clean up UI * chore: use ignite-metrics from npm * chore: update ignite-metrics and some types * fix(tests): tests dont fail on countly-sdk-web import * fix: build * chore: temporarily use updated ignite-metrics * chore: use deployed ignite-metrics version * chore: address PR comments * use latest ignite-metrics library * don't use singleton function for grabbing metricsProvider * ensure metrics initialize and update properly * chore: pin ignite-metrics dependency * chore(lint): fix lint errors * fix: use browser.runtime.sendMessage * Telemetry messages are passed between contexts using browser.runtime * Upgraded to @ipfs-shipyard/[email protected] * Updated consent handling from state to be more explicit --------- Co-authored-by: Nishant Arora <[email protected]> Co-authored-by: Marcin Rataj <[email protected]>
1 parent 7f11f86 commit 42eed02

File tree

23 files changed

+4336
-403
lines changed

23 files changed

+4336
-403
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@
1515
/coverage
1616
/.nyc_output
1717
/add-on/manifest.json
18+
19+
.DS_Store
20+
.vscode

add-on/_locales/en/messages.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,5 +710,45 @@
710710
"page_landingWelcome_projects_title": {
711711
"message": "Related Projects",
712712
"description": "Projects section title (page_landingWelcome_projects_title)"
713+
},
714+
"option_header_telemetry": {
715+
"message": "Telemetry",
716+
"description": "A section header on the Preferences screen (option_header_telemetry)"
717+
},
718+
"option_telemetry_disclaimer": {
719+
"message": "We're collecting minimal telemetry data to improve and prioritize our work. Please consent to the collection of these metrics to assist in our efforts!",
720+
"description": "Disclaimer about telemetry collection in the telemetry section on the Preferences screen (option_telemetry_disclaimer)"
721+
},
722+
"option_telemetryGroupMinimal_title": {
723+
"message": "Feature Telemetry",
724+
"description": "A title for the 'minimal' grouping of metrics we collect (option_telemetryGroupMinimal_title)"
725+
},
726+
"option_telemetryGroupMinimal_description": {
727+
"message": "Collect basic feature and usage metrics to help maintainers to prioritize work on the most useful features.",
728+
"description": "A description for the 'minimal' grouping of metrics we collect (option_telemetryGroupMinimal_description)"
729+
},
730+
"option_telemetryGroupMarketing_title": {
731+
"message": "Marketing title",
732+
"description": "A title for the 'marketing' grouping of metrics we collect (option_telemetryGroupMarketing_title)"
733+
},
734+
"option_telemetryGroupMarketing_description": {
735+
"message": "Marketing description",
736+
"description": "A description for the 'marketing' grouping of metrics we collect (option_telemetryGroupMarketing_description)"
737+
},
738+
"option_telemetryGroupPerformance_title": {
739+
"message": "Performance title",
740+
"description": "A title for the 'performance' grouping of metrics we collect (option_telemetryGroupPerformance_title)"
741+
},
742+
"option_telemetryGroupPerformance_description": {
743+
"message": "Performance description",
744+
"description": "A description for the 'performance' grouping of metrics we collect (option_telemetryGroupPerformance_description)"
745+
},
746+
"option_telemetryGroupTracking_title": {
747+
"message": "Tracking title",
748+
"description": "A title for the 'tracking' grouping of metrics we collect (option_telemetryGroupTracking_title)"
749+
},
750+
"option_telemetryGroupTracking_description": {
751+
"message": "Tracking description",
752+
"description": "A description for the 'tracking' grouping of metrics we collect (option_telemetryGroupTracking_description)"
713753
}
714754
}

add-on/src/background/background.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
<meta charset="utf-8">
33
<script src="/dist/bundles/ipfs.bundle.js"></script>
44
<script src="/dist/bundles/backgroundPage.bundle.js"></script>
5+
<body></body>

add-on/src/background/background.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ browser.runtime.setUninstallURL(getUninstallURL(browser))
1313

1414
// init add-on after all libs are loaded
1515
document.addEventListener('DOMContentLoaded', async () => {
16+
browser.runtime.sendMessage({ telemetry: { trackView: 'background' } })
1617
// setting debug namespaces require page reload to get applied
1718
const debugNs = (await browser.storage.local.get({ logNamespaces: optionDefaults.logNamespaces })).logNamespaces
1819
if (debugNs !== localStorage.debug) {
1920
localStorage.debug = debugNs
2021
window.location.reload()
2122
}
2223
// init inlined to read updated localStorage.debug
24+
// @ts-expect-error - TS does not know about window.ipfsCompanion
2325
window.ipfsCompanion = await createIpfsCompanion()
2426
})

add-on/src/landing-pages/welcome/page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const renderCompanionLogo = (i18n, isIpfsOnline) => {
5555

5656
return html`
5757
<div class="mt4 mb2 flex flex-column justify-center items-center transition-all ${stateUnknown && 'state-unknown'}">
58-
${logo({ path: logoPath, size: logoSize, isIpfsOnline: isIpfsOnline })}
58+
${logo({ path: logoPath, size: logoSize, isIpfsOnline })}
5959
<p class="montserrat mt3 mb0 f2">${i18n.getMessage('page_landingWelcome_logo_title')}</p>
6060
</div>
6161
`

add-on/src/landing-pages/welcome/store.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default function createWelcomePageStore (i18n, runtime) {
99
state.webuiRootUrl = null
1010
let port
1111
emitter.on('DOMContentLoaded', async () => {
12+
browser.runtime.sendMessage({ telemetry: { trackView: 'welcome' } })
1213
emitter.emit('render')
1314
port = runtime.connect({ name: 'browser-action-port' })
1415
port.onMessage.addListener(async (message) => {

add-on/src/lib/ipfs-client/brave.js

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,39 +16,39 @@ const waitFor = (f, t) => pWaitFor(f, { interval: tickMs, timeout: t || Infinity
1616
// wrapper for chrome.ipfs.* that gets us closer to ergonomics of promise-based browser.*
1717
export const brave = hasBraveChromeIpfs()
1818
? Object.freeze({
19-
// This is the main check - returns true only in Brave and only when
20-
// feature flag is enabled brave://flags and can be used for high level UI
21-
// decisions such as showing custom node type on Preferences
22-
getIPFSEnabled: async () =>
23-
Boolean(await promisifyBraveCheck(chrome.ipfs.getIPFSEnabled)),
24-
25-
// Obtains a string representation of the resolve method
26-
// method is one of the following strings:
27-
// "ask" uses a gateway but also prompts them to install a local node
28-
// "gateway" uses a gateway but also prompts them to install a local node
29-
// "local" uses a gateway but also prompts them to install a local node
30-
// "disabled" disabled by the user
31-
// "undefined" everything else (IPFS feature flag is not enabled, error etc)
32-
getResolveMethodType: async () =>
33-
String(await promisifyBraveCheck(chrome.ipfs.getResolveMethodType)),
34-
35-
// Obtains the config contents of the local IPFS node
36-
// Returns undefined if missing for any reason
37-
getConfig: async () =>
38-
await promisifyBraveCheck(chrome.ipfs.getConfig),
39-
40-
// Returns true if binary is present
41-
getExecutableAvailable: async () =>
42-
Boolean(await promisifyBraveCheck(chrome.ipfs.getExecutableAvailable)),
43-
44-
// Attempts to start the daemon and returns true if finished
45-
launch: async () =>
46-
Boolean(await promisifyBraveCheck(chrome.ipfs.launch)),
47-
48-
// Attempts to stop the daemon and returns true if finished
49-
shutdown: async () =>
50-
Boolean(await promisifyBraveCheck(chrome.ipfs.shutdown))
51-
})
19+
// This is the main check - returns true only in Brave and only when
20+
// feature flag is enabled brave://flags and can be used for high level UI
21+
// decisions such as showing custom node type on Preferences
22+
getIPFSEnabled: async () =>
23+
Boolean(await promisifyBraveCheck(chrome.ipfs.getIPFSEnabled)),
24+
25+
// Obtains a string representation of the resolve method
26+
// method is one of the following strings:
27+
// "ask" uses a gateway but also prompts them to install a local node
28+
// "gateway" uses a gateway but also prompts them to install a local node
29+
// "local" uses a gateway but also prompts them to install a local node
30+
// "disabled" disabled by the user
31+
// "undefined" everything else (IPFS feature flag is not enabled, error etc)
32+
getResolveMethodType: async () =>
33+
String(await promisifyBraveCheck(chrome.ipfs.getResolveMethodType)),
34+
35+
// Obtains the config contents of the local IPFS node
36+
// Returns undefined if missing for any reason
37+
getConfig: async () =>
38+
await promisifyBraveCheck(chrome.ipfs.getConfig),
39+
40+
// Returns true if binary is present
41+
getExecutableAvailable: async () =>
42+
Boolean(await promisifyBraveCheck(chrome.ipfs.getExecutableAvailable)),
43+
44+
// Attempts to start the daemon and returns true if finished
45+
launch: async () =>
46+
Boolean(await promisifyBraveCheck(chrome.ipfs.launch)),
47+
48+
// Attempts to stop the daemon and returns true if finished
49+
shutdown: async () =>
50+
Boolean(await promisifyBraveCheck(chrome.ipfs.shutdown))
51+
})
5252
: undefined
5353

5454
export async function init (browser, opts) {

add-on/src/lib/ipfs-companion.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import createRuntimeChecks from './runtime-checks.js'
2323
import { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway, contextMenuCopyPermalink, contextMenuCopyCidAddress } from './context-menus.js'
2424
import { registerSubdomainProxy } from './http-proxy.js'
2525
import { runPendingOnInstallTasks } from './on-installed.js'
26+
import { handleConsentFromState, startSession, endSession, trackView } from './telemetry.js'
2627
const log = debug('ipfs-companion:main')
2728
log.error = debug('ipfs-companion:main:error')
2829

@@ -33,6 +34,7 @@ export default async function init () {
3334
// INIT
3435
// ===================================================================
3536
let ipfs // ipfs-api instance
37+
/** @type {import('../types.js').CompanionState} */
3638
let state // avoid redundant API reads by utilizing local cache of various states
3739
let dnslinkResolver
3840
let ipfsPathValidator
@@ -55,8 +57,11 @@ export default async function init () {
5557
runtime = await createRuntimeChecks(browser)
5658
state = initState(options)
5759
notify = createNotifier(getState)
60+
// ensure consent is set properly on app init
61+
handleConsentFromState(state)
5862

5963
if (state.active) {
64+
startSession()
6065
// It's ok for this to fail, node might be unavailable or mis-configured
6166
try {
6267
ipfs = await initIpfsClient(browser, state)
@@ -167,6 +172,15 @@ export default async function init () {
167172
const result = validIpfsOrIpns(path) ? resolveToPublicUrl(path) : null
168173
return Promise.resolve({ pubGwUrlForIpfsOrIpnsPath: result })
169174
}
175+
if (request.telemetry) {
176+
return Promise.resolve(onTelemetryMessage(request.telemetry, sender))
177+
}
178+
}
179+
180+
function onTelemetryMessage (request, sender) {
181+
if (request.trackView) {
182+
return trackView(request.trackView)
183+
}
170184
}
171185

172186
// PORTS (connection-based messaging)
@@ -366,11 +380,11 @@ export default async function init () {
366380
// https://github.com/ipfs-shipyard/ipfs-companion/issues/398
367381
if (runtime.isFirefox && ipfsPathValidator.isIpfsPageActionsContext(url)) {
368382
if (sameGateway(url, state.gwURL) || sameGateway(url, state.apiURL)) {
369-
await browser.pageAction.setIcon({ tabId: tabId, path: '/icons/ipfs-logo-on.svg' })
370-
await browser.pageAction.setTitle({ tabId: tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtCustomGateway') })
383+
await browser.pageAction.setIcon({ tabId, path: '/icons/ipfs-logo-on.svg' })
384+
await browser.pageAction.setTitle({ tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtCustomGateway') })
371385
} else {
372-
await browser.pageAction.setIcon({ tabId: tabId, path: '/icons/ipfs-logo-off.svg' })
373-
await browser.pageAction.setTitle({ tabId: tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtPublicGateway') })
386+
await browser.pageAction.setIcon({ tabId, path: '/icons/ipfs-logo-off.svg' })
387+
await browser.pageAction.setTitle({ tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtPublicGateway') })
374388
}
375389
await browser.pageAction.show(tabId)
376390
}
@@ -554,6 +568,8 @@ export default async function init () {
554568
await registerSubdomainProxy(getState, runtime)
555569
shouldRestartIpfsClient = true
556570
shouldStopIpfsClient = !state.active
571+
// Any time the extension switches active state, start or stop the current session.
572+
state.active ? startSession() : endSession()
557573
break
558574
case 'ipfsNodeType':
559575
if (change.oldValue !== braveNodeType && change.newValue === braveNodeType) {
@@ -620,6 +636,8 @@ export default async function init () {
620636
break
621637
}
622638
}
639+
// ensure consent is set properly on state changes
640+
handleConsentFromState(state)
623641

624642
if ((state.active && shouldRestartIpfsClient) || shouldStopIpfsClient) {
625643
try {

add-on/src/lib/notifier.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export default function createNotifier (getState) {
2121
return await browser.notifications.create({
2222
type: 'basic',
2323
iconUrl: browser.runtime.getURL('icons/ipfs-logo-on.svg'),
24-
title: title,
25-
message: message
24+
title,
25+
message
2626
})
2727
} catch (err) {
2828
log.error('failed to create a notification', err)

add-on/src/lib/options.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
'use strict'
22

3-
import { isIPv4, isIPv6 } from 'is-ip'
43
import isFQDN from 'is-fqdn'
4+
import { isIPv4, isIPv6 } from 'is-ip'
55

6+
/**
7+
* @type {Readonly<import('../types.js').CompanionOptions>}
8+
*/
69
export const optionDefaults = Object.freeze({
710
active: true, // global ON/OFF switch, overrides everything else
811
ipfsNodeType: 'external',
@@ -31,7 +34,12 @@ export const optionDefaults = Object.freeze({
3134
importDir: '/ipfs-companion-imports/%Y-%M-%D_%h%m%s/',
3235
useLatestWebUI: false,
3336
dismissedUpdate: null,
34-
openViaWebUI: true
37+
openViaWebUI: true,
38+
telemetryGroupMinimal: true,
39+
telemetryGroupPerformance: false,
40+
telemetryGroupUx: false,
41+
telemetryGroupFeedback: false,
42+
telemetryGroupLocation: false
3543
})
3644

3745
function buildDefaultIpfsNodeConfig () {

0 commit comments

Comments
 (0)