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
This commit is contained in:
Greg Stoll
2025-07-23 17:30:25 +00:00
committed by dsmith@mozilla.com
parent 972e0fdf7c
commit 0acca0e06b
6 changed files with 406 additions and 1 deletions

View File

@@ -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<nsCString, 1> 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;

View File

@@ -306,6 +306,7 @@ class ContentAnalysis final : public nsIContentAnalysis,
template <typename T, typename U>
RefPtr<MozPromise<T, nsresult, true>> CallClientWithRetry(
StaticString aMethodName, U&& aClientCallFunc);
void RecordConnectionSettingsTelemetry(const nsString& clientSignature);
nsresult RunAnalyzeRequestTask(
const RefPtr<nsIContentAnalysisRequest>& aRequest, bool aAutoAcknowledge,

View File

@@ -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

View File

@@ -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();
};

View File

@@ -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({

View File

@@ -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",