Skip to content

Commit 966124a

Browse files
committed
feat: Add UI path statistics
1 parent 3ea5a3e commit 966124a

File tree

5 files changed

+198
-45
lines changed

5 files changed

+198
-45
lines changed

deltachat-jsonrpc/src/api.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,25 @@ use std::sync::Arc;
55
use std::time::Duration;
66
use std::{collections::HashMap, str::FromStr};
77

8-
use anyhow::{Context, Result, anyhow, bail, ensure};
9-
use deltachat::EventEmitter;
8+
use anyhow::{anyhow, bail, ensure, Context, Result};
109
pub use deltachat::accounts::Accounts;
1110
use deltachat::blob::BlobObject;
1211
use deltachat::chat::{
13-
self, Chat, ChatId, ChatItem, MessageListOptions, ProtectionStatus, add_contact_to_chat,
14-
forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, marknoticed_chat,
15-
remove_contact_from_chat,
12+
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
13+
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
14+
ProtectionStatus,
1615
};
1716
use deltachat::chatlist::Chatlist;
1817
use deltachat::config::Config;
1918
use deltachat::constants::DC_MSG_ID_DAYMARKER;
20-
use deltachat::contact::{Contact, ContactId, Origin, may_be_valid_addr};
19+
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
2120
use deltachat::context::get_info;
2221
use deltachat::ephemeral::Timer;
2322
use deltachat::imex;
2423
use deltachat::location;
2524
use deltachat::message::get_msg_read_receipts;
2625
use deltachat::message::{
27-
self, Message, MessageState, MsgId, Viewtype, delete_msgs_ex, markseen_msgs,
26+
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
2827
};
2928
use deltachat::peer_channels::{
3029
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
@@ -36,9 +35,10 @@ use deltachat::reaction::{get_msg_reactions, send_reaction};
3635
use deltachat::securejoin;
3736
use deltachat::stock_str::StockMessage;
3837
use deltachat::webxdc::StatusUpdateSerial;
38+
use deltachat::EventEmitter;
3939
use sanitize_filename::is_sanitized;
4040
use tokio::fs;
41-
use tokio::sync::{Mutex, RwLock, watch};
41+
use tokio::sync::{watch, Mutex, RwLock};
4242
use types::login_param::EnteredLoginParam;
4343
use walkdir::WalkDir;
4444
use yerpc::rpc;
@@ -64,7 +64,7 @@ use self::types::{
6464
JSONRPCMessageListItem, MessageNotificationInfo, MessageSearchResult, MessageViewtype,
6565
},
6666
};
67-
use crate::api::types::chat_list::{ChatListItemFetchResult, get_chat_list_item_by_id};
67+
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
6868
use crate::api::types::qr::QrObject;
6969

7070
#[derive(Debug)]
@@ -875,9 +875,15 @@ impl CommandApi {
875875
/// **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
876876
/// A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
877877
///
878-
async fn secure_join(&self, account_id: u32, qr: String, source: Option<u32>) -> Result<u32> {
878+
async fn secure_join(
879+
&self,
880+
account_id: u32,
881+
qr: String,
882+
source: Option<u32>,
883+
uipath: Option<u32>,
884+
) -> Result<u32> {
879885
let ctx = self.get_context(account_id).await?;
880-
let chat_id = securejoin::join_securejoin_with_source(&ctx, &qr, source).await?;
886+
let chat_id = securejoin::join_securejoin_with_source(&ctx, &qr, source, uipath).await?;
881887
Ok(chat_id.to_u32())
882888
}
883889

@@ -2017,7 +2023,7 @@ impl CommandApi {
20172023
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
20182024
let blob = message.get_webxdc_blob(&ctx, &path).await?;
20192025

2020-
use base64::{Engine as _, engine::general_purpose};
2026+
use base64::{engine::general_purpose, Engine as _};
20212027
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
20222028
}
20232029

src/securejoin.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ async fn get_self_fingerprint(context: &Context) -> Result<Fingerprint> {
146146
///
147147
/// The function returns immediately and the handshake will run in background.
148148
pub async fn join_securejoin(context: &Context, qr: &str) -> Result<ChatId> {
149-
join_securejoin_with_source(context, qr, None).await
149+
join_securejoin_with_source(context, qr, None, None).await
150150
}
151151

152152
/// Take a scanned QR-code and do the setup-contact/join-group/invite handshake.
@@ -159,6 +159,7 @@ pub async fn join_securejoin_with_source(
159159
context: &Context,
160160
qr: &str,
161161
source: Option<u32>,
162+
uipath: Option<u32>,
162163
) -> Result<ChatId> {
163164
let res = securejoin(context, qr).await.map_err(|err| {
164165
warn!(context, "Fatal joiner error: {:#}", err);
@@ -167,7 +168,7 @@ pub async fn join_securejoin_with_source(
167168
err
168169
})?;
169170

170-
self_reporting::count_securejoin_source(context, source)
171+
self_reporting::count_securejoin(context, source, uipath)
171172
.await
172173
.log_err(context)
173174
.ok();

src/self_reporting.rs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct Statistics {
2929
contact_stats: Vec<ContactStat>,
3030
message_stats: MessageStats,
3131
securejoin_source_stats: SecurejoinSourceStats,
32+
securejoin_uipath_stats: SecurejoinUIPathStats,
3233
}
3334

3435
#[derive(Serialize, PartialEq)]
@@ -83,6 +84,20 @@ struct SecurejoinSourceStats {
8384
scan: u32,
8485
}
8586

87+
#[derive(Debug, Clone, Copy, FromPrimitive, FromSql, PartialEq, Eq, PartialOrd, Ord)]
88+
enum SecurejoinUIPath {
89+
Unknown = 0,
90+
QrIcon = 1,
91+
NewContact = 2,
92+
}
93+
94+
#[derive(Serialize)]
95+
struct SecurejoinUIPathStats {
96+
other: u32,
97+
qr_icon: u32,
98+
new_contact: u32,
99+
}
100+
86101
/// Sends a message with statistics about the usage of Delta Chat,
87102
/// if the last time such a message was sent
88103
/// was more than a week ago.
@@ -189,8 +204,9 @@ async fn get_self_report(context: &Context) -> Result<String> {
189204
self_reporting_id,
190205
is_chatmail: context.is_chatmail().await?,
191206
contact_stats: get_contact_stats(context).await?,
192-
message_stats: get_message_stats(context, last_msgid).await?, // TODO
207+
message_stats: get_message_stats(context, last_msgid).await?,
193208
securejoin_source_stats: get_securejoin_source_stats(context).await?,
209+
securejoin_uipath_stats: get_securejoin_uipath_stats(context).await?,
194210
};
195211

196212
Ok(serde_json::to_string_pretty(&statistics)?)
@@ -423,7 +439,11 @@ async fn get_message_stats(context: &Context, last_msgid: u64) -> Result<Message
423439
Ok(message_stats)
424440
}
425441

426-
pub(crate) async fn count_securejoin_source(context: &Context, source: Option<u32>) -> Result<()> {
442+
pub(crate) async fn count_securejoin(
443+
context: &Context,
444+
source: Option<u32>,
445+
uipath: Option<u32>,
446+
) -> Result<()> {
427447
if context.get_config_bool(Config::SelfReporting).await? {
428448
let source = source
429449
.context("Missing securejoin source")
@@ -434,10 +454,23 @@ pub(crate) async fn count_securejoin_source(context: &Context, source: Option<u3
434454
.sql
435455
.execute(
436456
"INSERT INTO stats_securejoin_sources VALUES (?, 1)
437-
ON CONFLICT (source) DO UPDATE SET count=count+1;",
457+
ON CONFLICT (source) DO UPDATE SET count=count+1;",
438458
(source,),
439459
)
440460
.await?;
461+
462+
// We only get a UI path if the source is a QR code scan,
463+
// a loaded image, or a link pasted from the QR code,
464+
// so, no need to log an error if `uipath` is None:
465+
let uipath = uipath.unwrap_or(0);
466+
context
467+
.sql
468+
.execute(
469+
"INSERT INTO stats_securejoin_uipaths VALUES (?, 1)
470+
ON CONFLICT (uipath) DO UPDATE SET count=count+1;",
471+
(uipath,),
472+
)
473+
.await?;
441474
}
442475
Ok(())
443476
}
@@ -469,5 +502,29 @@ async fn get_securejoin_source_stats(context: &Context) -> Result<SecurejoinSour
469502
Ok(stats)
470503
}
471504

505+
async fn get_securejoin_uipath_stats(context: &Context) -> Result<SecurejoinUIPathStats> {
506+
let map = context
507+
.sql
508+
.query_map(
509+
"SELECT uipath, count FROM stats_securejoin_uipaths",
510+
(),
511+
|row| {
512+
let uipath: SecurejoinUIPath = row.get(0)?;
513+
let count: u32 = row.get(1)?;
514+
Ok((uipath, count))
515+
},
516+
|rows| Ok(rows.collect::<rusqlite::Result<BTreeMap<_, _>>>()?),
517+
)
518+
.await?;
519+
520+
let stats = SecurejoinUIPathStats {
521+
other: *map.get(&SecurejoinUIPath::Unknown).unwrap_or(&0),
522+
qr_icon: *map.get(&SecurejoinUIPath::QrIcon).unwrap_or(&0),
523+
new_contact: *map.get(&SecurejoinUIPath::NewContact).unwrap_or(&0),
524+
};
525+
526+
Ok(stats)
527+
}
528+
472529
#[cfg(test)]
473530
mod self_reporting_tests;

src/self_reporting/self_reporting_tests.rs

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ async fn test_self_report_one_contact() -> Result<()> {
8989
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
9090
async fn test_message_stats() -> Result<()> {
9191
fn check_report(report: &str, expected: &MessageStats) {
92+
let key = "message_stats";
9293
let actual: serde_json::Value = serde_json::from_str(&report).unwrap();
93-
let actual = &actual["message_stats"];
94+
let actual = &actual[key];
9495

9596
let expected = serde_json::to_string_pretty(&expected).unwrap();
9697
let expected: serde_json::Value = serde_json::from_str(&expected).unwrap();
@@ -184,6 +185,17 @@ async fn send_and_read_self_report(context: &TestContext) -> String {
184185

185186
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
186187
async fn test_self_report_securejoin_source_stats() -> Result<()> {
188+
async fn check_report(context: &TestContext, expected: &SecurejoinSourceStats) {
189+
let report = get_self_report(context).await.unwrap();
190+
let actual: serde_json::Value = serde_json::from_str(&report).unwrap();
191+
let actual = &actual["securejoin_source_stats"];
192+
193+
let expected = serde_json::to_string_pretty(&expected).unwrap();
194+
let expected: serde_json::Value = serde_json::from_str(&expected).unwrap();
195+
196+
assert_eq!(actual, &expected);
197+
}
198+
187199
let mut tcm = TestContextManager::new();
188200
let alice = &tcm.alice().await;
189201
let bob = &tcm.bob().await;
@@ -198,58 +210,123 @@ async fn test_self_report_securejoin_source_stats() -> Result<()> {
198210
scan: 0,
199211
};
200212

201-
check_securejoin_report(alice, &expected).await;
213+
check_report(alice, &expected).await;
202214

203215
let qr = get_securejoin_qr(bob, None).await?;
204216

205217
join_securejoin(alice, &qr).await?;
206218
expected.unknown += 1;
207-
check_securejoin_report(alice, &expected).await;
219+
check_report(alice, &expected).await;
208220

209221
join_securejoin(alice, &qr).await?;
210222
expected.unknown += 1;
211-
check_securejoin_report(alice, &expected).await;
223+
check_report(alice, &expected).await;
212224

213-
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Clipboard as u32)).await?;
225+
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Clipboard as u32), None).await?;
214226
expected.clipboard += 1;
215-
check_securejoin_report(alice, &expected).await;
216-
217-
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::ExternalLink as u32)).await?;
227+
check_report(alice, &expected).await;
228+
229+
join_securejoin_with_source(
230+
alice,
231+
&qr,
232+
Some(SecurejoinSource::ExternalLink as u32),
233+
None,
234+
)
235+
.await?;
218236
expected.external_link += 1;
219-
check_securejoin_report(alice, &expected).await;
220-
221-
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::InternalLink as u32)).await?;
237+
check_report(alice, &expected).await;
238+
239+
join_securejoin_with_source(
240+
alice,
241+
&qr,
242+
Some(SecurejoinSource::InternalLink as u32),
243+
None,
244+
)
245+
.await?;
222246
expected.internal_link += 1;
223-
check_securejoin_report(alice, &expected).await;
247+
check_report(alice, &expected).await;
224248

225-
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::ImageLoaded as u32)).await?;
249+
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::ImageLoaded as u32), None)
250+
.await?;
226251
expected.image_loaded += 1;
227-
check_securejoin_report(alice, &expected).await;
252+
check_report(alice, &expected).await;
228253

229-
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Scan as u32)).await?;
254+
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Scan as u32), None).await?;
230255
expected.scan += 1;
231-
check_securejoin_report(alice, &expected).await;
256+
check_report(alice, &expected).await;
232257

233-
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Clipboard as u32)).await?;
258+
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Clipboard as u32), None).await?;
234259
expected.clipboard += 1;
235-
check_securejoin_report(alice, &expected).await;
260+
check_report(alice, &expected).await;
236261

237-
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Clipboard as u32)).await?;
262+
join_securejoin_with_source(alice, &qr, Some(SecurejoinSource::Clipboard as u32), None).await?;
238263
expected.clipboard += 1;
239-
check_securejoin_report(alice, &expected).await;
264+
check_report(alice, &expected).await;
240265

241266
Ok(())
242267
}
243268

244-
async fn check_securejoin_report(context: &TestContext, expected: &SecurejoinSourceStats) {
245-
let report = get_self_report(context).await.unwrap();
246-
let actual: serde_json::Value = serde_json::from_str(&report).unwrap();
247-
let actual = &actual["securejoin_source_stats"];
269+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
270+
async fn test_self_report_securejoin_uipath_stats() -> Result<()> {
271+
async fn check_report(context: &TestContext, expected: &SecurejoinUIPathStats) {
272+
let report = get_self_report(context).await.unwrap();
273+
let actual: serde_json::Value = serde_json::from_str(&report).unwrap();
274+
let actual = &actual["securejoin_uipath_stats"];
275+
276+
let expected = serde_json::to_string_pretty(&expected).unwrap();
277+
let expected: serde_json::Value = serde_json::from_str(&expected).unwrap();
278+
279+
assert_eq!(actual, &expected);
280+
}
281+
282+
let mut tcm = TestContextManager::new();
283+
let alice = &tcm.alice().await;
284+
let bob = &tcm.bob().await;
285+
alice.set_config_bool(Config::SelfReporting, true).await?;
286+
287+
let mut expected = SecurejoinUIPathStats {
288+
other: 0,
289+
qr_icon: 0,
290+
new_contact: 0,
291+
};
292+
293+
check_report(alice, &expected).await;
294+
295+
let qr = get_securejoin_qr(bob, None).await?;
248296

249-
let expected = serde_json::to_string_pretty(&expected).unwrap();
250-
let expected: serde_json::Value = serde_json::from_str(&expected).unwrap();
297+
join_securejoin(alice, &qr).await?;
298+
expected.other += 1;
299+
check_report(alice, &expected).await;
300+
301+
join_securejoin(alice, &qr).await?;
302+
expected.other += 1;
303+
check_report(alice, &expected).await;
304+
305+
join_securejoin_with_source(
306+
alice,
307+
&qr,
308+
Some(0),
309+
Some(SecurejoinUIPath::NewContact as u32),
310+
)
311+
.await?;
312+
expected.new_contact += 1;
313+
check_report(alice, &expected).await;
314+
315+
join_securejoin_with_source(
316+
alice,
317+
&qr,
318+
Some(0),
319+
Some(SecurejoinUIPath::NewContact as u32),
320+
)
321+
.await?;
322+
expected.new_contact += 1;
323+
check_report(alice, &expected).await;
324+
325+
join_securejoin_with_source(alice, &qr, Some(0), Some(SecurejoinUIPath::QrIcon as u32)).await?;
326+
expected.qr_icon += 1;
327+
check_report(alice, &expected).await;
251328

252-
assert_eq!(actual, &expected);
329+
Ok(())
253330
}
254331

255332
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

0 commit comments

Comments
 (0)