From 0acca0e06b6e096367c12b6f68ea14f0f73b25c7 Mon Sep 17 00:00:00 2001 From: Greg Stoll Date: Wed, 23 Jul 2025 17:30:25 +0000 Subject: [PATCH] Bug 1963385 part 1 - add settings telemetry for Content Analysis a=diannaS Records telemetry for the settings used with Content Analysis. Note that there is no way in C++ to get the default value of a pref that was set via an Enterprise Policy (with setAndLockPref()) because that method actually sets the default value, so I had to modify some of my plans for telemetry to gather. For example, I was going to record interception points whose Enabled value differed from the default, but instead we record any interception points whose Enabled value is false. (right now these are equivalent, but Downloads will be default-off so this will change) For the allow/deny URL lists I didn't want to record the actual URLs on those lists, so we really do want to just record whether the values are different from the default. I hard-coded the default into ContentAnalysis.cpp, and we can rely on the JS test test_ca_enterprise_config_with_default_prefs_telemetry() to start failing if that changes. Also adds a forceRecreateClientForTest() method to ContentAnalysis that tests can use to force trying to connect to a client. (since this is when we record the settings telemetry) Original Revision: https://phabricator.services.mozilla.com/D252570 Differential Revision: https://phabricator.services.mozilla.com/D258214 --- .../contentanalysis/ContentAnalysis.cpp | 67 +++++++ .../contentanalysis/ContentAnalysis.h | 1 + .../components/contentanalysis/metrics.yaml | 152 +++++++++++++++ .../contentanalysis/nsIContentAnalysis.idl | 4 + .../browser_content_analysis_policies.js | 182 +++++++++++++++++- toolkit/components/glean/metrics_index.py | 1 + 6 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 toolkit/components/contentanalysis/metrics.yaml diff --git a/toolkit/components/contentanalysis/ContentAnalysis.cpp b/toolkit/components/contentanalysis/ContentAnalysis.cpp index 4f7a21812c8a..4b2a0a0fd382 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.cpp +++ b/toolkit/components/contentanalysis/ContentAnalysis.cpp @@ -23,6 +23,7 @@ #include "mozilla/dom/Promise.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/glean/ContentanalysisMetrics.h" #include "mozilla/Logging.h" #include "mozilla/ScopeExit.h" #include "mozilla/Services.h" @@ -77,6 +78,14 @@ const char* kPipePathNamePref = "browser.contentanalysis.pipe_path_name"; const char* kClientSignature = "browser.contentanalysis.client_signature"; const char* kAllowUrlPref = "browser.contentanalysis.allow_url_regex_list"; const char* kDenyUrlPref = "browser.contentanalysis.deny_url_regex_list"; +const char* kAgentNamePref = "browser.contentanalysis.agent_name"; +const char* kInterceptionPointPrefNames[] = { + "browser.contentanalysis.interception_point.clipboard.enabled", + "browser.contentanalysis.interception_point.download.enabled", + "browser.contentanalysis.interception_point.drag_and_drop.enabled", + "browser.contentanalysis.interception_point.file_upload.enabled", + "browser.contentanalysis.interception_point.print.enabled", +}; // Allow up to this many threads to be concurrently engaged in synchronous // communcations with the agent. That limit is set by @@ -1445,6 +1454,10 @@ bool ContentAnalysis::IsShutDown() { return *lock; } +NS_IMETHODIMP ContentAnalysis::ForceRecreateClientForTest() { + return CreateClientIfNecessary(/* aForceCreate */ true); +} + nsresult ContentAnalysis::CreateClientIfNecessary( bool aForceCreate /* = false */) { AssertIsOnMainThread(); @@ -1479,6 +1492,7 @@ nsresult ContentAnalysis::CreateClientIfNecessary( nsString clientSignature; // It's OK if this fails, we will default to the empty string Preferences::GetString(kClientSignature, clientSignature); + RecordConnectionSettingsTelemetry(clientSignature); LOGD("Dispatching background task to create Content Analysis client"); rv = NS_DispatchBackgroundTask(NS_NewCancelableRunnableFunction( "ContentAnalysis::CreateContentAnalysisClient", @@ -1494,6 +1508,59 @@ nsresult ContentAnalysis::CreateClientIfNecessary( return NS_OK; } +void ContentAnalysis::RecordConnectionSettingsTelemetry( + const nsString& clientSignature) { + AssertIsOnMainThread(); + { + nsCString agentName; + Preferences::GetCString(kAgentNamePref, agentName); + glean::content_analysis::agent_name.Set(agentName); + } + AutoTArray interceptionPointsOff; + for (const char* interceptionPointPrefName : kInterceptionPointPrefNames) { + bool interceptionPointPrefValue; + Preferences::GetBool(interceptionPointPrefName, + &interceptionPointPrefValue); + if (!interceptionPointPrefValue) { + interceptionPointsOff.AppendElement(interceptionPointPrefName); + } + } + if (!interceptionPointsOff.IsEmpty()) { + glean::content_analysis::interception_points_turned_off.Set( + interceptionPointsOff); + } + glean::content_analysis::show_blocked_result.Set( + StaticPrefs::browser_contentanalysis_show_blocked_result()); + glean::content_analysis::default_result.Set( + StaticPrefs::browser_contentanalysis_default_result()); + glean::content_analysis::timeout_result.Set( + StaticPrefs::browser_contentanalysis_timeout_result()); + if (!clientSignature.IsEmpty()) { + glean::content_analysis::client_signature.Set( + NS_ConvertUTF16toUTF8(clientSignature)); + } + glean::content_analysis::bypass_for_same_tab_operations.Set( + StaticPrefs::browser_contentanalysis_bypass_for_same_tab_operations()); + { + nsCString allowUrlRegexList; + Preferences::GetCString(kAllowUrlPref, allowUrlRegexList); + // Unfortunately because of the way enterprise policies set and lock prefs, + // we can't check if the value is different than the default in + // StaticPrefList.yaml, and instead we have to duplicate that value here. At + // least we have a test around this so we can update this value if the + // default changes. + const char* defaultAllowUrlRegexList = "^about:(?!blank|srcdoc).*"; + glean::content_analysis::allow_url_regex_list_set.Set( + !allowUrlRegexList.Equals(defaultAllowUrlRegexList)); + } + { + nsCString denyUrlRegexList; + Preferences::GetCString(kDenyUrlPref, denyUrlRegexList); + glean::content_analysis::deny_url_regex_list_set.Set( + !denyUrlRegexList.IsEmpty()); + } +} + NS_IMETHODIMP ContentAnalysis::GetIsActive(bool* aIsActive) { *aIsActive = false; diff --git a/toolkit/components/contentanalysis/ContentAnalysis.h b/toolkit/components/contentanalysis/ContentAnalysis.h index cf2e9fc13a2f..4f404d99b762 100644 --- a/toolkit/components/contentanalysis/ContentAnalysis.h +++ b/toolkit/components/contentanalysis/ContentAnalysis.h @@ -306,6 +306,7 @@ class ContentAnalysis final : public nsIContentAnalysis, template RefPtr> CallClientWithRetry( StaticString aMethodName, U&& aClientCallFunc); + void RecordConnectionSettingsTelemetry(const nsString& clientSignature); nsresult RunAnalyzeRequestTask( const RefPtr& aRequest, bool aAutoAcknowledge, diff --git a/toolkit/components/contentanalysis/metrics.yaml b/toolkit/components/contentanalysis/metrics.yaml new file mode 100644 index 000000000000..f05a192ecdd9 --- /dev/null +++ b/toolkit/components/contentanalysis/metrics.yaml @@ -0,0 +1,152 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Adding a new metric? We have docs for that! +# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html + +--- +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 +$tags: + - 'Firefox :: Data Loss Prevention' + +# The following metrics are used to track the usage of the Data Loss Prevention feature. + +content_analysis: + agent_name: + type: string + description: The name of the DLP agent that Firefox is + connected to. This is set via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + interception_points_turned_off: + type: string_list + description: The interception points that are turned off + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + show_blocked_result: + type: boolean + description: The show_blocked_result pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + default_result: + type: quantity + description: The default_result pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + unit: BlockWarnAllow + timeout_result: + type: quantity + description: The timeout_result pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + unit: BlockWarnAllow + client_signature: + type: string + description: The client_signature pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + bypass_for_same_tab_operations: + type: boolean + description: The bypass_for_same_tab_operations pref that is set + via enterprise policy. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + allow_url_regex_list_set: + type: boolean + description: Whether the allow_url_regex_list pref is set + to a non-default value. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com + deny_url_regex_list_set: + type: boolean + description: Whether the deny_url_regex_list pref is set + to a non-default value. + bugs: + - https://bugzil.la/1963385 + data_reviews: + - https://bugzil.la/1963385 + data_sensitivity: + - technical + expires: never + notification_emails: + - gstoll@mozilla.com + - haftandilian@mozilla.com + - dparks@mozilla.com diff --git a/toolkit/components/contentanalysis/nsIContentAnalysis.idl b/toolkit/components/contentanalysis/nsIContentAnalysis.idl index 1d4bd32edff8..2f6d3805f1c8 100644 --- a/toolkit/components/contentanalysis/nsIContentAnalysis.idl +++ b/toolkit/components/contentanalysis/nsIContentAnalysis.idl @@ -509,4 +509,8 @@ interface nsIContentAnalysis : nsISupports */ void sendCancelToAgent(in ACString aUserActionId); + /** + * Force the content analysis client to be recreated. For use in tests only. + */ + void forceRecreateClientForTest(); }; diff --git a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js index 3c541c92510a..738825af644b 100644 --- a/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js +++ b/toolkit/components/contentanalysis/tests/browser/browser_content_analysis_policies.js @@ -46,6 +46,12 @@ const kInterceptionPoints = [ "file_upload", "print", ]; +// Everything is on by default except download. +let kInterceptionPointsOnByDefault = kInterceptionPoints.slice(); +kInterceptionPointsOnByDefault.splice( + kInterceptionPointsOnByDefault.indexOf("download"), + 1 +); const kInterceptionPointsPlainTextOnly = ["clipboard", "drag_and_drop"]; const ca = Cc["@mozilla.org/contentanalysis;1"].getService( @@ -111,7 +117,7 @@ add_task(async function test_ca_active() { PoliciesPrefTracker.stop(); }); -add_task(async function test_ca_enterprise_config() { +add_task(async function test_ca_enterprise_config_with_default_prefs() { PoliciesPrefTracker.start(); await EnterprisePolicyTesting.setupPolicyEngineWithJson({ policies: { @@ -150,6 +156,79 @@ add_task(async function test_ca_enterprise_config() { PoliciesPrefTracker.stop(); }); +add_task( + async function test_ca_enterprise_config_with_default_prefs_telemetry() { + PoliciesPrefTracker.start(); + Services.fog.testResetFOG(); + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ContentAnalysis: { + Enabled: true, + }, + }, + }); + // Trigger the content analysis service to ensure telemetry is recorded. + let contentAnalysisService = Cc[ + "@mozilla.org/contentanalysis;1" + ].getService(Ci.nsIContentAnalysis); + is( + contentAnalysisService.isActive, + true, + "Content Analysis service is active" + ); + contentAnalysisService.forceRecreateClientForTest(); + + is( + Glean.contentAnalysis.agentName.testGetValue(), + "A DLP agent", + "agentName default" + ); + is( + Glean.contentAnalysis.interceptionPointsTurnedOff.testGetValue().length, + 1, + "interceptionPointsTurnedOff default" + ); + is( + Glean.contentAnalysis.interceptionPointsTurnedOff.testGetValue()[0], + "browser.contentanalysis.interception_point.download.enabled", + "interceptionPointsTurnedOff default" + ); + ok( + Glean.contentAnalysis.showBlockedResult.testGetValue(), + "showBlockedResult default" + ); + is( + Glean.contentAnalysis.defaultResult.testGetValue(), + 0, + "defaultResult default" + ); + is( + Glean.contentAnalysis.timeoutResult.testGetValue(), + 0, + "timeoutResult default" + ); + is( + Glean.contentAnalysis.clientSignature.testGetValue(), + null, + "clientSignature default" + ); + ok( + !Glean.contentAnalysis.bypassForSameTabOperations.testGetValue(), + "bypassForSameTabOperations default" + ); + ok( + !Glean.contentAnalysis.allowUrlRegexListSet.testGetValue(), + "allowUrlRegexListSet default" + ); + ok( + !Glean.contentAnalysis.denyUrlRegexListSet.testGetValue(), + "denyUrlRegexListSet default" + ); + + PoliciesPrefTracker.stop(); + } +); + add_task(async function test_ca_enterprise_config() { PoliciesPrefTracker.start(); const string1 = "this is a string"; @@ -277,6 +356,107 @@ add_task(async function test_ca_enterprise_config() { PoliciesPrefTracker.stop(); }); +add_task(async function test_ca_enterprise_config_telemetry() { + PoliciesPrefTracker.start(); + const string1 = "this is a string"; + const string2 = "this is another string"; + const string3 = "an agent name"; + const string4 = "a client signature"; + + Services.fog.testResetFOG(); + + await EnterprisePolicyTesting.setupPolicyEngineWithJson({ + policies: { + ContentAnalysis: { + Enabled: true, + PipePathName: "abc", + AgentTimeout: 99, + AllowUrlRegexList: string1, + DenyUrlRegexList: string2, + AgentName: string3, + ClientSignature: string4, + IsPerUser: true, + MaxConnectionsCount: 3, + ShowBlockedResult: false, + DefaultResult: 1, + TimeoutResult: 2, + BypassForSameTabOperations: true, + InterceptionPoints: { + Clipboard: { + Enabled: false, + PlainTextOnly: false, + }, + Download: { + Enabled: true, + }, + DragAndDrop: { + Enabled: false, + PlainTextOnly: false, + }, + FileUpload: { + Enabled: false, + }, + Print: { + Enabled: false, + }, + }, + }, + }, + }); + + // Trigger the content analysis service to ensure telemetry is recorded. + let contentAnalysisService = Cc["@mozilla.org/contentanalysis;1"].getService( + Ci.nsIContentAnalysis + ); + contentAnalysisService.forceRecreateClientForTest(); + is( + contentAnalysisService.isActive, + true, + "Content Analysis service is active" + ); + + is(Glean.contentAnalysis.agentName.testGetValue(), string3, "agentName"); + const interceptionPointsTurnedOff = + Glean.contentAnalysis.interceptionPointsTurnedOff.testGetValue(); + is( + interceptionPointsTurnedOff.length, + 4, + "interceptionPointsTurnedOff length" + ); + for (let i = 0; i < interceptionPointsTurnedOff.length; i++) { + is( + interceptionPointsTurnedOff[i], + `browser.contentanalysis.interception_point.${kInterceptionPointsOnByDefault[i]}.enabled`, + `interceptionPointsTurnedOff ${kInterceptionPointsOnByDefault[i]}` + ); + } + ok( + !Glean.contentAnalysis.showBlockedResult.testGetValue(), + "showBlockedResult" + ); + is(Glean.contentAnalysis.defaultResult.testGetValue(), 1, "defaultResult"); + is(Glean.contentAnalysis.timeoutResult.testGetValue(), 2, "timeoutResult"); + is( + Glean.contentAnalysis.clientSignature.testGetValue(), + string4, + "clientSignature" + ); + ok( + Glean.contentAnalysis.bypassForSameTabOperations.testGetValue(), + "bypassForSameTabOperations" + ); + ok( + Glean.contentAnalysis.allowUrlRegexListSet.testGetValue(), + "allowUrlRegexListSet" + ); + ok( + Glean.contentAnalysis.denyUrlRegexListSet.testGetValue(), + "denyUrlRegexListSet" + ); + + PoliciesPrefTracker.stop(); +}); + add_task(async function test_cleanup() { ca.testOnlySetCACmdLineArg(false); await EnterprisePolicyTesting.setupPolicyEngineWithJson({ diff --git a/toolkit/components/glean/metrics_index.py b/toolkit/components/glean/metrics_index.py index 24c14cb74f52..edd7dd27645e 100644 --- a/toolkit/components/glean/metrics_index.py +++ b/toolkit/components/glean/metrics_index.py @@ -141,6 +141,7 @@ firefox_desktop_metrics = [ "browser/modules/metrics.yaml", "dom/media/platforms/wmf/metrics.yaml", "services/fxaccounts/metrics.yaml", + "toolkit/components/contentanalysis/metrics.yaml", "toolkit/components/contentrelevancy/metrics.yaml", "toolkit/components/crashes/metrics.yaml", "toolkit/components/nimbus/metrics.yaml",