From 8437a235c9bdaa3f2f82921f5f0ea294e6c1520b Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 14 Dec 2023 11:55:51 -0700 Subject: [PATCH] feat(telemetry): Adding Scarf based telemetry to Superset (#26011) Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- docker-compose.yml | 2 + docs/docs/frequently-asked-questions.mdx | 6 +- ...stalling-superset-using-docker-compose.mdx | 4 +- superset-frontend/package-lock.json | 42 ++++++++++--- superset-frontend/package.json | 2 +- superset-frontend/spec/helpers/shim.tsx | 16 ++--- .../TelemetryPixel/TelemetryPixel.test.tsx | 49 +++++++++++++++ .../src/components/TelemetryPixel/index.tsx | 59 +++++++++++++++++++ .../src/features/home/RightMenu.tsx | 6 ++ superset-frontend/webpack.config.js | 1 + superset/config.py | 16 ++++- 12 files changed, 181 insertions(+), 24 deletions(-) create mode 100644 superset-frontend/src/components/TelemetryPixel/TelemetryPixel.test.tsx create mode 100644 superset-frontend/src/components/TelemetryPixel/index.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25b0e02edb..de40865edc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -622,7 +622,7 @@ cd superset-frontend npm ci ``` -Note that Superset uses [Scarf](https://docs.scarf.sh) to capture telemetry/analytics about versions being installed, including the `scarf-js` npm package. As noted elsewhere in this documentation, Scarf gathers aggregated stats for the sake of security/release strategy, and does not capture/retain PII. [You can read here](https://docs.scarf.sh/package-analytics/) about the package, and various means to opt out of it, but one easy way to opt out is to add this setting in `superset-frontent/package.json`: +Note that Superset uses [Scarf](https://docs.scarf.sh) to capture telemetry/analytics about versions being installed, including the `scarf-js` npm package and an analytics pixel. As noted elsewhere in this documentation, Scarf gathers aggregated stats for the sake of security/release strategy, and does not capture/retain PII. [You can read here](https://docs.scarf.sh/package-analytics/) about the `scarf-js` package, and various means to opt out of it, but you can opt out of the npm package _and_ the pixel by setting the `SCARF_ANALYTICS` envinronment variable to `false` or opt out of the pixel by adding this setting in `superset-frontent/package.json`: ```json // your-package/package.json diff --git a/docker-compose.yml b/docker-compose.yml index b2c9196b00..a2dfc26dba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -124,6 +124,8 @@ services: command: ["/app/docker/docker-frontend.sh"] env_file: docker/.env depends_on: *superset-depends-on + environment: + SCARF_ANALYTICS: "${SCARF_ANALYTICS}" volumes: *superset-volumes superset-worker: diff --git a/docs/docs/frequently-asked-questions.mdx b/docs/docs/frequently-asked-questions.mdx index 11682136d7..3007584ab1 100644 --- a/docs/docs/frequently-asked-questions.mdx +++ b/docs/docs/frequently-asked-questions.mdx @@ -268,8 +268,10 @@ This can be used, for example, to convert UTC time to local time. ### Does Superset collect any telemetry data? Superset uses [Scarf](https://about.scarf.sh/) by default to collect basic telemetry data upon installing and/or running Superset. This data helps the maintainers of Superset better understand which versions of Superset are being used, in order to prioritize patch/minor releases and security fixes. -We use the [Scarf Gateway](https://docs.scarf.sh/gateway/) to sit in front of container registries, and the [scarf-js](https://about.scarf.sh/package-sdks) package to track `npm` installations. -Scarf purges PII and provides aggregated statistics. Superset users can easily opt out of analytics in various ways documented [here](https://docs.scarf.sh/gateway/#do-not-track) and [here](https://docs.scarf.sh/package-analytics/#as-a-user-of-a-package-using-scarf-js-how-can-i-opt-out-of-analytics). Additional opt-out instructions for Docker users are available on the [Docker Installation](https://superset.apache.org/docs/installation/installing-superset-using-docker-compose) page. +We use the [Scarf Gateway](https://docs.scarf.sh/gateway/) to sit in front of container registries, the [scarf-js](https://about.scarf.sh/package-sdks) package to track `npm` installations, and a Scarf pixel to gather anonymous analytics on Superset page views. +Scarf purges PII and provides aggregated statistics. Superset users can easily opt out of analytics in various ways documented [here](https://docs.scarf.sh/gateway/#do-not-track) and [here](https://docs.scarf.sh/package-analytics/#as-a-user-of-a-package-using-scarf-js-how-can-i-opt-out-of-analytics). +Superset maintainers can also opt out of telemetry data collection by setting the `SCARF_ANALYTICS` environment variable to `false` in the Superset container (or anywhere Superset/webpack are run). +Additional opt-out instructions for Docker users are available on the [Docker Installation](https://superset.apache.org/docs/installation/installing-superset-using-docker-compose) page. ### Does Superset have an archive panel or trash bin from which a user can recover deleted assets? diff --git a/docs/docs/installation/installing-superset-using-docker-compose.mdx b/docs/docs/installation/installing-superset-using-docker-compose.mdx index 4611466ca3..7b6d975739 100644 --- a/docs/docs/installation/installing-superset-using-docker-compose.mdx +++ b/docs/docs/installation/installing-superset-using-docker-compose.mdx @@ -124,7 +124,9 @@ Users often want to connect to other databases from Superset. Currently, the eas :::note Superset uses [Scarf Gateway](https://about.scarf.sh/scarf-gateway) to collect telemetry data. Knowing the installation counts for different Superset versions informs the project's decisions about patching and long-term support. Scarf purges personally identifiable information (PII) and provides only aggregated statistics. -To opt-out of this data collection in your docker compose based installation, edit the `x-superset-image:` line in your `docker-compose.yml` and `docker-compose-non-dev.yml` files, replacing `apachesuperset.docker.scarf.sh/apache/superset` with `apache/superset` to pull the image directly from Docker Hub. +To opt-out of this data collection for packages downloaded through the Scarf Gateway by your docker compose based installation, edit the `x-superset-image:` line in your `docker-compose.yml` and `docker-compose-non-dev.yml` files, replacing `apachesuperset.docker.scarf.sh/apache/superset` with `apache/superset` to pull the image directly from Docker Hub. + +To disable the Scarf telemetry pixel, set the `SCARF_ANALYTICS` environment variable to `False` in your terminal and/or in your `docker/.env` and `docker/.env-non-dev` files. ::: ### 4. Log in to Superset diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index a747d9a78e..4794218b36 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -194,7 +194,7 @@ "@types/js-levenshtein": "^1.1.0", "@types/json-bigint": "^1.0.1", "@types/mousetrap": "^1.6.11", - "@types/react": "^16.9.43", + "@types/react": "^16.9.53", "@types/react-dom": "^16.9.8", "@types/react-gravatar": "^2.6.8", "@types/react-json-tree": "^0.6.11", @@ -19628,12 +19628,13 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "16.9.43", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.43.tgz", - "integrity": "sha512-PxshAFcnJqIWYpJbLPriClH53Z2WlJcVZE+NP2etUtWQs2s7yIMj3/LDKZT/5CHJ/F62iyjVCDu2H3jHEXIxSg==", + "version": "16.14.51", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.51.tgz", + "integrity": "sha512-4T/wsDXStA5OUGTj6w2INze3ZCz22IwQiWcApgqqNRU2A6vNUIPXpNkjAMUFxx6diYPVkvz+d7gEtU7AZ+0Xqg==", "dependencies": { "@types/prop-types": "*", - "csstype": "^2.2.0" + "@types/scheduler": "*", + "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { @@ -19775,6 +19776,11 @@ "@types/react": "*" } }, + "node_modules/@types/react/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, "node_modules/@types/redux-localstorage": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/redux-localstorage/-/redux-localstorage-1.0.8.tgz", @@ -19816,6 +19822,11 @@ "resolved": "https://registry.npmjs.org/@types/rison/-/rison-0.0.6.tgz", "integrity": "sha512-mE3eRK0fpTN/GnNBOIg2tGq2cFhchQXF6fCbrLxus75TgnoOECbdHikr948FGO/UAml7/ZhLMa5FbGkF5PKvmw==" }, + "node_modules/@types/scheduler": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", + "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==" + }, "node_modules/@types/seedrandom": { "version": "2.4.30", "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.30.tgz", @@ -79599,12 +79610,20 @@ "devOptional": true }, "@types/react": { - "version": "16.9.43", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.43.tgz", - "integrity": "sha512-PxshAFcnJqIWYpJbLPriClH53Z2WlJcVZE+NP2etUtWQs2s7yIMj3/LDKZT/5CHJ/F62iyjVCDu2H3jHEXIxSg==", + "version": "16.14.51", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.51.tgz", + "integrity": "sha512-4T/wsDXStA5OUGTj6w2INze3ZCz22IwQiWcApgqqNRU2A6vNUIPXpNkjAMUFxx6diYPVkvz+d7gEtU7AZ+0Xqg==", "requires": { "@types/prop-types": "*", - "csstype": "^2.2.0" + "@types/scheduler": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + } } }, "@types/react-dom": { @@ -79789,6 +79808,11 @@ "resolved": "https://registry.npmjs.org/@types/rison/-/rison-0.0.6.tgz", "integrity": "sha512-mE3eRK0fpTN/GnNBOIg2tGq2cFhchQXF6fCbrLxus75TgnoOECbdHikr948FGO/UAml7/ZhLMa5FbGkF5PKvmw==" }, + "@types/scheduler": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", + "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==" + }, "@types/seedrandom": { "version": "2.4.30", "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.30.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 083b83c69e..d8ed6f62e8 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -259,7 +259,7 @@ "@types/js-levenshtein": "^1.1.0", "@types/json-bigint": "^1.0.1", "@types/mousetrap": "^1.6.11", - "@types/react": "^16.9.43", + "@types/react": "^16.9.53", "@types/react-dom": "^16.9.8", "@types/react-gravatar": "^2.6.8", "@types/react-json-tree": "^0.6.11", diff --git a/superset-frontend/spec/helpers/shim.tsx b/superset-frontend/spec/helpers/shim.tsx index 152fcc370c..8270c56317 100644 --- a/superset-frontend/spec/helpers/shim.tsx +++ b/superset-frontend/spec/helpers/shim.tsx @@ -48,14 +48,14 @@ if (defaultView != null) { } const g = global as any; -g.window = g.window || {}; -g.window.location = { href: 'about:blank' }; -g.window.performance = { now: () => new Date().getTime() }; -g.window.Worker = Worker; -g.window.IntersectionObserver = IntersectionObserver; -g.window.ResizeObserver = ResizeObserver; -g.window.featureFlags = {}; -g.URL.createObjectURL = () => ''; +g.window ??= Object.create(window); +g.window.location ??= { href: 'about:blank' }; +g.window.performance ??= { now: () => new Date().getTime() }; +g.window.Worker ??= Worker; +g.window.IntersectionObserver ??= IntersectionObserver; +g.window.ResizeObserver ??= ResizeObserver; +g.window.featureFlags ??= {}; +g.URL.createObjectURL ??= () => ''; g.caches = new CacheStorage(); Object.defineProperty(window, 'matchMedia', { diff --git a/superset-frontend/src/components/TelemetryPixel/TelemetryPixel.test.tsx b/superset-frontend/src/components/TelemetryPixel/TelemetryPixel.test.tsx new file mode 100644 index 0000000000..4e3b3c9332 --- /dev/null +++ b/superset-frontend/src/components/TelemetryPixel/TelemetryPixel.test.tsx @@ -0,0 +1,49 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { render } from 'spec/helpers/testing-library'; +import TelemetryPixel from '.'; + +const OLD_ENV = process.env; + +// restor the process after messing with it! +afterAll(() => { + process.env = OLD_ENV; +}); + +test('should render', () => { + const { container } = render(); + expect(container).toBeInTheDocument(); +}); + +test('should render the pixel link when FF is on', () => { + process.env.SCARF_ANALYTICS = 'true'; + render(); + + const image = document.querySelector('img[src*="scarf.sh"]'); + expect(image).toBeInTheDocument(); +}); + +test('should NOT render the pixel link when FF is off', () => { + process.env.SCARF_ANALYTICS = 'false'; + render(); + + const image = document.querySelector('img[src*="scarf.sh"]'); + expect(image).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/components/TelemetryPixel/index.tsx b/superset-frontend/src/components/TelemetryPixel/index.tsx new file mode 100644 index 0000000000..6bfe1d20b9 --- /dev/null +++ b/superset-frontend/src/components/TelemetryPixel/index.tsx @@ -0,0 +1,59 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; + +interface TelemetryPixelProps { + version?: string; + sha?: string; + build?: string; +} + +/** + * Renders a telemetry pixel component to capture anonymous, aggregated telemetry via Scarf. + * This can be disabled by setting the SCARF_ANALYTICS environment variable to false. + * + * @component + * @param {TelemetryPixelProps} props - The props for the TelemetryPixel component. + * @param {string} props.version - The version of Superset that's currently in use. + * @param {string} props.sha - The SHA of Superset that's currently in use. + * @param {string} props.build - The build of Superset that's currently in use. + * @returns {JSX.Element | null} The rendered TelemetryPixel component. + */ + +const PIXEL_ID = '0d3461e1-abb1-4691-a0aa-5ed50de66af0'; + +const TelemetryPixel = ({ + version = 'unknownVersion', + sha = 'unknownSHA', + build = 'unknownBuild', +}: TelemetryPixelProps): React.ReactElement | null => { + const pixelPath = `https://apachesuperset.gateway.scarf.sh/pixel/${PIXEL_ID}/${version}/${sha}/${build}`; + return process.env.SCARF_ANALYTICS === 'false' || + process.env.SCARF_ANALYTICS === 'false' ? null : ( + + ); +}; +export default TelemetryPixel; diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index b79ebb65f8..a37214b66b 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -46,6 +46,7 @@ import { import { RootState } from 'src/dashboard/types'; import DatabaseModal from 'src/features/databases/DatabaseModal'; import { uploadUserPerms } from 'src/views/CRUD/utils'; +import TelemetryPixel from 'src/components/TelemetryPixel'; import LanguagePicker from './LanguagePicker'; import { ExtensionConfigs, @@ -562,6 +563,11 @@ const RightMenu = ({ {t('Login')} )} + ); }; diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index dea99be2cf..6b1753009a 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -117,6 +117,7 @@ const plugins = [ 'process.env.WEBPACK_MODE': JSON.stringify(mode), 'process.env.REDUX_DEFAULT_MIDDLEWARE': process.env.REDUX_DEFAULT_MIDDLEWARE, + 'process.env.SCARF_ANALYTICS': process.env.SCARF_ANALYTICS, }), new CopyPlugin({ diff --git a/superset/config.py b/superset/config.py index ca801442d9..348baef545 100644 --- a/superset/config.py +++ b/superset/config.py @@ -1427,7 +1427,13 @@ TALISMAN_CONFIG = { "content_security_policy": { "base-uri": ["'self'"], "default-src": ["'self'"], - "img-src": ["'self'", "blob:", "data:"], + "img-src": [ + "'self'", + "blob:", + "data:", + "https://apachesuperset.gateway.scarf.sh", + "https://static.scarf.sh/", + ], "worker-src": ["'self'", "blob:"], "connect-src": [ "'self'", @@ -1450,7 +1456,13 @@ TALISMAN_DEV_CONFIG = { "content_security_policy": { "base-uri": ["'self'"], "default-src": ["'self'"], - "img-src": ["'self'", "blob:", "data:"], + "img-src": [ + "'self'", + "blob:", + "data:", + "https://apachesuperset.gateway.scarf.sh", + "https://static.scarf.sh/", + ], "worker-src": ["'self'", "blob:"], "connect-src": [ "'self'",