Send events to Snowflake in the format they're expected by Amplitude (#20765)

This will allow us to use the events table directly in Amplitude, which
lets us use the newer event ingestion flow that detects changes to the
table. Otherwise we'll need a transformation.

I think Amplitude's API is probably a pretty good example to follow for
the raw event schema, even if we don't end up using their product. They
also recommend a "Noun Verbed" format for naming events, so I think we
should go with this. This will help us be consistent and encourage the
author of events to think more clearly about what event they're
reporting.

cc @ConradIrwin 

Release Notes:

- N/A
This commit is contained in:
Nathan Sobo 2024-11-16 09:58:19 -07:00 committed by GitHub
parent 97e9137cb7
commit f9990b42fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -15,6 +15,7 @@ use chrono::Duration;
use rpc::ExtensionMetadata; use rpc::ExtensionMetadata;
use semantic_version::SemanticVersion; use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize, Serializer};
use serde_json::json;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use telemetry_events::{ use telemetry_events::{
@ -1392,111 +1393,196 @@ fn for_snowflake(
body: EventRequestBody, body: EventRequestBody,
first_event_at: chrono::DateTime<chrono::Utc>, first_event_at: chrono::DateTime<chrono::Utc>,
) -> impl Iterator<Item = SnowflakeRow> { ) -> impl Iterator<Item = SnowflakeRow> {
body.events.into_iter().map(move |event| SnowflakeRow { body.events.into_iter().map(move |event| {
event: match &event.event { let timestamp =
Event::Editor(editor_event) => format!("editor_{}", editor_event.operation), first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
Event::InlineCompletion(inline_completion_event) => format!( let (event_type, mut event_properties) = match &event.event {
"inline_completion_{}", Event::Editor(e) => (
if inline_completion_event.suggestion_accepted { match e.operation.as_str() {
"accept " "open" => "Editor Opened".to_string(),
} else { "save" => "Editor Saved".to_string(),
"discard" _ => format!("Unknown Editor Event: {}", e.operation),
} },
serde_json::to_value(e).unwrap(),
), ),
Event::Call(call_event) => format!("call_{}", call_event.operation.replace(" ", "_")), Event::InlineCompletion(e) => (
Event::Assistant(assistant_event) => {
format!( format!(
"assistant_{}", "Inline Completion {}",
match assistant_event.phase { if e.suggestion_accepted {
telemetry_events::AssistantPhase::Response => "response", "Accepted"
telemetry_events::AssistantPhase::Invoked => "invoke", } else {
telemetry_events::AssistantPhase::Accepted => "accept", "Discarded"
telemetry_events::AssistantPhase::Rejected => "reject",
} }
) ),
serde_json::to_value(e).unwrap(),
),
Event::Call(e) => {
let event_type = match e.operation.trim() {
"unshare project" => "Project Unshared".to_string(),
"open channel notes" => "Channel Notes Opened".to_string(),
"share project" => "Project Shared".to_string(),
"join channel" => "Channel Joined".to_string(),
"hang up" => "Call Ended".to_string(),
"accept incoming" => "Incoming Call Accepted".to_string(),
"invite" => "Participant Invited".to_string(),
"disable microphone" => "Microphone Disabled".to_string(),
"enable microphone" => "Microphone Enabled".to_string(),
"enable screen share" => "Screen Share Enabled".to_string(),
"disable screen share" => "Screen Share Disabled".to_string(),
"decline incoming" => "Incoming Call Declined".to_string(),
"enable camera" => "Camera Enabled".to_string(),
"disable camera" => "Camera Disabled".to_string(),
_ => format!("Unknown Call Event: {}", e.operation),
};
(event_type, serde_json::to_value(e).unwrap())
} }
Event::Cpu(_) => "system_cpu".to_string(), Event::Assistant(e) => (
Event::Memory(_) => "system_memory".to_string(), match e.phase {
Event::App(app_event) => app_event.operation.replace(" ", "_"), telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
Event::Setting(_) => "setting_change".to_string(), telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
Event::Extension(_) => "extension_load".to_string(), telemetry_events::AssistantPhase::Accepted => {
Event::Edit(_) => "edit".to_string(), "Assistant Response Accepted".to_string()
Event::Action(_) => "command_palette_action".to_string(), }
Event::Repl(_) => "repl".to_string(), telemetry_events::AssistantPhase::Rejected => {
}, "Assistant Response Rejected".to_string()
system_id: body.system_id.clone(), }
timestamp: first_event_at + Duration::milliseconds(event.milliseconds_since_first_event), },
data: SnowflakeData { serde_json::to_value(e).unwrap(),
installation_id: body.installation_id.clone(), ),
session_id: body.session_id.clone(), Event::Cpu(e) => (
metrics_id: body.metrics_id.clone(), "System CPU Sampled".to_string(),
is_staff: body.is_staff, serde_json::to_value(e).unwrap(),
app_version: body.app_version.clone(), ),
os_name: body.os_name.clone(), Event::Memory(e) => (
os_version: body.os_version.clone(), "System Memory Sampled".to_string(),
architecture: body.architecture.clone(), serde_json::to_value(e).unwrap(),
release_channel: body.release_channel.clone(), ),
signed_in: event.signed_in, Event::App(e) => {
editor_event: match &event.event { let mut properties = json!({});
Event::Editor(editor_event) => Some(editor_event.clone()), let event_type = match e.operation.trim() {
_ => None, "extensions: install extension" => "Extension Installed".to_string(),
}, "open" => "App Opened".to_string(),
inline_completion_event: match &event.event { "project search: open" => "Project Search Opened".to_string(),
Event::InlineCompletion(inline_completion_event) => { "first open" => {
Some(inline_completion_event.clone()) properties["is_first_open"] = json!(true);
} "App First Opened".to_string()
_ => None, }
}, "extensions: uninstall extension" => "Extension Uninstalled".to_string(),
call_event: match &event.event { "welcome page: close" => "Welcome Page Closed".to_string(),
Event::Call(call_event) => Some(call_event.clone()), "open project" => {
_ => None, properties["is_first_time"] = json!(false);
}, "Project Opened".to_string()
assistant_event: match &event.event { }
Event::Assistant(assistant_event) => Some(assistant_event.clone()), "welcome page: install cli" => "CLI Installed".to_string(),
_ => None, "project diagnostics: open" => "Project Diagnostics Opened".to_string(),
}, "extensions page: open" => "Extensions Page Opened".to_string(),
cpu_event: match &event.event { "welcome page: change theme" => "Welcome Theme Changed".to_string(),
Event::Cpu(cpu_event) => Some(cpu_event.clone()), "welcome page: toggle metric telemetry" => {
_ => None, properties["enabled"] = json!(false);
}, "Welcome Telemetry Toggled".to_string()
memory_event: match &event.event { }
Event::Memory(memory_event) => Some(memory_event.clone()), "welcome page: change keymap" => "Keymap Changed".to_string(),
_ => None, "welcome page: toggle vim" => {
}, properties["enabled"] = json!(false);
app_event: match &event.event { "Welcome Vim Mode Toggled".to_string()
Event::App(app_event) => Some(app_event.clone()), }
_ => None, "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
}, "welcome page: toggle diagnostic telemetry" => {
setting_event: match &event.event { "Welcome Telemetry Toggled".to_string()
Event::Setting(setting_event) => Some(setting_event.clone()), }
_ => None, "welcome page: open" => "Welcome Page Opened".to_string(),
}, "close" => "App Closed".to_string(),
extension_event: match &event.event { "markdown preview: open" => "Markdown Preview Opened".to_string(),
Event::Extension(extension_event) => Some(extension_event.clone()), "welcome page: open extensions" => "Extensions Page Opened".to_string(),
_ => None, "open node project" | "open pnpm project" | "open yarn project" => {
}, properties["project_type"] = json!("node");
edit_event: match &event.event { properties["is_first_time"] = json!(false);
Event::Edit(edit_event) => Some(edit_event.clone()), "Project Opened".to_string()
_ => None, }
}, "repl sessions: open" => "REPL Session Started".to_string(),
repl_event: match &event.event { "welcome page: toggle helix" => {
Event::Repl(repl_event) => Some(repl_event.clone()), properties["enabled"] = json!(false);
_ => None, "Helix Mode Toggled".to_string()
}, }
action_event: match event.event { "welcome page: edit settings" => {
Event::Action(action_event) => Some(action_event.clone()), properties["changed_settings"] = json!([]);
_ => None, "Settings Edited".to_string()
}, }
}, "welcome page: view docs" => "Documentation Viewed".to_string(),
"open ssh project" => {
properties["is_first_time"] = json!(false);
"SSH Project Opened".to_string()
}
"create ssh server" => "SSH Server Created".to_string(),
"create ssh project" => "SSH Project Created".to_string(),
"first open for release channel" => {
properties["is_first_for_channel"] = json!(true);
"App First Opened For Release Channel".to_string()
}
_ => format!("Unknown App Event: {}", e.operation),
};
(event_type, properties)
}
Event::Setting(e) => (
"Settings Changed".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Extension(e) => (
"Extension Loaded".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Edit(e) => (
"Editor Edited".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Action(e) => (
"Action Invoked".to_string(),
serde_json::to_value(e).unwrap(),
),
Event::Repl(e) => (
"Kernel Status Changed".to_string(),
serde_json::to_value(e).unwrap(),
),
};
if let serde_json::Value::Object(ref mut map) = event_properties {
map.insert("app_version".to_string(), body.app_version.clone().into());
map.insert("os_name".to_string(), body.os_name.clone().into());
map.insert("os_version".to_string(), body.os_version.clone().into());
map.insert("architecture".to_string(), body.architecture.clone().into());
map.insert(
"release_channel".to_string(),
body.release_channel.clone().into(),
);
map.insert("signed_in".to_string(), event.signed_in.into());
}
let user_properties = Some(serde_json::json!({
"is_staff": body.is_staff,
}));
SnowflakeRow {
time: timestamp,
user_id: body.metrics_id.clone(),
device_id: body.system_id.clone(),
event_type,
event_properties,
user_properties,
insert_id: Some(Uuid::new_v4().to_string()),
}
}) })
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct SnowflakeRow { struct SnowflakeRow {
pub event: String, pub time: chrono::DateTime<chrono::Utc>,
pub system_id: Option<String>, pub user_id: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>, pub device_id: Option<String>,
pub data: SnowflakeData, pub event_type: String,
pub event_properties: serde_json::Value,
pub user_properties: Option<serde_json::Value>,
pub insert_id: Option<String>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]