Fleet Premium: de-anonymize usage stats (#7013)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
* Fleet Premium: De-anonymize usage statistics by adding an `organization` property to the usage statistics payload. For Fleet Free instances, organization is reported as "unknown". To turn off usage statistics in the Fleet UI, head to **Settings > Organization settings > Usage statistics**, de-select the "Enable usage statistics" checkbox, and select **Save**.
|
||||
|
||||
@@ -62,6 +62,7 @@ func TestMaybeSendStatistics(t *testing.T) {
|
||||
},
|
||||
},
|
||||
StoredErrors: []byte(`[]`),
|
||||
Organization: "Fleet",
|
||||
}, true, nil
|
||||
}
|
||||
recorded := false
|
||||
@@ -73,7 +74,7 @@ func TestMaybeSendStatistics(t *testing.T) {
|
||||
err := trySendStatistics(context.Background(), ds, fleet.StatisticsFrequency, ts.URL, fleetConfig, &fleet.LicenseInfo{Tier: "premium"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, recorded)
|
||||
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","numHostsEnrolled":999,"numUsers":99,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"numWeeklyActiveUsers":111,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"storedErrors":[],"numHostsNotResponding":0}`, requestBody)
|
||||
assert.Equal(t, `{"anonymousIdentifier":"ident","fleetVersion":"1.2.3","licenseTier":"premium","organization":"Fleet","numHostsEnrolled":999,"numUsers":99,"numTeams":9,"numPolicies":0,"numLabels":3,"softwareInventoryEnabled":true,"vulnDetectionEnabled":true,"systemUsersEnabled":true,"hostsStatusWebHookEnabled":true,"numWeeklyActiveUsers":111,"hostsEnrolledByOperatingSystem":{"linux":[{"version":"1.2.3","numEnrolled":22}]},"storedErrors":[],"numHostsNotResponding":0}`, requestBody)
|
||||
}
|
||||
|
||||
func TestMaybeSendStatisticsSkipsSendingIfNotNeeded(t *testing.T) {
|
||||
|
||||
@@ -302,7 +302,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
|
||||
return err
|
||||
}
|
||||
|
||||
// disable anonymous analytics collection and enable software inventory for preview
|
||||
// disable analytics collection and enable software inventory for preview
|
||||
if err := client.ApplyAppConfig(map[string]map[string]bool{
|
||||
"host_settings": {"enable_software_inventory": true},
|
||||
"server_settings": {"enable_analytics": false},
|
||||
@@ -319,12 +319,12 @@ Use the stop and reset subcommands to manage the server and dependencies once st
|
||||
return errors.New("Expected 1 active enroll secret")
|
||||
}
|
||||
|
||||
// disable anonymous analytics collection for preview
|
||||
// disable analytics collection for preview
|
||||
if err := client.ApplyAppConfig(map[string]map[string]bool{
|
||||
"server_settings": {"enable_analytics": false},
|
||||
},
|
||||
); err != nil {
|
||||
return fmt.Errorf("Error disabling anonymous analytics collection in app config: %w", err)
|
||||
return fmt.Errorf("Error disabling analytics collection in app config: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Fleet will now log you into the UI automatically.")
|
||||
|
||||
@@ -105,7 +105,7 @@ func setupCommand() *cli.Command {
|
||||
return fmt.Errorf("error setting token for the current context: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Fleet Device Management Inc. periodically collects anonymous information about your instance.\nSending usage statistics from your Fleet instance is optional and can be disabled in settings.")
|
||||
fmt.Println("Fleet Device Management Inc. periodically collects information about your instance.\nSending usage statistics from your Fleet instance is optional and can be disabled in settings.")
|
||||
fmt.Println("[+] Fleet setup successful and context configured!")
|
||||
|
||||
return nil
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ℹ️ In Fleet 4.0, Usage statistics were introduced.
|
||||
```
|
||||
|
||||
Fleet Device Management Inc. periodically collects anonymous information about your instance.
|
||||
Fleet Device Management Inc. periodically collects information about your instance.
|
||||
|
||||
## What is included in usage statistics in Fleet?
|
||||
|
||||
@@ -15,6 +15,7 @@ Fleet Device Management Inc. periodically collects anonymous information about y
|
||||
"anonymousIdentifier": "9pnzNmrES3mQG66UQtd29cYTiX2+fZ4CYxDvh495720=",
|
||||
"fleetVersion": "x.x.x",
|
||||
"licenseTier": "free",
|
||||
"organization": "Fleet",
|
||||
"numHostsEnrolled": 999,
|
||||
"numUsers": 999,
|
||||
"numTeams": 999,
|
||||
@@ -89,7 +90,7 @@ Fleet Device Management Inc. periodically collects anonymous information about y
|
||||
}
|
||||
```
|
||||
|
||||
- All statistics are anonymous and contain no personal information about any particular device, organization, or person.
|
||||
- Statistics contain no personal information about any particular device or person.
|
||||
|
||||
- Sending Usage statistics from your Fleet instance is optional and can be disabled.
|
||||
|
||||
|
||||
@@ -98,9 +98,9 @@ const ConfirmationPage = ({
|
||||
{importOsqueryConfig()}
|
||||
</div>
|
||||
<p>
|
||||
Fleet Device Management Inc. periodically collects anonymous information
|
||||
about your instance. Sending usage statistics from your Fleet instance
|
||||
is optional and can be disabled in settings.
|
||||
Fleet Device Management Inc. periodically collects information about
|
||||
your instance. Sending usage statistics from your Fleet instance is
|
||||
optional and can be disabled in settings.
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -87,7 +87,7 @@ const Statistics = ({
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>Usage statistics</h2>
|
||||
<p className={`${baseClass}__section-description`}>
|
||||
Help improve Fleet by sending anonymous usage statistics.
|
||||
Help improve Fleet by sending usage statistics.
|
||||
<br />
|
||||
<br />
|
||||
This information helps our team better understand feature adoption
|
||||
|
||||
@@ -70,6 +70,7 @@ export const usageStatsPreview = {
|
||||
anonymousIdentifier: "9pnzNmrES3mQG66UQtd29cYTiX2+fZ4CYxDvh495720=",
|
||||
fleetVersion: "x.x.x",
|
||||
licenseTier: "free",
|
||||
organization: "Fleet",
|
||||
numHostsEnrolled: 999,
|
||||
numUsers: 999,
|
||||
numTeams: 999,
|
||||
|
||||
@@ -70,6 +70,10 @@ func (ds *Datastore) ShouldSendStatistics(ctx context.Context, frequency time.Du
|
||||
stats.HostsEnrolledByOperatingSystem = enrolledHostsByOS
|
||||
stats.StoredErrors = storedErrs
|
||||
stats.NumHostsNotResponding = amountHostsNotResponding
|
||||
stats.Organization = "unknown"
|
||||
if license.IsPremium() {
|
||||
stats.Organization = license.Organization
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -125,15 +125,17 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
|
||||
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
|
||||
|
||||
license := &fleet.LicenseInfo{Tier: "premium"}
|
||||
premiumLicense := &fleet.LicenseInfo{Tier: "premium", Organization: "Fleet"}
|
||||
freeLicense := &fleet.LicenseInfo{Tier: "free"}
|
||||
|
||||
// First time running, we send statistics
|
||||
stats, shouldSend, err := ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, license)
|
||||
stats, shouldSend, err := ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, premiumLicense)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, shouldSend)
|
||||
assert.NotEmpty(t, stats.AnonymousIdentifier)
|
||||
assert.NotEmpty(t, stats.FleetVersion)
|
||||
assert.Equal(t, stats.LicenseTier, "premium")
|
||||
assert.Equal(t, stats.Organization, "Fleet")
|
||||
assert.Equal(t, stats.NumHostsEnrolled, 1)
|
||||
assert.Equal(t, stats.NumUsers, 2)
|
||||
assert.Equal(t, stats.NumTeams, 1)
|
||||
@@ -152,7 +154,7 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// If we try right away, it shouldn't ask to send
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, license)
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, fleet.StatisticsFrequency, fleetConfig, premiumLicense)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, shouldSend)
|
||||
|
||||
@@ -224,10 +226,12 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Lower the frequency to trigger an "outdated" sent
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, license)
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, premiumLicense)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, shouldSend)
|
||||
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
|
||||
assert.Equal(t, stats.LicenseTier, "premium")
|
||||
assert.Equal(t, stats.Organization, "Fleet")
|
||||
assert.Equal(t, stats.NumHostsEnrolled, 5)
|
||||
assert.Equal(t, stats.NumUsers, 2)
|
||||
assert.Equal(t, stats.NumWeeklyActiveUsers, 0) // no active user since last stats were sent
|
||||
@@ -255,10 +259,12 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
// wait a bit and resend statistics
|
||||
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
|
||||
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, license)
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, premiumLicense)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, shouldSend)
|
||||
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
|
||||
assert.Equal(t, stats.LicenseTier, "premium")
|
||||
assert.Equal(t, stats.Organization, "Fleet")
|
||||
assert.Equal(t, stats.NumHostsEnrolled, 5)
|
||||
assert.Equal(t, stats.NumUsers, 2)
|
||||
assert.Equal(t, stats.NumWeeklyActiveUsers, 1)
|
||||
@@ -279,10 +285,21 @@ func testStatisticsShouldSend(t *testing.T, ds *Datastore) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, license)
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, premiumLicense)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, shouldSend)
|
||||
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
|
||||
assert.Equal(t, stats.LicenseTier, "premium")
|
||||
assert.Equal(t, stats.Organization, "Fleet")
|
||||
assert.Equal(t, 6, stats.NumHostsEnrolled)
|
||||
assert.Equal(t, 1, stats.NumHostsNotResponding)
|
||||
|
||||
// trigger again with a free license, organization should be "unknown"
|
||||
time.Sleep(1100 * time.Millisecond) // ensure the DB timestamp is not in the same second
|
||||
stats, shouldSend, err = ds.ShouldSendStatistics(ctx, time.Millisecond, fleetConfig, freeLicense)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, shouldSend)
|
||||
assert.Equal(t, firstIdentifier, stats.AnonymousIdentifier)
|
||||
assert.Equal(t, stats.LicenseTier, "free")
|
||||
assert.Equal(t, stats.Organization, "unknown")
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ type StatisticsPayload struct {
|
||||
AnonymousIdentifier string `json:"anonymousIdentifier"`
|
||||
FleetVersion string `json:"fleetVersion"`
|
||||
LicenseTier string `json:"licenseTier"`
|
||||
Organization string `json:"organization"`
|
||||
NumHostsEnrolled int `json:"numHostsEnrolled"`
|
||||
NumUsers int `json:"numUsers"`
|
||||
NumTeams int `json:"numTeams"`
|
||||
|
||||
@@ -24,6 +24,7 @@ module.exports = {
|
||||
hostsEnrolledByOperatingSystem: { type: 'json', defaultsTo: {} },
|
||||
storedErrors: { type: 'json', defaultsTo: '[]' },
|
||||
numHostsNotResponding: { type: 'number', defaultsTo: 0, description: 'The number of hosts per deployment that have not submitted results for distibuted queries. A host is counted as not responding if Fleet hasn\'t received a distributed write to requested distibuted queries for the host during the 2-hour interval since the host was last seen. Hosts that have not been seen for 7 days or more are not counted.', },
|
||||
organization: { type: 'string', defaultsTo: 'unknown', description: 'For Fleet Premium deployments, the organization registered with the license.', },
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ module.exports = {
|
||||
hostsEnrolledByOperatingSystem: { required: true, type: 'json' },
|
||||
storedErrors: { required: true, type: 'json' },
|
||||
numHostsNotResponding: { required: true, type: 'number', description: 'The number of hosts per deployment that have not submitted results for distibuted queries. A host is counted as not responding if Fleet hasn\'t received a distributed write to requested distibuted queries for the host during the 2-hour interval since the host was last seen. Hosts that have not been seen for 7 days or more are not counted.', },
|
||||
organization: { required: true, type: 'string' },
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
||||
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
||||
Cloud.setup({
|
||||
|
||||
/* eslint-disable */
|
||||
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled","numWeeklyActiveUsers","hostsEnrolledByOperatingSystem","storedErrors"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":["emailAddress"]}}
|
||||
methods: {"downloadSitemap":{"verb":"GET","url":"/sitemap.xml","args":[]},"receiveUsageAnalytics":{"verb":"POST","url":"/api/v1/webhooks/receive-usage-analytics","args":["anonymousIdentifier","fleetVersion","licenseTier","numHostsEnrolled","numUsers","numTeams","numPolicies","numLabels","softwareInventoryEnabled","vulnDetectionEnabled","systemUsersEnabled","hostStatusWebhookEnabled","numWeeklyActiveUsers","hostsEnrolledByOperatingSystem","storedErrors","numHostsNotResponding","organization"]},"receiveFromGithub":{"verb":"GET","url":"/api/v1/webhooks/github","args":["botSignature","action","sender","repository","changes","issue","comment","pull_request","label"]},"deliverContactFormMessage":{"verb":"POST","url":"/api/v1/deliver-contact-form-message","args":["emailAddress","topic","firstName","lastName","message"]},"sendPasswordRecoveryEmail":{"verb":"POST","url":"/api/v1/entrance/send-password-recovery-email","args":["emailAddress"]},"signup":{"verb":"POST","url":"/api/v1/customers/signup","args":["emailAddress","password","organization","firstName","lastName"]},"updateProfile":{"verb":"POST","url":"/api/v1/account/update-profile","args":["firstName","lastName","organization","emailAddress"]},"updatePassword":{"verb":"POST","url":"/api/v1/account/update-password","args":["oldPassword","newPassword"]},"updateBillingCard":{"verb":"POST","url":"/api/v1/account/update-billing-card","args":["stripeToken","billingCardLast4","billingCardBrand","billingCardExpMonth","billingCardExpYear"]},"login":{"verb":"POST","url":"/api/v1/customers/login","args":["emailAddress","password","rememberMe"]},"logout":{"verb":"GET","url":"/api/v1/account/logout","args":[]},"createQuote":{"verb":"POST","url":"/api/v1/customers/create-quote","args":["numberOfHosts"]},"saveBillingInfoAndSubscribe":{"verb":"POST","url":"/api/v1/customers/save-billing-info-and-subscribe","args":["quoteId","paymentSource"]},"updatePasswordAndLogin":{"verb":"POST","url":"/api/v1/entrance/update-password-and-login","args":["password","token"]},"deliverDemoSignup":{"verb":"POST","url":"/api/v1/deliver-demo-signup","args":["emailAddress"]}}
|
||||
/* eslint-enable */
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user