Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parrot analytics library #74

Merged
merged 3 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ members = [
"honey-badger",
"mole",
"squirrel",
"parrot",
]
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ The library allows to register for and list withdraw collect offers (e.g. Lightn
## Squirrel
Squirrels love to store nuts for the winter.
The library allows backups of local data to be created.

## Parrot
This feathered companion, similar to its namesakes, repeats what he hears and as such delivers,
important analytics data about payments to the backend. This data is pseudonymized and used to improve our services.
6 changes: 6 additions & 0 deletions graphql/schemas/operations.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,9 @@ mutation RecoverBackup($schemaName: String!) {
updatedAt
}
}

mutation ReportPaymentTelemetry($telemetryId: String!, $events: PaymentTelemetryEventsInput) {
report_payment_telemetry(telemetryId: $telemetryId, events: $events) {
payFailed
}
}
12 changes: 12 additions & 0 deletions graphql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub use crate::errors::*;
pub use perro;
pub use reqwest;

use chrono::{DateTime, Utc};
use graphql_client::reqwest::{post_graphql, post_graphql_blocking};
use graphql_client::Response;
use perro::{permanent_failure, runtime_error, MapToError, OptionToError};
Expand Down Expand Up @@ -202,3 +203,14 @@ mod tests {
assert_eq!(timestamp, 1695314361 + 2 * 3600);
}
}

pub trait ToRfc3339 {
fn to_rfc3339(&self) -> String;
}

impl ToRfc3339 for SystemTime {
fn to_rfc3339(&self) -> String {
let datetime: DateTime<Utc> = DateTime::from(*self);
datetime.to_rfc3339()
}
}
8 changes: 8 additions & 0 deletions graphql/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,11 @@ pub struct CreateBackup;
response_derives = "Debug"
)]
pub struct RecoverBackup;

#[derive(GraphQLQuery)]
#[graphql(
schema_path = "schemas/schema_wallet_read.graphql",
query_path = "schemas/operations.graphql",
response_derives = "Debug"
)]
pub struct ReportPaymentTelemetry;
8 changes: 8 additions & 0 deletions parrot/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "parrot"
version = "0.1.0"
edition = "2021"

[dependencies]
graphql = { path = "../graphql" }
honey-badger = { path = "../honey-badger" }
220 changes: 220 additions & 0 deletions parrot/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
use graphql::schema::report_payment_telemetry::{
PayFailedInput, PayInitiatedInput, PaySource, PaySucceededInput, PaymentTelemetryEventsInput,
RequestInitiatedInput, RequestSucceededInput,
};
use graphql::schema::{report_payment_telemetry, ReportPaymentTelemetry};
use graphql::{build_client, post_blocking, ToRfc3339};
use honey_badger::Auth;
use std::sync::Arc;
use std::time::SystemTime;

pub enum PaymentSource {
Camera,
Clipboard,
Nfc,
Manual,
}
pub enum PayFailureReason {
NoRoute,
Unknown,
}

pub enum AnalyticsEvent {
PayInitiated {
payment_hash: String,

paid_amount_msat: u64,
requested_amount_msat: Option<u64>,
sats_per_user_currency: Option<u32>,

source: PaymentSource,
user_currency: String,

process_started_at: SystemTime,
executed_at: SystemTime,
},
PaySucceeded {
payment_hash: String,

ln_fees_paid_msat: u64,
confirmed_at: SystemTime,
},
PayFailed {
payment_hash: String,

reason: PayFailureReason,
failed_at: SystemTime,
},
RequestInitiated {
payment_hash: String,

entered_amount_msat: Option<u64>,
sats_per_user_currency: Option<u32>,

user_currency: String,
request_currency: String,

created_at: SystemTime,
},
RequestSucceeded {
payment_hash: String,

paid_amount_sat: u64,
channel_opening_fee_msat: u64,

received_at: SystemTime,
},
}

pub struct AnalyticsClient {
backend_url: String,
analytics_id: String,
auth: Arc<Auth>,
}

impl AnalyticsClient {
pub fn new(backend_url: String, analytics_id: String, auth: Arc<Auth>) -> Self {
Self {
backend_url,
analytics_id,
auth,
}
}

pub fn report_event(&self, analytics_event: AnalyticsEvent) -> graphql::Result<()> {
let variables = match analytics_event {
AnalyticsEvent::PayInitiated {
payment_hash,
paid_amount_msat,
requested_amount_msat,
sats_per_user_currency,
source,
user_currency,
process_started_at,
executed_at,
} => report_payment_telemetry::Variables {
telemetry_id: self.analytics_id.clone(),
events: Some(PaymentTelemetryEventsInput {
pay_failed: None,
pay_initiated: Some(PayInitiatedInput {
process_started_at: process_started_at.to_rfc3339(),
executed_at: executed_at.to_rfc3339(),
paid_amount_m_sat: paid_amount_msat,
payment_hash,
requested_amount_m_sat: requested_amount_msat.unwrap_or(0),
sats_per_user_currency: sats_per_user_currency.unwrap_or(0) as i64,
source: map_payment_source(source),
user_currency,
}),
pay_succeeded: None,
request_initiated: None,
request_succeeded: None,
}),
},
AnalyticsEvent::PaySucceeded {
payment_hash,
ln_fees_paid_msat,
confirmed_at,
} => report_payment_telemetry::Variables {
telemetry_id: self.analytics_id.clone(),
events: Some(PaymentTelemetryEventsInput {
pay_failed: None,
pay_initiated: None,
pay_succeeded: Some(PaySucceededInput {
confirmed_at: confirmed_at.to_rfc3339(),
ln_fees_paid_m_sat: ln_fees_paid_msat,
payment_hash,
}),
request_initiated: None,
request_succeeded: None,
}),
},
AnalyticsEvent::PayFailed {
payment_hash,
reason,
failed_at,
} => report_payment_telemetry::Variables {
telemetry_id: self.analytics_id.clone(),
events: Some(PaymentTelemetryEventsInput {
pay_failed: Some(PayFailedInput {
failed_at: failed_at.to_rfc3339(),
payment_hash,
reason: map_pay_failure_reason(reason),
}),
pay_initiated: None,
pay_succeeded: None,
request_initiated: None,
request_succeeded: None,
}),
},
AnalyticsEvent::RequestInitiated {
payment_hash,
entered_amount_msat,
sats_per_user_currency,
user_currency,
request_currency,
created_at,
} => report_payment_telemetry::Variables {
telemetry_id: self.analytics_id.clone(),
events: Some(PaymentTelemetryEventsInput {
pay_failed: None,
pay_initiated: None,
pay_succeeded: None,
request_initiated: Some(RequestInitiatedInput {
created_at: created_at.to_rfc3339(),
entered_amount_m_sat: entered_amount_msat.unwrap_or(0),
payment_hash,
request_currency,
sats_per_user_currency: sats_per_user_currency.unwrap_or(0) as i64,
user_currency,
}),
request_succeeded: None,
}),
},
AnalyticsEvent::RequestSucceeded {
payment_hash,
paid_amount_sat,
channel_opening_fee_msat,
received_at,
} => report_payment_telemetry::Variables {
telemetry_id: self.analytics_id.clone(),
events: Some(PaymentTelemetryEventsInput {
pay_failed: None,
pay_initiated: None,
pay_succeeded: None,
request_initiated: None,
request_succeeded: Some(RequestSucceededInput {
channel_opening_fee_m_sat: channel_opening_fee_msat,
paid_amount_m_sat: paid_amount_sat,
payment_hash,
payment_received_at: received_at.to_rfc3339(),
}),
}),
},
};

let token = self.auth.query_token()?;
let client = build_client(Some(&token))?;
post_blocking::<ReportPaymentTelemetry>(&client, &self.backend_url, variables)?;

Ok(())
}
}

fn map_payment_source(payment_source: PaymentSource) -> PaySource {
match payment_source {
PaymentSource::Camera => PaySource::CAMERA,
PaymentSource::Clipboard => PaySource::CLIPBOARD,
PaymentSource::Nfc => PaySource::NFC,
PaymentSource::Manual => PaySource::MANUAL,
}
}

fn map_pay_failure_reason(
pay_failure_reason: PayFailureReason,
) -> report_payment_telemetry::PayFailureReason {
match pay_failure_reason {
PayFailureReason::NoRoute => report_payment_telemetry::PayFailureReason::NO_ROUTE,
PayFailureReason::Unknown => report_payment_telemetry::PayFailureReason::UNKNOWN,
}
}
1 change: 1 addition & 0 deletions squirrel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ hex = "0.4.3"

graphql = { path = "../graphql" }
honey-badger = { path = "../honey-badger" }
bdk = { version = "0.29.0", features = ["keys-bip39"] }

[dev-dependencies]
bitcoin = { version = "0.29.2" }
Expand Down
2 changes: 1 addition & 1 deletion squirrel/tests/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bitcoin::Network;
use bdk::bitcoin::Network;
use graphql::perro::Error::RuntimeError;
use graphql::GraphQlRuntimeErrorCode;
use honey_badger::asynchronous::Auth;
Expand Down
Loading