Skip to content

Commit f95cac0

Browse files
committed
feat(telemetry): implement DaoTelemetryService for reporting usage events to Tianji endpoint
1 parent ae38db3 commit f95cac0

8 files changed

Lines changed: 312 additions & 7 deletions

File tree

src/dao/browser/telemetry/BUILD.gn

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2026 Dao Browser Authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
# -----------------------------------------------------------------------------
6+
# Public source_set: dao_telemetry
7+
#
8+
# Lightweight HTTP-based event reporter that pushes anonymous usage events
9+
# to the Tianji application tracking endpoint. Compiled into //chrome/browser
10+
# so it can be reached from PostProfileInit and from agent-side handlers.
11+
# -----------------------------------------------------------------------------
12+
13+
source_set("dao_telemetry") {
14+
sources = [
15+
"dao_telemetry_service.cc",
16+
"dao_telemetry_service.h",
17+
]
18+
19+
deps = [
20+
"//base",
21+
"//chrome/browser/profiles:profile",
22+
"//content/public/browser",
23+
"//net/traffic_annotation",
24+
"//services/network/public/cpp",
25+
"//services/network/public/mojom",
26+
"//url",
27+
]
28+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2026 Dao Browser Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#include "dao/browser/telemetry/dao_telemetry_service.h"
6+
7+
#include <utility>
8+
9+
#include "base/functional/bind.h"
10+
#include "base/json/json_writer.h"
11+
#include "base/logging.h"
12+
#include "base/memory/raw_ptr.h"
13+
#include "base/memory/weak_ptr.h"
14+
#include "base/no_destructor.h"
15+
#include "base/values.h"
16+
#include "chrome/browser/profiles/profile.h"
17+
#include "content/public/browser/storage_partition.h"
18+
#include "net/base/load_flags.h"
19+
#include "net/traffic_annotation/network_traffic_annotation.h"
20+
#include "services/network/public/cpp/resource_request.h"
21+
#include "services/network/public/cpp/shared_url_loader_factory.h"
22+
#include "services/network/public/cpp/simple_url_loader.h"
23+
#include "services/network/public/mojom/url_response_head.mojom.h"
24+
#include "url/gurl.h"
25+
26+
namespace dao {
27+
28+
namespace {
29+
30+
// Tianji application tracking endpoint and identifier. See
31+
// https://tianji.dev/docs/application/tracking for the protocol.
32+
constexpr char kTianjiEventEndpoint[] =
33+
"https://app.tianji.dev/api/application/send";
34+
constexpr char kTianjiApplicationId[] = "cmpb4zdf0he9p78rcrkg6j7vg";
35+
36+
} // namespace
37+
38+
class DaoTelemetryService::Impl {
39+
public:
40+
Impl() = default;
41+
~Impl() = default;
42+
43+
Impl(const Impl&) = delete;
44+
Impl& operator=(const Impl&) = delete;
45+
46+
void SetProfile(Profile* profile) { profile_ = profile; }
47+
48+
void ReportBrowserOpenedOnce() {
49+
if (browser_opened_reported_) {
50+
return;
51+
}
52+
browser_opened_reported_ = true;
53+
ReportEvent("open", base::DictValue());
54+
}
55+
56+
void ReportEvent(const std::string& event_name,
57+
base::DictValue event_data) {
58+
if (!profile_) {
59+
return;
60+
}
61+
62+
base::DictValue payload;
63+
payload.Set("application", kTianjiApplicationId);
64+
payload.Set("name", event_name);
65+
payload.Set("data", std::move(event_data));
66+
67+
base::DictValue envelope;
68+
envelope.Set("type", "event");
69+
envelope.Set("payload", std::move(payload));
70+
71+
std::string body;
72+
if (!base::JSONWriter::Write(envelope, &body)) {
73+
return;
74+
}
75+
76+
auto request = std::make_unique<network::ResourceRequest>();
77+
request->url = GURL(kTianjiEventEndpoint);
78+
request->method = "POST";
79+
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
80+
request->load_flags = net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
81+
82+
static const net::NetworkTrafficAnnotationTag annotation =
83+
net::DefineNetworkTrafficAnnotation("dao_telemetry_tianji", R"(
84+
semantics {
85+
sender: "Dao Browser Telemetry"
86+
description:
87+
"Reports anonymous application events (browser opened, "
88+
"agent message sent, etc.) to the Tianji application "
89+
"tracking endpoint so the Dao Browser team can understand "
90+
"aggregate feature usage."
91+
trigger:
92+
"Browser startup, or specific user-initiated actions like "
93+
"sending a message to the agent."
94+
data:
95+
"An application identifier, an event name (e.g. 'open'), "
96+
"and optionally a small dict of event metadata. No URLs, "
97+
"no page content, no user credentials, no PII."
98+
destination: WEBSITE
99+
}
100+
policy {
101+
cookies_allowed: NO
102+
setting:
103+
"There is no in-product toggle yet; telemetry can be "
104+
"disabled by blocking app.tianji.dev at the network layer."
105+
policy_exception_justification:
106+
"Anonymous aggregate usage metrics for product "
107+
"improvement; no user identifiers are sent."
108+
})");
109+
110+
std::unique_ptr<network::SimpleURLLoader> loader =
111+
network::SimpleURLLoader::Create(std::move(request), annotation);
112+
loader->SetTimeoutDuration(base::Seconds(10));
113+
loader->AttachStringForUpload(body, "application/json");
114+
115+
network::SimpleURLLoader* loader_ptr = loader.get();
116+
inflight_[loader_ptr] = std::move(loader);
117+
118+
scoped_refptr<network::SharedURLLoaderFactory> factory =
119+
profile_->GetDefaultStoragePartition()
120+
->GetURLLoaderFactoryForBrowserProcess();
121+
122+
loader_ptr->DownloadToString(
123+
factory.get(),
124+
base::BindOnce(&Impl::OnRequestComplete, weak_factory_.GetWeakPtr(),
125+
loader_ptr),
126+
/*max_body_size=*/8 * 1024);
127+
}
128+
129+
private:
130+
void OnRequestComplete(network::SimpleURLLoader* loader_ptr,
131+
std::optional<std::string> body) {
132+
inflight_.erase(loader_ptr);
133+
}
134+
135+
raw_ptr<Profile> profile_ = nullptr;
136+
bool browser_opened_reported_ = false;
137+
std::map<network::SimpleURLLoader*, std::unique_ptr<network::SimpleURLLoader>>
138+
inflight_;
139+
base::WeakPtrFactory<Impl> weak_factory_{this};
140+
};
141+
142+
// static
143+
DaoTelemetryService* DaoTelemetryService::GetInstance() {
144+
static base::NoDestructor<DaoTelemetryService> instance;
145+
return instance.get();
146+
}
147+
148+
DaoTelemetryService::DaoTelemetryService() : impl_(std::make_unique<Impl>()) {}
149+
DaoTelemetryService::~DaoTelemetryService() = default;
150+
151+
void DaoTelemetryService::SetProfile(Profile* profile) {
152+
impl_->SetProfile(profile);
153+
}
154+
155+
void DaoTelemetryService::ReportBrowserOpenedOnce() {
156+
impl_->ReportBrowserOpenedOnce();
157+
}
158+
159+
void DaoTelemetryService::ReportEvent(const std::string& event_name,
160+
base::DictValue event_data) {
161+
impl_->ReportEvent(event_name, std::move(event_data));
162+
}
163+
164+
} // namespace dao
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2026 Dao Browser Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#ifndef DAO_BROWSER_TELEMETRY_DAO_TELEMETRY_SERVICE_H_
6+
#define DAO_BROWSER_TELEMETRY_DAO_TELEMETRY_SERVICE_H_
7+
8+
#include <memory>
9+
#include <string>
10+
11+
#include "base/no_destructor.h"
12+
#include "base/values.h"
13+
14+
class Profile;
15+
16+
namespace network {
17+
class SimpleURLLoader;
18+
}
19+
20+
namespace dao {
21+
22+
// Process-wide telemetry facade. Reports anonymous usage events to a Tianji
23+
// application endpoint over HTTPS.
24+
//
25+
// Threading: all methods must be called on the UI thread.
26+
//
27+
// Lifetime: a single instance lives for the duration of the process. Pending
28+
// SimpleURLLoaders are kept alive in an in-flight map and removed on
29+
// completion (results are discarded — telemetry must never affect user-facing
30+
// behavior).
31+
class DaoTelemetryService {
32+
public:
33+
static DaoTelemetryService* GetInstance();
34+
35+
DaoTelemetryService(const DaoTelemetryService&) = delete;
36+
DaoTelemetryService& operator=(const DaoTelemetryService&) = delete;
37+
38+
// Records the URLLoaderFactory source. Safe to call multiple times; the most
39+
// recent profile wins. Without this, ReportEvent silently drops the event.
40+
void SetProfile(Profile* profile);
41+
42+
// Fires the canonical "browser opened" event once per process. Subsequent
43+
// calls are no-ops.
44+
void ReportBrowserOpenedOnce();
45+
46+
// Fires an arbitrary application event. `event_data` is forwarded as the
47+
// payload.data dict. No-op if SetProfile has not been called.
48+
void ReportEvent(const std::string& event_name, base::DictValue event_data);
49+
50+
private:
51+
friend class base::NoDestructor<DaoTelemetryService>;
52+
53+
DaoTelemetryService();
54+
~DaoTelemetryService();
55+
56+
class Impl;
57+
std::unique_ptr<Impl> impl_;
58+
};
59+
60+
} // namespace dao
61+
62+
#endif // DAO_BROWSER_TELEMETRY_DAO_TELEMETRY_SERVICE_H_

src/dao/browser/ui/webui/resources/agent/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ build_webui("build") {
3939
"pi_llm_stream.ts",
4040
"pi_tool_adapter.ts",
4141
"readability_bundle.ts",
42+
"dao_telemetry.ts",
4243
"skill_registry.ts",
4344
"skills.ts",
4445
"tool_catalog.ts",

src/dao/browser/ui/webui/resources/agent/dao_chat_view.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {getActiveLLMConfig} from './llm_config.js';
2828
import {lookupModelCapabilities} from './model_capabilities.js';
2929
import {buildPageAttachment, buildSelectionAttachment, captureCurrentPageMarkdown, clearCurrentSelection, fetchCurrentPageInfo, fetchCurrentSelection, fetchPageProbeState, insertTextIntoFocusedInput, isCapturablePageUrl, type PageInfo, type SelectionCapture} from './dao_page_capture.js';
3030
import {renderShareImage} from './dao_share_image.js';
31+
import {reportTelemetryEvent} from './dao_telemetry.js';
3132
import {buildAgentTools} from './pi_tool_adapter.js';
3233
import {toolConfigChannel} from './tool_catalog.js';
3334
import './dao_chat_history_panel.js';
@@ -805,6 +806,10 @@ export class DaoChatView extends CrLitElement {
805806
this.origSendMessage_ = iface.sendMessage.bind(iface);
806807
// eslint-disable-next-line @typescript-eslint/no-explicit-any
807808
iface.sendMessage = async (text: string, attachments: any[]) => {
809+
reportTelemetryEvent('agent_message_send', {
810+
textLength: text?.length ?? 0,
811+
attachmentCount: attachments?.length ?? 0,
812+
});
808813
this.refreshModel_();
809814
// Pull latest soul into the live systemPrompt before the turn is
810815
// packed into the LLM request. Handles the same-tab update_soul
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright 2026 Dao Browser Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// Lightweight client for the Tianji application tracking endpoint. Mirrors the
6+
// HTTP shape of `tianji-client-sdk` (POST /api/application/send with a
7+
// {type, payload} envelope) so the same Tianji application can receive both
8+
// C++-side `open` events and WebUI-side agent events.
9+
//
10+
// All errors are swallowed — telemetry must never affect user-facing
11+
// behavior. See dao_telemetry_service.cc for the C++ counterpart.
12+
13+
const TIANJI_ENDPOINT = 'https://app.tianji.dev/api/application/send';
14+
const TIANJI_APPLICATION_ID = 'cmpb4zdf0he9p78rcrkg6j7vg';
15+
16+
type EventData = Record<string, unknown>;
17+
18+
export function reportTelemetryEvent(
19+
eventName: string, eventData?: EventData): void {
20+
const envelope = {
21+
type: 'event',
22+
payload: {
23+
application: TIANJI_APPLICATION_ID,
24+
name: eventName,
25+
data: eventData ?? {},
26+
},
27+
};
28+
29+
try {
30+
void fetch(TIANJI_ENDPOINT, {
31+
method: 'POST',
32+
headers: {'Content-Type': 'application/json'},
33+
body: JSON.stringify(envelope),
34+
credentials: 'omit',
35+
keepalive: true,
36+
}).catch(() => {});
37+
} catch {
38+
// ignore
39+
}
40+
}

src/patches/chrome/browser/BUILD.gn.patch

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
2-
index 969c2af443..ca88af90b8 100644
2+
index 969c2af443..39a0fb2bc5 100644
33
--- a/chrome/browser/BUILD.gn
44
+++ b/chrome/browser/BUILD.gn
5-
@@ -6495,6 +6495,7 @@ static_library("browser") {
5+
@@ -6495,6 +6495,8 @@ static_library("browser") {
66
"//chrome/services/mac_notifications",
77
"//chrome/services/mac_notifications/public/cpp",
88
"//chrome/services/mac_notifications/public/mojom",
9+
+ "//dao/browser/telemetry:dao_telemetry",
910
+ "//dao/browser/updater:dao_updater",
1011
"//components/crash/core/app",
1112
"//components/device_signals/core/browser/mac",
1213
"//components/enterprise/platform_auth:enterprise_platform_auth",
13-
@@ -8307,7 +8308,7 @@ static_library("browser") {
14+
@@ -8307,7 +8309,7 @@ static_library("browser") {
1415
# all public deps of "browser" into a new target, and all circular dependencies
1516
# must include this target in a public_dep. This ensures stability and
1617
# correctness of the build-graph outside of //chrome/browser.
@@ -19,7 +20,7 @@ index 969c2af443..ca88af90b8 100644
1920
public_deps = [
2021
# To avoid every target that is circularly dependent on
2122
# //chrome/browser:browser needing to include 2 separate targets, we include
22-
@@ -8458,7 +8459,7 @@ static_library("browser_public_dependencies") {
23+
@@ -8458,7 +8460,7 @@ static_library("browser_public_dependencies") {
2324
# //chrome/browser:browser should be listed here. This ensures that the build
2425
# graph is correct and that all headers are generated before any translation
2526
# units are compiled.

src/patches/chrome/browser/chrome_browser_main_mac.mm.patch

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
diff --git a/chrome/browser/chrome_browser_main_mac.mm b/chrome/browser/chrome_browser_main_mac.mm
2-
index 7244516188..06660cb7e4 100644
2+
index 7244516188..4e775fa45a 100644
33
--- a/chrome/browser/chrome_browser_main_mac.mm
44
+++ b/chrome/browser/chrome_browser_main_mac.mm
5-
@@ -39,6 +39,7 @@
5+
@@ -39,6 +39,8 @@
66
#include "chrome/common/chrome_switches.h"
77
#include "chrome/common/pref_names.h"
88
#include "chrome/grit/branded_strings.h"
9+
+#include "dao/browser/telemetry/dao_telemetry_service.h"
910
+#include "dao/browser/updater/dao_updater_service.h"
1011
#include "components/metrics/metrics_service.h"
1112
#include "components/os_crypt/sync/os_crypt.h"
1213
#include "components/version_info/channel.h"
13-
@@ -178,6 +179,13 @@
14+
@@ -178,6 +180,16 @@
1415
void ChromeBrowserMainPartsMac::PostProfileInit(Profile* profile,
1516
bool is_initial_profile) {
1617
ChromeBrowserMainPartsPosix::PostProfileInit(profile, is_initial_profile);
@@ -20,6 +21,9 @@ index 7244516188..06660cb7e4 100644
2021
+ // already-running updater instance — Init() is idempotent.
2122
+ if (is_initial_profile) {
2223
+ dao::DaoUpdaterService::GetInstance()->Init();
24+
+
25+
+ dao::DaoTelemetryService::GetInstance()->SetProfile(profile);
26+
+ dao::DaoTelemetryService::GetInstance()->ReportBrowserOpenedOnce();
2327
+ }
2428
}
2529

0 commit comments

Comments
 (0)