1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-23 07:19:41 +02:00

feat(telemetry): replace GA with matomo (#4140)

* feat(core/telemetry): add posthog

* feat(core/telemetry): add posthog

* feat(core/telemetry): add matomo

* feat(core/telemetry): update matomo

* feat(core/telemetry): update matomo

* feat(core/telemetry): update matomo

* feat(telemetry): remove google analytics code

* refactor(telemetry): move matomo code to bundle

* refactor(telemetry): move matomo lib to assets

* refactor(telemetry): depreciate --no-analytics

* feat(settings): introduce a setting to enable telemetry

* fix(cli): fix typo

* feat(settings): allow toggle telemetry from settings

* fix(settings): handle case where AuthenticationMethod is missing

* feat(admin): set telemetry on admin init

* refactor(app); revert file

* refactor(state-manager): move optout to state manager

* feat(telemetry): set matomo url

* feat(core/settings): minor UI update

* feat(core/telemetry): update custom URL

* feat(core/telemetry): add placeholder for privacy policy

* feat(core/telemetry): add privacy policy link

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
Chaim Lev-Ari 2020-08-07 01:46:25 +03:00 committed by GitHub
parent 7aaf9d0eb7
commit 2158cc5157
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 2879 additions and 2605 deletions

View file

@ -6,7 +6,6 @@ env:
globals: globals:
angular: true angular: true
__CONFIG_GA_ID: true
extends: extends:
- 'eslint:recommended' - 'eslint:recommended'

View file

@ -15,6 +15,7 @@ func (m *Migrator) updateSettingsToDB25() error {
} }
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
legacySettings.EnableTelemetry = true
legacySettings.AllowContainerCapabilitiesForRegularUsers = true legacySettings.AllowContainerCapabilitiesForRegularUsers = true

View file

@ -2,6 +2,7 @@ package cli
import ( import (
"errors" "errors"
"log"
"time" "time"
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
@ -35,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(), EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(), NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Default(defaultNoAnalytics).Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(), TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(), TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
@ -88,7 +89,9 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
} }
func displayDeprecationWarnings(flags *portainer.CLIFlags) { func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if flags.NoAnalytics != nil {
log.Println("Warning: The --no-analytics has been deprecated and will be removed in a future version of Portainer. It currently has no effect, telemetry settings are available in the Portainer settings.")
}
} }
func validateEndpointURL(endpointURL string) error { func validateEndpointURL(endpointURL string) error {

View file

@ -154,7 +154,6 @@ func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelServic
func initStatus(flags *portainer.CLIFlags) *portainer.Status { func initStatus(flags *portainer.CLIFlags) *portainer.Status {
return &portainer.Status{ return &portainer.Status{
Analytics: !*flags.NoAnalytics,
Version: portainer.APIVersion, Version: portainer.APIVersion,
} }
} }
@ -168,6 +167,7 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
settings.LogoURL = *flags.Logo settings.LogoURL = *flags.Logo
settings.SnapshotInterval = *flags.SnapshotInterval settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
if *flags.Templates != "" { if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates settings.TemplatesURL = *flags.Templates

View file

@ -22,6 +22,7 @@ type publicSettingsResponse struct {
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
OAuthLoginURI string `json:"OAuthLoginURI"` OAuthLoginURI string `json:"OAuthLoginURI"`
EnableTelemetry bool `json:"EnableTelemetry"`
} }
// GET request on /api/settings/public // GET request on /api/settings/public
@ -43,6 +44,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures, EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
EnableTelemetry: settings.EnableTelemetry,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.AuthorizationURI,
settings.OAuthSettings.ClientID, settings.OAuthSettings.ClientID,

View file

@ -33,10 +33,11 @@ type settingsUpdatePayload struct {
EdgeAgentCheckinInterval *int EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool EnableEdgeComputeFeatures *bool
UserSessionTimeout *string UserSessionTimeout *string
EnableTelemetry *bool
} }
func (payload *settingsUpdatePayload) Validate(r *http.Request) error { func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 { if payload.AuthenticationMethod != nil && *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 {
return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)") return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)")
} }
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) { if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
@ -164,6 +165,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
} }
if payload.EnableTelemetry != nil {
settings.EnableTelemetry = *payload.EnableTelemetry
}
tlsError := handler.updateTLS(settings) tlsError := handler.updateTLS(settings)
if tlsError != nil { if tlsError != nil {
return tlsError return tlsError

View file

@ -532,6 +532,7 @@ type (
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
UserSessionTimeout string `json:"UserSessionTimeout"` UserSessionTimeout string `json:"UserSessionTimeout"`
EnableTelemetry bool `json:"EnableTelemetry"`
// Deprecated fields // Deprecated fields
DisplayDonationHeader bool DisplayDonationHeader bool
@ -566,7 +567,6 @@ type (
// Status represents the application status // Status represents the application status
Status struct { Status struct {
Analytics bool `json:"Analytics"`
Version string `json:"Version"` Version string `json:"Version"`
} }

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,9 @@ import '@babel/polyfill';
import angular from 'angular'; import angular from 'angular';
import './matomo-setup';
import './assets/js/angulartics-matomo';
import './agent'; import './agent';
import './azure/_module'; import './azure/_module';
import './docker/__module'; import './docker/__module';
@ -21,7 +24,6 @@ angular.module('portainer', [
'angularUtils.directives.dirPagination', 'angularUtils.directives.dirPagination',
'LocalStorageModule', 'LocalStorageModule',
'angular-jwt', 'angular-jwt',
'angular-google-analytics',
'angular-json-tree', 'angular-json-tree',
'angular-loading-bar', 'angular-loading-bar',
'angular-clipboard', 'angular-clipboard',
@ -37,6 +39,8 @@ angular.module('portainer', [
'portainer.integrations', 'portainer.integrations',
'rzModule', 'rzModule',
'moment-picker', 'moment-picker',
'angulartics',
'angulartics.matomo',
]); ]);
if (require) { if (require) {

223
app/assets/js/angulartics-matomo.js vendored Normal file
View file

@ -0,0 +1,223 @@
import angular from 'angular';
// forked from https://github.com/angulartics/angulartics-piwik/blob/master/src/angulartics-piwik.js
/* global _paq */
/**
* @ngdoc overview
* @name angulartics.piwik
* Enables analytics support for Piwik/Matomo (http://piwik.org/docs/tracking-api/)
*/
angular.module('angulartics.matomo', ['angulartics']).config([
'$analyticsProvider',
'$windowProvider',
function ($analyticsProvider, $windowProvider) {
var $window = $windowProvider.$get();
$analyticsProvider.settings.pageTracking.trackRelativePath = true;
// Add piwik specific trackers to angulartics API
// Requires the CustomDimensions plugin for Piwik.
$analyticsProvider.api.setCustomDimension = function (dimensionId, value) {
if ($window._paq) {
$window._paq.push(['setCustomDimension', dimensionId, value]);
}
};
// Requires the CustomDimensions plugin for Piwik.
$analyticsProvider.api.deleteCustomDimension = function (dimensionId) {
if ($window._paq) {
$window._paq.push(['deleteCustomDimension', dimensionId]);
}
};
// scope: visit or page. Defaults to 'page'
$analyticsProvider.api.setCustomVariable = function (varIndex, varName, value, scope) {
if ($window._paq) {
scope = scope || 'page';
$window._paq.push(['setCustomVariable', varIndex, varName, value, scope]);
}
};
// scope: visit or page. Defaults to 'page'
$analyticsProvider.api.deleteCustomVariable = function (varIndex, scope) {
if ($window._paq) {
scope = scope || 'page';
$window._paq.push(['deleteCustomVariable', varIndex, scope]);
}
};
// trackSiteSearch(keyword, category, [searchCount])
$analyticsProvider.api.trackSiteSearch = function (keyword, category, searchCount) {
// keyword is required
if ($window._paq && keyword) {
var params = ['trackSiteSearch', keyword, category || false];
// searchCount is optional
if (angular.isDefined(searchCount)) {
params.push(searchCount);
}
$window._paq.push(params);
}
};
// logs a conversion for goal 1. revenue is optional
// trackGoal(goalID, [revenue]);
$analyticsProvider.api.trackGoal = function (goalID, revenue) {
if ($window._paq) {
_paq.push(['trackGoal', goalID, revenue || 0]);
}
};
// track outlink or download
// linkType is 'link' or 'download', 'link' by default
// trackLink(url, [linkType]);
$analyticsProvider.api.trackLink = function (url, linkType) {
var type = linkType || 'link';
if ($window._paq) {
$window._paq.push(['trackLink', url, type]);
}
};
// Set default angulartics page and event tracking
$analyticsProvider.registerSetUsername(function (username) {
if ($window._paq) {
$window._paq.push(['setUserId', username]);
}
});
// locationObj is the angular $location object
$analyticsProvider.registerPageTrack(function (path) {
if ($window._paq) {
$window._paq.push(['setDocumentTitle', $window.document.title]);
$window._paq.push(['setReferrerUrl', '']);
$window._paq.push(['setCustomUrl', 'http://portainer-ce.app' + path]);
$window._paq.push(['trackPageView']);
}
});
/**
* @name eventTrack
* Track a basic event in Piwik, or send an ecommerce event.
*
* @param {string} action A string corresponding to the type of event that needs to be tracked.
* @param {object} properties The properties that need to be logged with the event.
*/
$analyticsProvider.registerEventTrack(function (action, properties) {
if ($window._paq) {
properties = properties || {};
switch (action) {
/**
* @description Sets the current page view as a product or category page view. When you call
* setEcommerceView it must be followed by a call to trackPageView to record the product or
* category page view.
*
* @link https://piwik.org/docs/ecommerce-analytics/#tracking-product-page-views-category-page-views-optional
* @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce
*
* @property productSKU (required) SKU: Product unique identifier
* @property productName (optional) Product name
* @property categoryName (optional) Product category, or array of up to 5 categories
* @property price (optional) Product Price as displayed on the page
*/
case 'setEcommerceView':
$window._paq.push(['setEcommerceView', properties.productSKU, properties.productName, properties.categoryName, properties.price]);
break;
/**
* @description Adds a product into the ecommerce order. Must be called for each product in
* the order.
*
* @link https://piwik.org/docs/ecommerce-analytics/#tracking-ecommerce-orders-items-purchased-required
* @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce
*
* @property productSKU (required) SKU: Product unique identifier
* @property productName (optional) Product name
* @property categoryName (optional) Product category, or array of up to 5 categories
* @property price (recommended) Product price
* @property quantity (optional, default to 1) Product quantity
*/
case 'addEcommerceItem':
$window._paq.push(['addEcommerceItem', properties.productSKU, properties.productName, properties.productCategory, properties.price, properties.quantity]);
break;
/**
* @description Tracks a shopping cart. Call this javascript function every time a user is
* adding, updating or deleting a product from the cart.
*
* @link https://piwik.org/docs/ecommerce-analytics/#tracking-add-to-cart-items-added-to-the-cart-optional
* @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce
*
* @property grandTotal (required) Cart amount
*/
case 'trackEcommerceCartUpdate':
$window._paq.push(['trackEcommerceCartUpdate', properties.grandTotal]);
break;
/**
* @description Tracks an Ecommerce order, including any ecommerce item previously added to
* the order. orderId and grandTotal (ie. revenue) are required parameters.
*
* @link https://piwik.org/docs/ecommerce-analytics/#tracking-ecommerce-orders-items-purchased-required
* @link https://developer.piwik.org/api-reference/tracking-javascript#ecommerce
*
* @property orderId (required) Unique Order ID
* @property grandTotal (required) Order Revenue grand total (includes tax, shipping, and subtracted discount)
* @property subTotal (optional) Order sub total (excludes shipping)
* @property tax (optional) Tax amount
* @property shipping (optional) Shipping amount
* @property discount (optional) Discount offered (set to false for unspecified parameter)
*/
case 'trackEcommerceOrder':
$window._paq.push(['trackEcommerceOrder', properties.orderId, properties.grandTotal, properties.subTotal, properties.tax, properties.shipping, properties.discount]);
break;
/**
* @description Logs an event with an event category (Videos, Music, Games...), an event
* action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...), and an optional
* event name and optional numeric value.
*
* @link https://piwik.org/docs/event-tracking/
* @link https://developer.piwik.org/api-reference/tracking-javascript#using-the-tracker-object
*
* @property category
* @property action
* @property name (optional, recommended)
* @property value (optional)
*/
default:
// PAQ requires that eventValue be an integer, see: http://piwik.org/docs/event-tracking
if (properties.value) {
var parsed = parseInt(properties.value, 10);
properties.value = isNaN(parsed) ? 0 : parsed;
}
$window._paq.push([
'trackEvent',
properties.category,
action,
properties.name || properties.label, // Changed in favour of Piwik documentation. Added fallback so it's backwards compatible.
properties.value,
]);
}
}
});
/**
* @name exceptionTrack
* Sugar on top of the eventTrack method for easily handling errors
*
* @param {object} error An Error object to track: error.toString() used for event 'action', error.stack used for event 'label'.
* @param {object} cause The cause of the error given from $exceptionHandler, not used.
*/
$analyticsProvider.registerExceptionTrack(function (error) {
if ($window._paq) {
$window._paq.push(['trackEvent', 'Exceptions', error.toString(), error.stack, 0]);
}
});
},
]);

View file

@ -7,11 +7,10 @@ angular.module('portainer').config([
'$httpProvider', '$httpProvider',
'localStorageServiceProvider', 'localStorageServiceProvider',
'jwtOptionsProvider', 'jwtOptionsProvider',
'AnalyticsProvider',
'$uibTooltipProvider', '$uibTooltipProvider',
'$compileProvider', '$compileProvider',
'cfpLoadingBarProvider', 'cfpLoadingBarProvider',
function ($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) { function ($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
'use strict'; 'use strict';
var environment = '@@ENVIRONMENT'; var environment = '@@ENVIRONMENT';
@ -52,9 +51,6 @@ angular.module('portainer').config([
}, },
]); ]);
AnalyticsProvider.setAccount({ tracker: __CONFIG_GA_ID, set: { anonymizeIp: true } });
AnalyticsProvider.startOffline(true);
toastr.options.timeOut = 3000; toastr.options.timeOut = 3000;
Terminal.applyAddon(fit); Terminal.applyAddon(fit);

14
app/matomo-setup.js Normal file
View file

@ -0,0 +1,14 @@
const _paq = (window._paq = window._paq || []);
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['enableLinkTracking']);
var u = 'https://portainer-ce.matomo.cloud/';
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '1']);
var d = document,
g = d.createElement('script'),
s = d.getElementsByTagName('script')[0];
g.type = 'text/javascript';
g.async = true;
g.src = '//cdn.matomo.cloud/portainer-ce.matomo.cloud/matomo.js';
s.parentNode.insertBefore(g, s);

View file

@ -15,16 +15,6 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat
await Authentication.init(); await Authentication.init();
} }
function initAnalytics(Analytics, $rootScope) {
Analytics.offline(false);
Analytics.registerScriptTags();
Analytics.registerTrackers();
$rootScope.$on('$stateChangeSuccess', function (event, toState) {
Analytics.trackPage(toState.url);
Analytics.pageView();
});
}
angular.module('portainer.app', ['portainer.oauth']).config([ angular.module('portainer.app', ['portainer.oauth']).config([
'$stateRegistryProvider', '$stateRegistryProvider',
function ($stateRegistryProvider) { function ($stateRegistryProvider) {
@ -38,23 +28,19 @@ angular.module('portainer.app', ['portainer.oauth']).config([
'StateManager', 'StateManager',
'Authentication', 'Authentication',
'Notifications', 'Notifications',
'Analytics',
'authManager', 'authManager',
'$rootScope', '$rootScope',
'$state', '$state',
'$async', '$async',
'$q', '$q',
(StateManager, Authentication, Notifications, Analytics, authManager, $rootScope, $state, $async, $q) => { (StateManager, Authentication, Notifications, authManager, $rootScope, $state, $async, $q) => {
const deferred = $q.defer(); const deferred = $q.defer();
const appState = StateManager.getState(); const appState = StateManager.getState();
if (!appState.loading) { if (!appState.loading) {
deferred.resolve(); deferred.resolve();
} else { } else {
StateManager.initialize() StateManager.initialize()
.then(function success(state) { .then(function success() {
if (state.application.analytics) {
initAnalytics(Analytics, $rootScope);
}
return $async(initAuthentication, authManager, Authentication, $rootScope, $state); return $async(initAuthentication, authManager, Authentication, $rootScope, $state);
}) })
.then(() => deferred.resolve()) .then(() => deferred.resolve())

View file

@ -17,6 +17,7 @@ export function SettingsViewModel(data) {
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
this.UserSessionTimeout = data.UserSessionTimeout; this.UserSessionTimeout = data.UserSessionTimeout;
this.EnableTelemetry = data.EnableTelemetry;
} }
export function PublicSettingsViewModel(settings) { export function PublicSettingsViewModel(settings) {
@ -32,6 +33,7 @@ export function PublicSettingsViewModel(settings) {
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
this.LogoURL = settings.LogoURL; this.LogoURL = settings.LogoURL;
this.OAuthLoginURI = settings.OAuthLoginURI; this.OAuthLoginURI = settings.OAuthLoginURI;
this.EnableTelemetry = settings.EnableTelemetry;
} }
export function LDAPSettingsViewModel(data) { export function LDAPSettingsViewModel(data) {

View file

@ -1,7 +1,6 @@
export function StatusViewModel(data) { export function StatusViewModel(data) {
this.Authentication = data.Authentication; this.Authentication = data.Authentication;
this.Snapshot = data.Snapshot; this.Snapshot = data.Snapshot;
this.Analytics = data.Analytics;
this.Version = data.Version; this.Version = data.Version;
} }

View file

@ -11,7 +11,19 @@ angular.module('portainer.app').factory('StateManager', [
'StatusService', 'StatusService',
'APPLICATION_CACHE_VALIDITY', 'APPLICATION_CACHE_VALIDITY',
'AgentPingService', 'AgentPingService',
function StateManagerFactory($q, SystemService, InfoHelper, EndpointProvider, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY, AgentPingService) { '$analytics',
function StateManagerFactory(
$q,
SystemService,
InfoHelper,
EndpointProvider,
LocalStorage,
SettingsService,
StatusService,
APPLICATION_CACHE_VALIDITY,
AgentPingService,
$analytics
) {
'use strict'; 'use strict';
var manager = {}; var manager = {};
@ -106,9 +118,15 @@ angular.module('portainer.app').factory('StateManager', [
LocalStorage.storeApplicationState(state.application); LocalStorage.storeApplicationState(state.application);
}; };
manager.updateEnableTelemetry = function updateEnableTelemetry(enableTelemetry) {
state.application.enableTelemetry = enableTelemetry;
$analytics.setOptOut(!enableTelemetry);
LocalStorage.storeApplicationState(state.application);
};
function assignStateFromStatusAndSettings(status, settings) { function assignStateFromStatusAndSettings(status, settings) {
state.application.analytics = status.Analytics;
state.application.version = status.Version; state.application.version = status.Version;
state.application.enableTelemetry = settings.EnableTelemetry;
state.application.logo = settings.LogoURL; state.application.logo = settings.LogoURL;
state.application.snapshotInterval = settings.SnapshotInterval; state.application.snapshotInterval = settings.SnapshotInterval;
state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures; state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
@ -134,6 +152,7 @@ angular.module('portainer.app').factory('StateManager', [
var status = data.status; var status = data.status;
var settings = data.settings; var settings = data.settings;
assignStateFromStatusAndSettings(status, settings); assignStateFromStatusAndSettings(status, settings);
$analytics.setOptOut(!settings.EnableTelemetry);
LocalStorage.storeApplicationState(state.application); LocalStorage.storeApplicationState(state.application);
deferred.resolve(state); deferred.resolve(state);
}) })
@ -176,6 +195,7 @@ angular.module('portainer.app').factory('StateManager', [
} else { } else {
state.application = applicationState; state.application = applicationState;
state.loading = false; state.loading = false;
$analytics.setOptOut(!state.application.enableTelemetry);
deferred.resolve(state); deferred.resolve(state);
} }
} else { } else {

View file

@ -29,20 +29,16 @@
<div> <div>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Opt-out</u> <i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>Opt-out</u>
<ul> <ul>
<li>You may opt-out by passing the <em>--no-analytics</em> flag as part of the docker run command when starting Portainer.</li> <li>You may opt-out by turning off the analytics in the settings page.</li>
<li
>If you believe that we could improve our analytics approach make sure to let us know! There is an open discussion on
<a target="_blank" href="https://github.com/portainer/portainer/issues/3310"><i class="fab fa-github" aria-hidden="true"></i> Github</a></li
>
</ul> </ul>
</div> </div>
<div> <div>
<i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>What we collect & GDPR</u> <i class="fa fa-chevron-circle-right" aria-hidden="true"></i> <u>What we collect & GDPR</u>
<ul> <ul>
<li <li>
>We dont know who uses Portainer, where its used, to what scale its used, all we know (from analytics) is how often Portainer is used and which pages within the app We don't know who uses Portainer, where its used, to what scale its used, all we know (from analytics) is how often Portainer is used and which pages within the app
are most frequently used.</li are most frequently used.
> </li>
<li>As we are only collecting a very small amount of totally anonymous data, it is deemed that opt-in is not required.</li> <li>As we are only collecting a very small amount of totally anonymous data, it is deemed that opt-in is not required.</li>
</ul> </ul>
</div> </div>

View file

@ -82,6 +82,17 @@
</div> </div>
</div> </div>
<!-- !actions --> <!-- !actions -->
<!-- enableTelemetry-->
<div class="form-group">
<div class="col-sm-12">
<input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" />
<span class="text-muted small" style="margin-left: 2px;"
>Allow collection of anonymous statistics. You can find more information about this in our
<a href="https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/" target="_blank">privacy policy</a>.</span
>
</div>
</div>
<!-- !enableTelemetry-->
</form> </form>
<!-- !init password form --> <!-- !init password form -->
</div> </div>

View file

@ -5,16 +5,18 @@ angular.module('portainer.app').controller('InitAdminController', [
'Notifications', 'Notifications',
'Authentication', 'Authentication',
'StateManager', 'StateManager',
'SettingsService',
'UserService', 'UserService',
'EndpointService', 'EndpointService',
'ExtensionService', 'ExtensionService',
function ($async, $scope, $state, Notifications, Authentication, StateManager, UserService, EndpointService, ExtensionService) { function ($async, $scope, $state, Notifications, Authentication, StateManager, SettingsService, UserService, EndpointService, ExtensionService) {
$scope.logo = StateManager.getState().application.logo; $scope.logo = StateManager.getState().application.logo;
$scope.formValues = { $scope.formValues = {
Username: 'admin', Username: 'admin',
Password: '', Password: '',
ConfirmPassword: '', ConfirmPassword: '',
enableTelemetry: true,
}; };
$scope.state = { $scope.state = {
@ -45,6 +47,10 @@ angular.module('portainer.app').controller('InitAdminController', [
.then(function success() { .then(function success() {
return retrieveAndSaveEnabledExtensions(); return retrieveAndSaveEnabledExtensions();
}) })
.then(function success() {
StateManager.updateEnableTelemetry($scope.formValues.enableTelemetry);
return SettingsService.update({ enableTelemetry: $scope.formValues.enableTelemetry });
})
.then(function () { .then(function () {
return EndpointService.endpoints(0, 100); return EndpointService.endpoints(0, 100);
}) })

View file

@ -26,6 +26,18 @@
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" /><i></i> </label> <label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" /><i></i> </label>
</div> </div>
</div> </div>
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_enableTelemetry" class="control-label text-left">
Allow the collection of anonymous statistics
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" /><i></i> </label>
</div>
<div class="col-sm-12 text-muted small" style="margin-top: 10px;">
You can find more information about this in our
<a href="https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/" target="_blank">privacy policy</a>.
</div>
</div>
<div ng-if="formValues.customLogo"> <div ng-if="formValues.customLogo">
<div class="form-group"> <div class="form-group">
<span class="col-sm-12 text-muted small"> <span class="col-sm-12 text-muted small">

View file

@ -36,6 +36,7 @@ angular.module('portainer.app').controller('SettingsController', [
allowDeviceMappingForRegularUsers: false, allowDeviceMappingForRegularUsers: false,
allowStackManagementForRegularUsers: false, allowStackManagementForRegularUsers: false,
disableContainerCapabilitiesForRegularUsers: false, disableContainerCapabilitiesForRegularUsers: false,
enableTelemetry: false,
}; };
$scope.isContainerEditDisabled = function isContainerEditDisabled() { $scope.isContainerEditDisabled = function isContainerEditDisabled() {
@ -85,6 +86,7 @@ angular.module('portainer.app').controller('SettingsController', [
settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers; settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers;
settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers; settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers;
settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers; settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers;
settings.EnableTelemetry = $scope.formValues.enableTelemetry;
$scope.state.actionInProgress = true; $scope.state.actionInProgress = true;
updateSettings(settings); updateSettings(settings);
@ -105,6 +107,7 @@ angular.module('portainer.app').controller('SettingsController', [
StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers); StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers);
StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers); StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers);
StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers); StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers);
StateManager.updateEnableTelemetry(settings.EnableTelemetry);
$state.reload(); $state.reload();
}) })
.catch(function error(err) { .catch(function error(err) {
@ -133,6 +136,7 @@ angular.module('portainer.app').controller('SettingsController', [
$scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers; $scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers;
$scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers; $scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers;
$scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers; $scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers;
$scope.formValues.enableTelemetry = settings.EnableTelemetry;
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings'); Notifications.error('Failure', err, 'Unable to retrieve application settings');

View file

@ -25,7 +25,6 @@ import 'angular-resource';
import 'angular-utils-pagination'; import 'angular-utils-pagination';
import 'angular-local-storage'; import 'angular-local-storage';
import 'angular-jwt'; import 'angular-jwt';
import 'angular-google-analytics';
import 'angular-json-tree'; import 'angular-json-tree';
import 'angular-loading-bar'; import 'angular-loading-bar';
import 'angular-clipboard'; import 'angular-clipboard';
@ -37,5 +36,6 @@ import 'js-yaml/dist/js-yaml.js';
import 'angular-ui-bootstrap'; import 'angular-ui-bootstrap';
import 'angular-moment-picker'; import 'angular-moment-picker';
import 'angular-multiselect/isteven-multi-select.js'; import 'angular-multiselect/isteven-multi-select.js';
import 'angulartics/dist/angulartics.min.js';
window.angular = angular; window.angular = angular;

View file

@ -177,12 +177,12 @@ function shell_run_container() {
'docker rm -f portainer', 'docker rm -f portainer',
'docker run -d -p 8000:8000 -p 9000:9000 -v $(pwd)/dist:/app -v ' + 'docker run -d -p 8000:8000 -p 9000:9000 -v $(pwd)/dist:/app -v ' +
portainer_data + portainer_data +
':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics', ':/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer',
].join(';'); ].join(';');
} }
function shell_run_localserver() { function shell_run_localserver() {
return './dist/portainer --no-analytics'; return './dist/portainer';
} }
function shell_install_yarndeps() { function shell_install_yarndeps() {

View file

@ -10,9 +10,6 @@
"bugs": { "bugs": {
"url": "https://github.com/portainer/portainer/issues" "url": "https://github.com/portainer/portainer/issues"
}, },
"config": {
"GA_ID": "UA-84944922-2"
},
"licenses": [ "licenses": [
{ {
"type": "Zlib", "type": "Zlib",
@ -56,7 +53,6 @@
"angular": "1.8.0", "angular": "1.8.0",
"angular-clipboard": "^1.6.2", "angular-clipboard": "^1.6.2",
"angular-file-saver": "^1.1.3", "angular-file-saver": "^1.1.3",
"angular-google-analytics": "github:revolunet/angular-google-analytics#semver:~1.1.9",
"angular-json-tree": "1.0.1", "angular-json-tree": "1.0.1",
"angular-jwt": "~0.1.8", "angular-jwt": "~0.1.8",
"angular-loading-bar": "~0.9.0", "angular-loading-bar": "~0.9.0",
@ -71,6 +67,7 @@
"angular-utils-pagination": "~0.11.1", "angular-utils-pagination": "~0.11.1",
"angularjs-scroll-glue": "^2.2.0", "angularjs-scroll-glue": "^2.2.0",
"angularjs-slider": "^6.4.0", "angularjs-slider": "^6.4.0",
"angulartics": "^1.6.0",
"babel-plugin-angularjs-annotate": "^0.10.0", "babel-plugin-angularjs-annotate": "^0.10.0",
"bootbox": "^5.4.0", "bootbox": "^5.4.0",
"bootstrap": "^3.4.0", "bootstrap": "^3.4.0",

View file

@ -1,5 +1,5 @@
const path = require('path'); const path = require('path');
const { ProvidePlugin, IgnorePlugin, DefinePlugin } = require('webpack'); const { ProvidePlugin, IgnorePlugin } = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const WebpackBuildNotifierPlugin = require('webpack-build-notifier'); const WebpackBuildNotifierPlugin = require('webpack-build-notifier');
const CleanTerminalPlugin = require('clean-terminal-webpack-plugin'); const CleanTerminalPlugin = require('clean-terminal-webpack-plugin');
@ -92,9 +92,6 @@ module.exports = {
collections: true, collections: true,
paths: true, paths: true,
}), }),
new DefinePlugin({
__CONFIG_GA_ID: JSON.stringify(pkg.config.GA_ID),
}),
], ],
optimization: { optimization: {
splitChunks: { splitChunks: {

View file

@ -1205,10 +1205,6 @@ angular-file-saver@^1.1.3:
blob-tmp "^1.0.0" blob-tmp "^1.0.0"
file-saver "^1.3.3" file-saver "^1.3.3"
"angular-google-analytics@github:revolunet/angular-google-analytics#semver:~1.1.9":
version "1.1.8"
resolved "https://codeload.github.com/revolunet/angular-google-analytics/tar.gz/92768a525870bc066dcf85fbe9d9f115358a6d91"
angular-json-tree@1.0.1: angular-json-tree@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/angular-json-tree/-/angular-json-tree-1.0.1.tgz#61b6e76ab165130335d9ec46fa572eb99604de51" resolved "https://registry.yarnpkg.com/angular-json-tree/-/angular-json-tree-1.0.1.tgz#61b6e76ab165130335d9ec46fa572eb99604de51"
@ -1301,6 +1297,11 @@ angularjs-slider@^6.4.0:
resolved "https://registry.yarnpkg.com/angularjs-slider/-/angularjs-slider-6.7.0.tgz#eb2229311b81b79315a36e7b5eb700e128f50319" resolved "https://registry.yarnpkg.com/angularjs-slider/-/angularjs-slider-6.7.0.tgz#eb2229311b81b79315a36e7b5eb700e128f50319"
integrity sha512-Cizsuax65wN2Y+htmA3safE5ALOSCyWcKyWkziaO8vCVymi26bQQs6kKDhkYc8GFix/KE7Oc9gH3QLlTUgD38w== integrity sha512-Cizsuax65wN2Y+htmA3safE5ALOSCyWcKyWkziaO8vCVymi26bQQs6kKDhkYc8GFix/KE7Oc9gH3QLlTUgD38w==
angulartics@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/angulartics/-/angulartics-1.6.0.tgz#a89c17ef8ea2334ebced65d6265951846f848172"
integrity sha512-fywhCi1InawcX+rpLv9NQ32Ed87KoZeH20SUIsRUz9dYJSxuk4/uxiKiopITveGxTC8THYHFEATj9Y/X+BvMqA==
ansi-colors@^3.0.0, ansi-colors@^3.2.1: ansi-colors@^3.0.0, ansi-colors@^3.2.1:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"