diff --git a/.gitignore b/.gitignore index b04240c279..fd587a0946 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ target # Editors /.idea/ .DS_Store + +# NPM utilities for building docs +/node_modules/ +package-lock.json diff --git a/Cargo.lock b/Cargo.lock index a2f58b582c..e8552d2088 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,6 +958,15 @@ dependencies = [ "backtrace", ] +[[package]] +name = "error-chain" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d371106cc88ffdfb1eabd7111e432da544f16f3e2d7bf1dfe8bf575f1df045cd" +dependencies = [ + "version_check 0.9.2", +] + [[package]] name = "failure" version = "0.1.8" @@ -1501,7 +1510,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f7eadeaf4b52700de180d147c4805f199854600b36faa963d91114827b2ffc" dependencies = [ - "error-chain", + "error-chain 0.8.1", "socket2", "widestring", "winapi 0.3.8", @@ -1555,6 +1564,16 @@ name = "json-forensics" version = "0.1.0" source = "git+https://github.com/getsentry/rust-json-forensics#3896ab98bae363570b7fc0e0af353f287ab17282" +[[package]] +name = "jsonway" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "effcb749443c905fbaef49d214f8b1049c240e0adb7af9baa0e201e625e4f9de" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -2153,6 +2172,35 @@ dependencies = [ "sha-1", ] +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared", + "rand 0.7.3", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -2273,6 +2321,19 @@ dependencies = [ "unicode-xid 0.2.1", ] +[[package]] +name = "publicsuffix" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" +dependencies = [ + "error-chain 0.12.2", + "idna 0.2.0", + "lazy_static", + "regex", + "url 2.1.1", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2341,7 +2402,7 @@ dependencies = [ "rand_isaac", "rand_jitter", "rand_os", - "rand_pcg", + "rand_pcg 0.1.2", "rand_xorshift", "winapi 0.3.8", ] @@ -2357,6 +2418,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc 0.2.0", + "rand_pcg 0.2.1", ] [[package]] @@ -2465,6 +2527,15 @@ dependencies = [ "rand_core 0.4.2", ] +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_xorshift" version = "0.1.1" @@ -2629,6 +2700,7 @@ dependencies = [ "relay-config", "relay-general", "relay-server", + "schemars", "sentry", "serde", "serde_json", @@ -2664,6 +2736,7 @@ dependencies = [ "lru", "parking_lot 0.10.2", "regex", + "schemars", "sentry-types", "serde", ] @@ -2725,6 +2798,7 @@ dependencies = [ "regex", "relay-common", "relay-general-derive", + "schemars", "serde", "serde_json", "serde_urlencoded 0.5.5", @@ -2734,6 +2808,7 @@ dependencies = [ "uaparser", "url 2.1.1", "uuid 0.8.1", + "valico", ] [[package]] @@ -2952,6 +3027,31 @@ dependencies = [ "parking_lot 0.10.2", ] +[[package]] +name = "schemars" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be77ed66abed6954aabf6a3e31a84706bedbf93750d267e92ef4a6d90bbd6a61" +dependencies = [ + "chrono", + "schemars_derive", + "serde", + "serde_json", + "uuid 0.8.1", +] + +[[package]] +name = "schemars_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11af7a475c9ee266cfaa9e303a47c830ebe072bf3101ab907a7b7b9d816fa01d" +dependencies = [ + "proc-macro2 1.0.18", + "quote 1.0.7", + "serde_derive_internals", + "syn 1.0.33", +] + [[package]] name = "scoped-tls" version = "0.1.2" @@ -3121,6 +3221,17 @@ dependencies = [ "syn 1.0.33", ] +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2 1.0.18", + "quote 1.0.7", + "syn 1.0.33", +] + [[package]] name = "serde_json" version = "1.0.55" @@ -4064,6 +4175,25 @@ dependencies = [ "v_escape", ] +[[package]] +name = "valico" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e06847fab030aa7355f219287e04fe6e090343aa6de55724704b38ba956abb6" +dependencies = [ + "chrono", + "jsonway", + "percent-encoding 2.1.0", + "phf", + "phf_codegen", + "publicsuffix", + "regex", + "serde", + "serde_json", + "url 2.1.1", + "uuid 0.8.1", +] + [[package]] name = "vcpkg" version = "0.2.10" diff --git a/Cargo.toml b/Cargo.toml index 4652f4a14c..5c91adf876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ exclude = [ default = ["ssl"] ssl = ["relay-server/ssl"] processing = ["relay-server/processing"] +jsonschema = ["relay-general/jsonschema"] [profile.release] debug = true @@ -46,6 +47,7 @@ relay-config = { path = "relay-config" } relay-server = { path = "relay-server" } relay-general = { path = "relay-general" } sentry = { version = "0.18.0", features = ["with_debug_meta"] } +schemars = { version = "0.7.1", features = ["uuid", "chrono"] } serde = "1.0.114" serde_json = "1.0.55" hostname = "0.3.1" diff --git a/Makefile b/Makefile index 5d31222789..10afa1fe43 100644 --- a/Makefile +++ b/Makefile @@ -91,22 +91,35 @@ api-docs: setup-git @cargo doc .PHONY: api-docs -prose-docs: .venv/bin/python extract-doc +prose-docs: .venv/bin/python extract-metric-docs extract-jsonschema-docs .venv/bin/mkdocs build touch site/.nojekyll .PHONY: prose-docs -extract-doc: .venv/bin/python +extract-metric-docs: .venv/bin/python .venv/bin/pip install -U -r requirements-doc.txt cd scripts && ../.venv/bin/python extract_metric_docs.py +extract-jsonschema-docs: install-jsonschema-docs + rm -rf docs/event-schema/event.schema.* + set -e && cargo run --features jsonschema -- event-json-schema \ + > docs/event-schema/event.schema.json + set -e && ./node_modules/.bin/quicktype-markdown \ + Event docs/event-schema/event.schema.json \ + > docs/event-schema/event.schema.md + +install-jsonschema-docs: + npm install git+https://github.com/untitaker/quicktype-markdown + docserver: prose-docs .venv/bin/mkdocs serve .PHONY: docserver travis-upload-prose-docs: prose-docs cd site && zip -r gh-pages . - zeus upload -t "application/zip+docs" site/gh-pages.zip \ + set -e && zeus upload -t "application/zip+docs" site/gh-pages.zip \ + || [[ ! "$(TRAVIS_BRANCH)" =~ ^release/ ]] + set -e && zeus upload -t "application/octet-stream" -n event.schema.json docs/event-schema/event.schema.json \ || [[ ! "$(TRAVIS_BRANCH)" =~ ^release/ ]] .PHONY: travis-upload-docs diff --git a/docs/.gitignore b/docs/.gitignore index 505366e8d0..c98657fcc0 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1 +1,2 @@ +event-schema/event.schema.* configuration/metrics.md diff --git a/docs/event-schema/index.md b/docs/event-schema/index.md new file mode 100644 index 0000000000..f6ea3996a1 --- /dev/null +++ b/docs/event-schema/index.md @@ -0,0 +1,14 @@ +# Event schema + +This page is intended to eventually replace [our current event schema documentation]. As opposed to the current one, this one is automatically generated from source code and therefore more likely to be up-to-date and exhaustive. The plan is to eventually make this document the source of truth, i.e. move it into `develop.sentry.dev`. + +**It is still a work-in-progress.** Right now we recommend using our existing docs as linked above and only fall back to this doc to resolve ambiguities. + +In addition to documentation the event schema is documented in machine-readable form: + +- [Download JSON schema](event.schema.json) (which is what this document is generated from) + +{% include "event-schema/event.schema.md" %} + + +[our current event schema documentation]: https://develop.sentry.dev/sdk/event-payloads/ diff --git a/mkdocs.yml b/mkdocs.yml index 63bf1247cf..326e99359c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,6 +25,8 @@ nav: - architecture/ingest-event-path.md - architecture/project-configuration.md + - "": event-schema/index.md + markdown_extensions: - admonition: {} - tables: {} @@ -37,6 +39,15 @@ markdown_extensions: class: mermaid format: !!python/name:pymdownx.superfences.fence_div_format +plugins: + - search + # Used in event schema docs + - macros + + - exclude: + glob: + - event-schema/event.schema.md + extra_css: # Config for diagrams - https://unpkg.com/mermaid@7.1.2/dist/mermaid.css diff --git a/relay-cabi/Cargo.lock b/relay-cabi/Cargo.lock index 6bebc7d65e..0031d1fc56 100644 --- a/relay-cabi/Cargo.lock +++ b/relay-cabi/Cargo.lock @@ -1014,6 +1014,7 @@ dependencies = [ "chrono", "cookie", "debugid", + "derive_more", "dynfmt", "failure", "hmac", @@ -1027,6 +1028,7 @@ dependencies = [ "regex", "relay-common", "relay-general-derive", + "schemars", "serde", "serde_json", "serde_urlencoded", @@ -1069,6 +1071,31 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" +[[package]] +name = "schemars" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be77ed66abed6954aabf6a3e31a84706bedbf93750d267e92ef4a6d90bbd6a61" +dependencies = [ + "chrono", + "schemars_derive", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11af7a475c9ee266cfaa9e303a47c830ebe072bf3101ab907a7b7b9d816fa01d" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "serde_derive_internals", + "syn 1.0.17", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1136,6 +1163,17 @@ dependencies = [ "syn 1.0.33", ] +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + [[package]] name = "serde_json" version = "1.0.55" diff --git a/relay-common/Cargo.toml b/relay-common/Cargo.toml index ece80df365..69245c723b 100644 --- a/relay-common/Cargo.toml +++ b/relay-common/Cargo.toml @@ -22,7 +22,9 @@ lru = "0.4.0" parking_lot = "0.10.0" regex = "1.3.9" sentry-types = "0.14.1" +schemars = { version = "0.7.1", features = ["uuid", "chrono"], optional = true } serde = { version = "1.0.114", features = ["derive"] } [features] +jsonschema = ["schemars"] default = [] diff --git a/relay-common/src/constants.rs b/relay-common/src/constants.rs index e8500cec9d..a1556d3d9d 100644 --- a/relay-common/src/constants.rs +++ b/relay-common/src/constants.rs @@ -4,10 +4,13 @@ use std::fmt; use std::str::FromStr; use failure::Fail; +#[cfg(feature = "jsonschema")] +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// The type of an event. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[serde(rename_all = "lowercase")] pub enum EventType { /// Events that carry an exception payload. @@ -152,7 +155,9 @@ impl From for DataCategory { // // Note: This type is represented as a u8 in Snuba/Clickhouse, with Unknown being the default // value. We use repr(u8) to statically validate that the trace status has 255 variants at most. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] +#[serde(rename_all = "snake_case")] #[repr(u8)] // size limit in clickhouse pub enum SpanStatus { /// The operation completed successfully. diff --git a/relay-general/Cargo.toml b/relay-general/Cargo.toml index bbb6980682..0c82076f9d 100644 --- a/relay-general/Cargo.toml +++ b/relay-general/Cargo.toml @@ -35,14 +35,17 @@ uaparser = { version = "0.3.3", optional = true } url = "2.1.1" uuid = { version = "0.8.1", features = ["v4", "serde"] } relay-common = { path = "../relay-common" } +schemars = { version = "0.7.1", features = ["uuid", "chrono"], optional = true } [dev-dependencies] difference = "2.0.0" insta = { version = "0.15.0", features = ["ron", "redactions"] } criterion = "0.3" +valico = "3.2.0" [features] mmap = ["maxminddb/mmap", "memmap"] +jsonschema = ["relay-common/jsonschema", "schemars"] default = ["uaparser", "mmap"] [[bench]] diff --git a/relay-general/derive/src/jsonschema.rs b/relay-general/derive/src/jsonschema.rs new file mode 100644 index 0000000000..fac5158f28 --- /dev/null +++ b/relay-general/derive/src/jsonschema.rs @@ -0,0 +1,83 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::Visibility; + +use crate::parse_field_attributes; + +pub fn derive_jsonschema(mut s: synstructure::Structure<'_>) -> TokenStream { + let _ = s.add_bounds(synstructure::AddBounds::Generics); + + let mut arms = quote!(); + + for variant in s.variants() { + let mut fields = quote!(); + + let mut is_tuple_struct = false; + + for (index, bi) in variant.bindings().iter().enumerate() { + let field_attrs = parse_field_attributes(index, &bi.ast(), &mut is_tuple_struct); + let name = field_attrs.field_name; + + fields = quote!(#fields #[schemars(rename = #name)]); + + if !field_attrs.required.unwrap_or(false) { + fields = quote!(#fields #[schemars(default = "__schemars_null")]); + } + + if field_attrs.additional_properties || field_attrs.omit_from_schema { + fields = quote!(#fields #[schemars(skip)]); + } + + let mut ast = bi.ast().clone(); + ast.vis = Visibility::Inherited; + ast.attrs.retain(|attr| { + attr.parse_meta() + .ok() + .and_then(|meta| Some(meta.path().get_ident()? == "doc")) + .unwrap_or(false) + }); + fields = quote!(#fields #ast,); + } + + let ident = variant.ast().ident; + + let arm = if is_tuple_struct { + quote!( #ident( #fields ) ) + } else { + quote!( #ident { #fields } ) + }; + + arms = quote! { + #arms + #arm, + }; + } + + let ident = &s.ast().ident; + + s.gen_impl(quote! { + // Massive hack to tell schemars that fields are nullable. Causes it to emit {"default": + // null} even though Option<()> is not a valid instance of T. + fn __schemars_null() -> Option<()> { + None + } + + #[automatically_derived] + gen impl schemars::JsonSchema for @Self { + fn schema_name() -> String { + stringify!(#ident).to_owned() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + #[derive(schemars::JsonSchema)] + #[cfg_attr(feature = "jsonschema", schemars(untagged))] + #[cfg_attr(feature = "jsonschema", schemars(deny_unknown_fields))] + enum Helper { + #arms + } + + Helper::json_schema(gen) + } + } + }) +} diff --git a/relay-general/derive/src/lib.rs b/relay-general/derive/src/lib.rs index a1619ea1cf..1c9306fd65 100644 --- a/relay-general/derive/src/lib.rs +++ b/relay-general/derive/src/lib.rs @@ -3,6 +3,7 @@ #![deny(unused_must_use)] mod empty; +mod jsonschema; mod process; use std::str::FromStr; @@ -22,6 +23,7 @@ decl_derive!([Empty, attributes(metastructure)] => empty::derive_empty); decl_derive!([ToValue, attributes(metastructure)] => derive_to_value); decl_derive!([FromValue, attributes(metastructure)] => derive_from_value); decl_derive!([ProcessValue, attributes(metastructure)] => process::derive_process_value); +decl_derive!([JsonSchema, attributes(metastructure)] => jsonschema::derive_jsonschema); fn derive_to_value(s: synstructure::Structure<'_>) -> TokenStream { derive_metastructure(s, Trait::To) @@ -652,6 +654,7 @@ impl Pii { #[derive(Default)] struct FieldAttrs { additional_properties: bool, + omit_from_schema: bool, field_name: String, required: Option, nonempty: Option, @@ -835,6 +838,8 @@ fn parse_field_attributes( if ident == "additional_properties" { rv.additional_properties = true; + } else if ident == "omit_from_schema" { + rv.omit_from_schema = true; } else { panic!("Unknown attribute {}", ident); } diff --git a/relay-general/src/processor/impls.rs b/relay-general/src/processor/impls.rs index 2ca36999bc..16ae7b2740 100644 --- a/relay-general/src/processor/impls.rs +++ b/relay-general/src/processor/impls.rs @@ -1,4 +1,3 @@ -use chrono::{DateTime, Utc}; use uuid::Uuid; use crate::processor::{process_value, ProcessValue, ProcessingState, Processor, ValueType}; @@ -104,26 +103,6 @@ impl ProcessValue for f64 { } } -impl ProcessValue for DateTime { - #[inline] - fn value_type(&self) -> Option { - Some(ValueType::DateTime) - } - - #[inline] - fn process_value

( - &mut self, - meta: &mut Meta, - processor: &mut P, - state: &ProcessingState<'_>, - ) -> ProcessingResult - where - P: Processor, - { - processor.process_timestamp(self, meta, state) - } -} - impl ProcessValue for Uuid {} impl ProcessValue for Array diff --git a/relay-general/src/processor/traits.rs b/relay-general/src/processor/traits.rs index 6e147a387e..3da64e122c 100644 --- a/relay-general/src/processor/traits.rs +++ b/relay-general/src/processor/traits.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use crate::processor::{process_value, ProcessingState, ValueType}; -use crate::types::{FromValue, Meta, ProcessingResult, Timestamp, ToValue}; +use crate::types::{FromValue, Meta, ProcessingResult, ToValue}; macro_rules! process_method { ($name: ident, $ty:ident $(::$path:ident)*) => { @@ -56,7 +56,6 @@ pub trait Processor: Sized { process_method!(process_i64, i64); process_method!(process_f64, f64); process_method!(process_bool, bool); - process_method!(process_timestamp, Timestamp); process_method!(process_value, crate::types::Value); process_method!(process_array, crate::types::Array); @@ -68,6 +67,7 @@ pub trait Processor: Sized { T: crate::protocol::AsPair ); process_method!(process_values, crate::protocol::Values); + process_method!(process_timestamp, crate::protocol::Timestamp); process_method!(process_event, crate::protocol::Event); process_method!(process_exception, crate::protocol::Exception); diff --git a/relay-general/src/protocol/breadcrumb.rs b/relay-general/src/protocol/breadcrumb.rs index f792e0a6f7..45b6b026b7 100644 --- a/relay-general/src/protocol/breadcrumb.rs +++ b/relay-general/src/protocol/breadcrumb.rs @@ -1,17 +1,16 @@ #[cfg(test)] -use chrono::TimeZone; +use chrono::{TimeZone, Utc}; -use chrono::{DateTime, Utc}; - -use crate::protocol::Level; +use crate::protocol::{Level, Timestamp}; use crate::types::{Annotated, Object, Value}; /// A breadcrumb. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_breadcrumb", value_type = "Breadcrumb")] pub struct Breadcrumb { /// The timestamp of the breadcrumb. - pub timestamp: Annotated>, + pub timestamp: Annotated, /// The type of the breadcrumb. #[metastructure(field = "type", max_chars = "enumlike")] @@ -67,7 +66,7 @@ fn test_breadcrumb_roundtrip() { }"#; let breadcrumb = Annotated::new(Breadcrumb { - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), ty: Annotated::new("mytype".to_string()), category: Annotated::new("mycategory".to_string()), level: Annotated::new(Level::Fatal), @@ -100,7 +99,7 @@ fn test_breadcrumb_default_values() { let output = r#"{"timestamp":946684800.0}"#; let breadcrumb = Annotated::new(Breadcrumb { - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), ..Default::default() }); diff --git a/relay-general/src/protocol/clientsdk.rs b/relay-general/src/protocol/clientsdk.rs index ed3fa27df4..9b589b7027 100644 --- a/relay-general/src/protocol/clientsdk.rs +++ b/relay-general/src/protocol/clientsdk.rs @@ -3,6 +3,7 @@ use crate::types::{Annotated, Array, Object, Value}; /// An installed and loaded package as part of the Sentry SDK. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct ClientSdkPackage { /// Name of the package. pub name: Annotated, @@ -12,6 +13,7 @@ pub struct ClientSdkPackage { /// Information about the Sentry SDK. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_client_sdk_info", value_type = "ClientSdkInfo")] pub struct ClientSdkInfo { /// Unique SDK name. diff --git a/relay-general/src/protocol/contexts.rs b/relay-general/src/protocol/contexts.rs index 6bb1f739af..c634c68c21 100644 --- a/relay-general/src/protocol/contexts.rs +++ b/relay-general/src/protocol/contexts.rs @@ -7,6 +7,7 @@ use crate::types::{Annotated, Empty, Error, FromValue, Object, SkipSerialization /// Device information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct DeviceContext { /// Name of the device. #[metastructure(pii = "maybe")] @@ -110,6 +111,7 @@ impl DeviceContext { /// Operating system information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct OsContext { /// Name of the operating system. pub name: Annotated, @@ -146,6 +148,7 @@ impl OsContext { /// Runtime information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct RuntimeContext { /// Runtime name. pub name: Annotated, @@ -175,6 +178,7 @@ impl RuntimeContext { /// Application information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct AppContext { /// Start time of the app. #[metastructure(pii = "maybe")] @@ -213,6 +217,7 @@ impl AppContext { /// Web browser information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct BrowserContext { /// Runtime name. pub name: Annotated, @@ -243,6 +248,7 @@ lazy_static::lazy_static! { /// GPU information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct GpuContext(#[metastructure(pii = "maybe")] pub Object); impl From> for GpuContext { @@ -274,6 +280,7 @@ impl GpuContext { /// Monitor information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct MonitorContext(#[metastructure(pii = "maybe")] pub Object); impl From> for MonitorContext { @@ -303,7 +310,9 @@ impl MonitorContext { } } +/// A 32-character hex string as described in the W3C trace context spec. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct TraceId(pub String); impl FromValue for TraceId { @@ -328,7 +337,9 @@ impl FromValue for TraceId { } } +/// A 16-character hex string as described in the W3C trace context spec. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct SpanId(pub String); impl FromValue for SpanId { @@ -355,11 +366,14 @@ impl FromValue for SpanId { /// Trace context #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct TraceContext { /// The trace ID. + #[metastructure(required = "true")] pub trace_id: Annotated, /// The ID of the span. + #[metastructure(required = "true")] pub span_id: Annotated, /// The ID of the span enclosing this span. @@ -371,6 +385,7 @@ pub struct TraceContext { /// Whether the trace failed or succeeded. Currently only used to indicate status of individual /// transactions. + #[metastructure(required = "true")] pub status: Annotated, /// Additional arbitrary fields for forwards compatibility. @@ -461,6 +476,7 @@ impl TraceContext { /// A context describes environment info (e.g. device, os or browser). #[derive(Clone, Debug, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_context")] pub enum Context { /// Device information. @@ -504,6 +520,7 @@ impl Context { } #[derive(Clone, Debug, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct ContextInner(#[metastructure(bag_size = "large")] pub Context); impl std::ops::Deref for ContextInner { @@ -528,6 +545,7 @@ impl From for ContextInner { /// An object holding multiple contexts. #[derive(Clone, Debug, PartialEq, Empty, ToValue, ProcessValue, Default)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Contexts(pub Object); impl Contexts { diff --git a/relay-general/src/protocol/debugmeta.rs b/relay-general/src/protocol/debugmeta.rs index 31630a4545..ce36d11b32 100644 --- a/relay-general/src/protocol/debugmeta.rs +++ b/relay-general/src/protocol/debugmeta.rs @@ -1,4 +1,13 @@ -use debugid::{CodeId, DebugId}; +use std::fmt; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +#[cfg(feature = "jsonschema")] +use schemars::gen::SchemaGenerator; +#[cfg(feature = "jsonschema")] +use schemars::schema::Schema; + +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::processor::{ProcessValue, ProcessingState, Processor, ValueType}; @@ -13,6 +22,7 @@ use crate::types::{ /// /// Those strings get special treatment in our PII processor to avoid stripping the basename. #[derive(Debug, FromValue, ToValue, Empty, Clone, PartialEq)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct NativeImagePath(pub String); impl NativeImagePath { @@ -63,6 +73,7 @@ impl ProcessValue for NativeImagePath { /// This is relevant for iOS and other platforms that have a system /// SDK. Not to be confused with the client SDK. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct SystemSdkInfo { /// The internal name of the SDK. pub sdk_name: Annotated, @@ -83,6 +94,7 @@ pub struct SystemSdkInfo { /// Apple debug image in #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct AppleDebugImage { /// Path and name of the debug image (required). #[metastructure(required = "true")] @@ -118,7 +130,18 @@ pub struct AppleDebugImage { } macro_rules! impl_traits { - ($type:ty, $expectation:literal) => { + ($type:ident, $inner:path, $expectation:literal) => { + #[cfg(feature = "jsonschema")] + impl schemars::JsonSchema for $type { + fn schema_name() -> String { + stringify!($type).to_owned() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + String::json_schema(gen) + } + } + impl Empty for $type { #[inline] fn is_empty(&self) -> bool { @@ -165,14 +188,67 @@ macro_rules! impl_traits { } impl ProcessValue for $type {} + + impl fmt::Display for $type { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } + } + + impl FromStr for $type { + type Err = <$inner as FromStr>::Err; + + fn from_str(s: &str) -> Result { + FromStr::from_str(s).map($type) + } + } + + impl Deref for $type { + type Target = $inner; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for $type { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } }; } -impl_traits!(CodeId, "a code identifier"); -impl_traits!(DebugId, "a debug identifier"); +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct DebugId(pub debugid::DebugId); + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct CodeId(pub debugid::CodeId); + +impl_traits!(CodeId, debugid::CodeId, "a code identifier"); +impl_traits!(DebugId, debugid::DebugId, "a debug identifier"); + +impl From for DebugId +where + debugid::DebugId: From, +{ + fn from(t: T) -> Self { + DebugId(t.into()) + } +} + +impl From for CodeId +where + debugid::CodeId: From, +{ + fn from(t: T) -> Self { + CodeId(t.into()) + } +} /// A native platform debug information file. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct NativeDebugImage { /// Optional identifier of the code file. /// @@ -213,6 +289,7 @@ pub struct NativeDebugImage { /// Proguard mapping file. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct ProguardDebugImage { /// UUID computed from the file contents. #[metastructure(required = "true")] @@ -225,6 +302,7 @@ pub struct ProguardDebugImage { /// A debug information file (debug image). #[derive(Clone, Debug, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_debug_image")] pub enum DebugImage { /// Legacy apple debug images (MachO). @@ -248,6 +326,7 @@ pub enum DebugImage { /// Debugging and processing meta information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_debug_meta")] pub struct DebugMeta { /// Information about the system SDK (e.g. iOS SDK). diff --git a/relay-general/src/protocol/event.rs b/relay-general/src/protocol/event.rs index 6c53c0f68f..af183f5d62 100644 --- a/relay-general/src/protocol/event.rs +++ b/relay-general/src/protocol/event.rs @@ -1,17 +1,18 @@ use std::fmt; use std::str::FromStr; -use chrono::{DateTime, Utc}; -use serde::{Serialize, Serializer}; +#[cfg(feature = "jsonschema")] +use schemars::gen::SchemaGenerator; +#[cfg(feature = "jsonschema")] +use schemars::schema::Schema; -#[cfg(test)] -use chrono::TimeZone; +use serde::{Serialize, Serializer}; use crate::processor::ProcessValue; use crate::protocol::{ Breadcrumb, ClientSdkInfo, Contexts, Csp, DebugMeta, Exception, ExpectCt, ExpectStaple, Fingerprint, Hpkp, LenientString, Level, LogEntry, Metrics, Request, Span, Stacktrace, Tags, - TemplateInfo, Thread, User, Values, + TemplateInfo, Thread, Timestamp, User, Values, }; use crate::types::{ Annotated, Array, Empty, ErrorKind, FromValue, Object, SkipSerialization, ToValue, Value, @@ -19,6 +20,7 @@ use crate::types::{ /// Wrapper around a UUID with slightly different formatting. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct EventId(pub uuid::Uuid); impl EventId { @@ -110,6 +112,21 @@ impl ProcessValue for EventType {} #[derive(Debug, FromValue, ToValue, ProcessValue, Empty, Clone, PartialEq)] pub struct ExtraValue(#[metastructure(bag_size = "larger")] pub Value); +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for ExtraValue { + fn schema_name() -> String { + Value::schema_name() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + Value::json_schema(gen) + } + + fn is_referenceable() -> bool { + false + } +} + impl> From for ExtraValue { fn from(value: T) -> ExtraValue { ExtraValue(value.into()) @@ -118,9 +135,10 @@ impl> From for ExtraValue { /// An event processing error. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct EventProcessingError { - #[metastructure(field = "type", required = "true")] /// The error kind. + #[metastructure(field = "type", required = "true")] pub ty: Annotated, /// Affected key or deep path. @@ -149,6 +167,7 @@ pub struct GroupingConfig { /// The sentry v7 event structure. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_event", value_type = "Event")] pub struct Event { /// Unique identifier of this event. @@ -200,13 +219,13 @@ pub struct Event { pub platform: Annotated, /// Timestamp when the event was created. - pub timestamp: Annotated>, + pub timestamp: Annotated, /// Timestamp when the event has started (relevant for event type = "transaction") - pub start_timestamp: Annotated>, + pub start_timestamp: Annotated, /// Timestamp when the event has been received by Sentry. - pub received: Annotated>, + pub received: Annotated, /// Server or device name the event was generated on. #[metastructure(pii = "true", max_chars = "symbol")] @@ -270,13 +289,18 @@ pub struct Event { #[metastructure(skip_serialization = "empty")] pub exceptions: Annotated>, - /// Deprecated event stacktrace. + /// Event stacktrace. + /// + /// DEPRECATED: Prefer `threads` or `exception` depending on which is more appropriate. #[metastructure(skip_serialization = "empty")] #[metastructure(legacy_alias = "sentry.interfaces.Stacktrace")] pub stacktrace: Annotated, /// Simplified template error location information. + /// DEPRECATED: Non-Raven clients are not supposed to send this anymore, but rather just report + /// synthetic frames. #[metastructure(legacy_alias = "sentry.interfaces.Template")] + #[metastructure(omit_from_schema)] pub template: Annotated, /// Threads that were active when the event occurred. @@ -313,26 +337,32 @@ pub struct Event { pub project: Annotated, /// The grouping configuration for this event. + #[metastructure(omit_from_schema)] pub grouping_config: Annotated>, /// Legacy checksum used for grouping before fingerprint hashes. #[metastructure(max_chars = "hash")] + #[metastructure(omit_from_schema)] pub checksum: Annotated, /// CSP (security) reports. #[metastructure(legacy_alias = "sentry.interfaces.Csp")] + #[metastructure(omit_from_schema)] pub csp: Annotated, /// HPKP (security) reports. #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.Hpkp")] + #[metastructure(omit_from_schema)] pub hpkp: Annotated, /// ExpectCT (security) reports. #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectCT")] + #[metastructure(omit_from_schema)] pub expectct: Annotated, /// ExpectStaple (security) reports. #[metastructure(pii = "true", legacy_alias = "sentry.interfaces.ExpectStaple")] + #[metastructure(omit_from_schema)] pub expectstaple: Annotated, /// Spans for tracing. @@ -341,6 +371,7 @@ pub struct Event { /// Internal ingestion and processing metrics. /// /// This value should not be ingested and will be overwritten by the store normalizer. + #[metastructure(omit_from_schema)] pub _metrics: Annotated, /// Additional arbitrary fields for forwards compatibility. @@ -350,6 +381,8 @@ pub struct Event { #[test] fn test_event_roundtrip() { + use chrono::{TimeZone, Utc}; + use crate::protocol::TagEntry; use crate::types::{Map, Meta}; @@ -416,7 +449,7 @@ fn test_event_roundtrip() { Annotated::new(map) }, platform: Annotated::new("myplatform".to_string()), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), server_name: Annotated::new("myhost".to_string()), release: Annotated::new("myrelease".to_string().into()), dist: Annotated::new("mydist".to_string()), diff --git a/relay-general/src/protocol/exception.rs b/relay-general/src/protocol/exception.rs index 8d127ebb0b..0f777bb178 100644 --- a/relay-general/src/protocol/exception.rs +++ b/relay-general/src/protocol/exception.rs @@ -3,6 +3,7 @@ use crate::types::{Annotated, Object, Value}; /// A single exception. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_exception", value_type = "Exception")] pub struct Exception { /// Exception type. One of value or exception is required, checked in StoreNormalizeProcessor diff --git a/relay-general/src/protocol/fingerprint.rs b/relay-general/src/protocol/fingerprint.rs index eca673dbae..00b3619214 100644 --- a/relay-general/src/protocol/fingerprint.rs +++ b/relay-general/src/protocol/fingerprint.rs @@ -6,6 +6,7 @@ use crate::types::{ /// A fingerprint value. #[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Fingerprint(Vec); impl std::ops::Deref for Fingerprint { diff --git a/relay-general/src/protocol/logentry.rs b/relay-general/src/protocol/logentry.rs index 93499b5c8f..43952e016b 100644 --- a/relay-general/src/protocol/logentry.rs +++ b/relay-general/src/protocol/logentry.rs @@ -6,6 +6,7 @@ use crate::types::{Annotated, Error, FromValue, Meta, Object, Value}; /// A log message is similar to the `message` attribute on the event itself but /// can additionally hold optional parameters. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_logentry", value_type = "LogEntry")] pub struct LogEntry { /// The log message with parameter placeholders. @@ -35,6 +36,7 @@ impl From for LogEntry { } #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(value_type = "Message")] pub struct Message(String); diff --git a/relay-general/src/protocol/mechanism.rs b/relay-general/src/protocol/mechanism.rs index 049d1b3721..998e64fff7 100644 --- a/relay-general/src/protocol/mechanism.rs +++ b/relay-general/src/protocol/mechanism.rs @@ -2,6 +2,7 @@ use crate::types::{Annotated, Error, FromValue, Object, Value}; /// POSIX signal with optional extended data. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct CError { /// The error code as specified by ISO C99, POSIX.1-2001 or POSIX.1-2008. pub number: Annotated, @@ -12,6 +13,7 @@ pub struct CError { /// Mach exception information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct MachException { /// The mach exception type. #[metastructure(field = "exception")] @@ -29,6 +31,7 @@ pub struct MachException { /// POSIX signal with optional extended data. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct PosixSignal { /// The POSIX signal number. pub number: Annotated, @@ -45,6 +48,7 @@ pub struct PosixSignal { /// Operating system or runtime meta information to an exception mechanism. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct MechanismMeta { /// Optional ISO C standard error code. pub errno: Annotated, @@ -62,6 +66,7 @@ pub struct MechanismMeta { /// The mechanism by which an exception was generated and handled. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Mechanism { /// Mechanism type (required). #[metastructure( diff --git a/relay-general/src/protocol/metrics.rs b/relay-general/src/protocol/metrics.rs index 6bb13a8a82..44c62b4397 100644 --- a/relay-general/src/protocol/metrics.rs +++ b/relay-general/src/protocol/metrics.rs @@ -6,6 +6,7 @@ use crate::types::Annotated; /// These values are collected in Relay and Sentry and finally persisted into the event payload. A /// value of `0` is equivalent to N/A and should not be considered in aggregations and analysis. #[derive(Clone, Debug, Default, Empty, PartialEq, FromValue, ToValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Metrics { /// The size of the original event payload ingested into Sentry. /// diff --git a/relay-general/src/protocol/mod.rs b/relay-general/src/protocol/mod.rs index 93d625b076..92592b34b3 100644 --- a/relay-general/src/protocol/mod.rs +++ b/relay-general/src/protocol/mod.rs @@ -11,6 +11,8 @@ mod logentry; mod mechanism; mod metrics; mod request; +#[cfg(feature = "jsonschema")] +mod schema; mod security_report; mod session; mod span; @@ -30,7 +32,8 @@ pub use self::contexts::{ OperationType, OsContext, RuntimeContext, SpanId, SpanStatus, TraceContext, TraceId, }; pub use self::debugmeta::{ - AppleDebugImage, DebugImage, DebugMeta, NativeDebugImage, NativeImagePath, SystemSdkInfo, + AppleDebugImage, CodeId, DebugId, DebugImage, DebugMeta, NativeDebugImage, NativeImagePath, + SystemSdkInfo, }; pub use self::event::{ Event, EventId, EventProcessingError, EventType, ExtraValue, GroupingConfig, @@ -42,6 +45,8 @@ pub use self::logentry::{LogEntry, Message}; pub use self::mechanism::{CError, MachException, Mechanism, MechanismMeta, PosixSignal}; pub use self::metrics::Metrics; pub use self::request::{Cookies, HeaderName, HeaderValue, Headers, Query, Request}; +#[cfg(feature = "jsonschema")] +pub use self::schema::event_json_schema; pub use self::security_report::{Csp, ExpectCt, ExpectStaple, Hpkp, SecurityReportType}; pub use self::session::{ParseSessionStatusError, SessionAttributes, SessionStatus, SessionUpdate}; pub use self::span::Span; @@ -50,8 +55,8 @@ pub use self::tags::{TagEntry, Tags}; pub use self::templateinfo::TemplateInfo; pub use self::thread::{Thread, ThreadId}; pub use self::types::{ - Addr, AsPair, InvalidRegVal, IpAddr, JsonLenientString, LenientString, Level, PairList, - ParseLevelError, RegVal, Values, + datetime_to_timestamp, Addr, AsPair, InvalidRegVal, IpAddr, JsonLenientString, LenientString, + Level, PairList, ParseLevelError, RegVal, Timestamp, Values, }; pub use self::user::{Geo, User}; pub use self::user_report::UserReport; diff --git a/relay-general/src/protocol/request.rs b/relay-general/src/protocol/request.rs index 8f4275d468..a61d1394f6 100644 --- a/relay-general/src/protocol/request.rs +++ b/relay-general/src/protocol/request.rs @@ -10,6 +10,7 @@ type CookieEntry = Annotated<(Annotated, Annotated)>; /// A map holding cookies. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Cookies(pub PairList<(Annotated, Annotated)>); impl Cookies { @@ -84,6 +85,7 @@ impl FromValue for Cookies { /// A "into-string" type that normalizes header names. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_header_name")] pub struct HeaderName(String); @@ -156,6 +158,7 @@ impl FromValue for HeaderName { /// A "into-string" type that normalizes header values. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct HeaderValue(String); impl HeaderValue { @@ -225,6 +228,7 @@ impl FromValue for HeaderValue { /// A map holding headers. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Headers(pub PairList<(Annotated, Annotated)>); impl Headers { @@ -279,6 +283,7 @@ impl FromValue for Headers { /// A map holding query string pairs. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Query(pub PairList<(Annotated, Annotated)>); impl Query { @@ -343,6 +348,7 @@ impl FromValue for Query { /// Http request information. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_request", value_type = "Request")] pub struct Request { /// URL of the request. diff --git a/relay-general/src/protocol/schema.rs b/relay-general/src/protocol/schema.rs new file mode 100644 index 0000000000..3d1776ca63 --- /dev/null +++ b/relay-general/src/protocol/schema.rs @@ -0,0 +1,6 @@ +use crate::protocol::Event; + +/// Get the event schema as JSON schema. The return type is serde-serializable. +pub fn event_json_schema() -> impl serde::Serialize { + schemars::schema_for!(Event) +} diff --git a/relay-general/src/protocol/span.rs b/relay-general/src/protocol/span.rs index b2178abba6..029976a595 100644 --- a/relay-general/src/protocol/span.rs +++ b/relay-general/src/protocol/span.rs @@ -1,18 +1,17 @@ -use chrono::{DateTime, Utc}; - -use crate::protocol::{OperationType, SpanId, SpanStatus, TraceId}; +use crate::protocol::{OperationType, SpanId, SpanStatus, Timestamp, TraceId}; use crate::types::{Annotated, Object, Value}; #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_span", value_type = "Span")] pub struct Span { /// Timestamp when the span was ended. #[metastructure(required = "true")] - pub timestamp: Annotated>, + pub timestamp: Annotated, /// Timestamp when the span started. #[metastructure(required = "true")] - pub start_timestamp: Annotated>, + pub start_timestamp: Annotated, /// Human readable description of a span (e.g. method URL). #[metastructure(pii = "maybe")] @@ -45,7 +44,7 @@ pub struct Span { #[cfg(test)] mod tests { use super::*; - use chrono::TimeZone; + use chrono::{TimeZone, Utc}; #[test] fn test_span_serialization() { @@ -60,8 +59,8 @@ mod tests { }"#; let span = Annotated::new(Span { - timestamp: Annotated::new(Utc.ymd(1970, 1, 1).and_hms_nano(0, 0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(1968, 1, 1).and_hms_nano(0, 0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(1970, 1, 1).and_hms_nano(0, 0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(1968, 1, 1).and_hms_nano(0, 0, 0, 0).into()), description: Annotated::new("desc".to_owned()), op: Annotated::new("operation".to_owned()), trace_id: Annotated::new(TraceId("4c79f60c11214eb38604f4ae0781bfb2".into())), diff --git a/relay-general/src/protocol/stacktrace.rs b/relay-general/src/protocol/stacktrace.rs index 69c0cc64ac..d2a9d9ce8a 100644 --- a/relay-general/src/protocol/stacktrace.rs +++ b/relay-general/src/protocol/stacktrace.rs @@ -5,6 +5,7 @@ use crate::types::{Annotated, Array, FromValue, Object, Value}; /// Holds information about a single stacktrace frame. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_frame", value_type = "Frame")] pub struct Frame { /// Name of the frame's function. This might include the name of a class. @@ -120,10 +121,14 @@ pub struct Frame { /// Frame local variables. #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct FrameVars(#[metastructure(skip_serialization = "empty")] pub Object); /// Additional frame data information. +/// +/// This value is set by the server and should not be set by the SDK. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct FrameData { /// A reference to the sourcemap used. #[metastructure(max_chars = "path")] @@ -180,6 +185,7 @@ impl FromValue for FrameVars { /// Holds information about an entirey stacktrace. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_raw_stacktrace", value_type = "Stacktrace")] pub struct RawStacktrace { #[metastructure(required = "true", nonempty = "true", skip_serialization = "empty")] @@ -199,6 +205,7 @@ pub struct RawStacktrace { /// Newtype to distinguish `raw_stacktrace` attributes from the rest. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_stacktrace")] pub struct Stacktrace(pub RawStacktrace); diff --git a/relay-general/src/protocol/tags.rs b/relay-general/src/protocol/tags.rs index f8da11ece5..73829022e4 100644 --- a/relay-general/src/protocol/tags.rs +++ b/relay-general/src/protocol/tags.rs @@ -2,6 +2,7 @@ use crate::protocol::{AsPair, LenientString, PairList}; use crate::types::{Annotated, Array, FromValue, Value}; #[derive(Clone, Debug, Default, PartialEq, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct TagEntry( #[metastructure(max_chars = "tag_key", match_regex = r"^[a-zA-Z0-9_\.:-]+\z")] pub Annotated, @@ -43,6 +44,7 @@ impl FromValue for TagEntry { /// Manual key/value tag pairs. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct Tags(pub PairList); impl std::ops::Deref for Tags { diff --git a/relay-general/src/protocol/thread.rs b/relay-general/src/protocol/thread.rs index f8577d0fd1..52c6556081 100644 --- a/relay-general/src/protocol/thread.rs +++ b/relay-general/src/protocol/thread.rs @@ -7,6 +7,7 @@ use crate::types::{Annotated, Empty, Error, FromValue, Object, SkipSerialization /// Represents a thread id. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[serde(untagged)] pub enum ThreadId { /// Integer representation of the thread id. @@ -69,6 +70,7 @@ impl Empty for ThreadId { /// A process thread of an event. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_thread", value_type = "Thread")] pub struct Thread { /// Identifier of this thread within the process (usually an integer). diff --git a/relay-general/src/protocol/types.rs b/relay-general/src/protocol/types.rs index 17487260d3..bb3a2ecfb5 100644 --- a/relay-general/src/protocol/types.rs +++ b/relay-general/src/protocol/types.rs @@ -1,11 +1,19 @@ //! Common types of the protocol. use std::borrow::Cow; +use std::cmp::Ordering; use std::fmt; use std::iter::{FromIterator, IntoIterator}; use std::net; +use std::ops::{Add, Deref, DerefMut}; use std::str::FromStr; +use chrono::{DateTime, Datelike, Duration, LocalResult, NaiveDateTime, TimeZone, Utc}; use failure::Fail; +#[cfg(feature = "jsonschema")] +use schemars::gen::SchemaGenerator; +#[cfg(feature = "jsonschema")] +use schemars::schema::Schema; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::processor::{process_value, ProcessValue, ProcessingState, Processor, ValueType}; @@ -27,6 +35,30 @@ pub struct Values { pub other: Object, } +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for Values +where + T: schemars::JsonSchema, +{ + fn schema_name() -> String { + format!("Values_for_{}", T::schema_name()) + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(schemars::JsonSchema)] + #[allow(unused)] + struct Helper { + values: T, + } + + Helper::>::json_schema(gen) + } + + fn is_referenceable() -> bool { + false + } +} + impl Default for Values { fn default() -> Values { // Default implemented manually even if does not impl Default. @@ -265,6 +297,33 @@ impl FromValue for PairList { } } +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for PairList +where + T: schemars::JsonSchema + AsPair, + ::Value: schemars::JsonSchema, +{ + fn schema_name() -> String { + format!("PairList_of_{}", T::schema_name()) + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + #[derive(schemars::JsonSchema)] + #[cfg_attr(feature = "jsonschema", schemars(untagged))] + #[allow(unused)] + enum Helper { + Object(Object), + Array(Array), + } + + Helper::::json_schema(gen) + } + + fn is_referenceable() -> bool { + false + } +} + impl ProcessValue for PairList where T: ProcessValue + AsPair, @@ -374,6 +433,20 @@ macro_rules! hex_metrastructure { } impl ProcessValue for $type {} + #[cfg(feature = "jsonschema")] + impl schemars::JsonSchema for $type { + fn schema_name() -> String { + stringify!($type).to_owned() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + String::json_schema(gen) + } + + fn is_referenceable() -> bool { + true + } + } }; } @@ -400,6 +473,21 @@ hex_metrastructure!(Addr, "address"); )] pub struct IpAddr(pub String); +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for IpAddr { + fn schema_name() -> String { + String::schema_name() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + String::json_schema(gen) + } + + fn is_referenceable() -> bool { + true + } +} + impl IpAddr { /// Returns the auto marker ip address. pub fn auto() -> IpAddr { @@ -494,6 +582,8 @@ pub struct ParseLevelError; /// Severity level of an event or breadcrumb. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "jsonschema", schemars(rename_all = "lowercase"))] pub enum Level { /// Indicates very spammy debug information. Debug, @@ -619,6 +709,21 @@ impl Empty for Level { #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, ToValue, ProcessValue)] pub struct LenientString(pub String); +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for LenientString { + fn schema_name() -> String { + "LenientString".to_owned() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + String::json_schema(gen) + } + + fn is_referenceable() -> bool { + false + } +} + impl LenientString { /// Returns the string value. pub fn as_str(&self) -> &str { @@ -688,6 +793,7 @@ impl FromValue for LenientString { /// A "into-string" type of value. All non-string values are serialized as JSON. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Empty, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] pub struct JsonLenientString(pub String); impl JsonLenientString { @@ -740,6 +846,220 @@ impl From for JsonLenientString { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct Timestamp(pub DateTime); + +impl Timestamp { + pub fn into_inner(self) -> DateTime { + self.0 + } +} + +impl ProcessValue for Timestamp { + #[inline] + fn value_type(&self) -> Option { + Some(ValueType::DateTime) + } + + #[inline] + fn process_value

( + &mut self, + meta: &mut Meta, + processor: &mut P, + state: &ProcessingState<'_>, + ) -> ProcessingResult + where + P: Processor, + { + processor.process_timestamp(self, meta, state) + } +} + +impl From> for Timestamp { + fn from(value: DateTime) -> Self { + Timestamp(value) + } +} + +impl Deref for Timestamp { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Timestamp { + fn deref_mut(&mut self) -> &mut ::Target { + &mut self.0 + } +} + +impl Add for Timestamp { + type Output = Self; + + fn add(self, duration: Duration) -> Self::Output { + Timestamp(self.0 + duration) + } +} + +impl PartialEq> for Timestamp { + fn eq(&self, other: &DateTime) -> bool { + &self.0 == other + } +} + +impl PartialOrd> for Timestamp { + fn partial_cmp(&self, other: &DateTime) -> Option { + self.0.partial_cmp(other) + } +} + +impl fmt::Display for Timestamp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +pub fn datetime_to_timestamp(dt: DateTime) -> f64 { + // f64s cannot store nanoseconds. To verify this just try to fit the current timestamp in + // nanoseconds into a 52-bit number (which is the significand of a double). + // + // Round off to microseconds to not show more decimal points than we know are correct. Anything + // else might trick the user into thinking the nanoseconds in those timestamps mean anything. + // + // This needs to be done regardless of whether the input value was a ISO-formatted string or a + // number because it all ends up as a f64 on serialization. + // + // If we want to support nanoseconds at some point we will probably have to start using strings + // everywhere. Even then it's unclear how to deal with it in Python code as a `datetime` cannot + // store nanoseconds. + // + // We use `timestamp_subsec_nanos` instead of `timestamp_subsec_micros` anyway to get better + // rounding behavior. + let micros = (f64::from(dt.timestamp_subsec_nanos()) / 1_000f64).round(); + dt.timestamp() as f64 + (micros / 1_000_000f64) +} + +fn utc_result_to_annotated( + result: LocalResult>, + original_value: V, + mut meta: Meta, +) -> Annotated> { + match result { + LocalResult::Single(value) => Annotated(Some(value), meta), + LocalResult::Ambiguous(_, _) => { + meta.add_error(Error::expected("ambiguous timestamp")); + meta.set_original_value(Some(original_value)); + Annotated(None, meta) + } + LocalResult::None => { + meta.add_error(Error::invalid("timestamp out of range")); + meta.set_original_value(Some(original_value)); + Annotated(None, meta) + } + } +} + +impl FromValue for Timestamp { + fn from_value(value: Annotated) -> Annotated { + let rv = match value { + Annotated(Some(Value::String(value)), mut meta) => { + let parsed = match value.parse::() { + Ok(dt) => Ok(DateTime::from_utc(dt, Utc)), + Err(_) => value.parse(), + }; + match parsed { + Ok(value) => Annotated(Some(value), meta), + Err(err) => { + meta.add_error(Error::invalid(err)); + meta.set_original_value(Some(value)); + Annotated(None, meta) + } + } + } + Annotated(Some(Value::U64(ts)), meta) => { + utc_result_to_annotated(Utc.timestamp_opt(ts as i64, 0), ts, meta) + } + Annotated(Some(Value::I64(ts)), meta) => { + utc_result_to_annotated(Utc.timestamp_opt(ts, 0), ts, meta) + } + Annotated(Some(Value::F64(ts)), meta) => { + let secs = ts as i64; + // at this point we probably already lose nanosecond precision, but we deal with + // this in `datetime_to_timestamp`. + let nanos = (ts.fract() * 1_000_000_000f64) as u32; + utc_result_to_annotated(Utc.timestamp_opt(secs, nanos), ts, meta) + } + Annotated(None, meta) => Annotated(None, meta), + Annotated(Some(value), mut meta) => { + meta.add_error(Error::expected("a timestamp")); + meta.set_original_value(Some(value)); + Annotated(None, meta) + } + }; + + match rv { + Annotated(Some(value), mut meta) => { + if value.year() > 9999 { + // We need to enforce this because Python has a max value for year and + // otherwise crashes. Also this is probably nicer UX than silently showing the + // wrong value. + meta.add_error(Error::invalid("timestamp out of range")); + meta.set_original_value(Some(Timestamp(value))); + Annotated(None, meta) + } else { + Annotated(Some(Timestamp(value)), meta) + } + } + x => x.map_value(Timestamp), + } + } +} + +impl ToValue for Timestamp { + fn to_value(self) -> Value { + Value::F64(datetime_to_timestamp(self.0)) + } + + fn serialize_payload(&self, s: S, _behavior: SkipSerialization) -> Result + where + Self: Sized, + S: Serializer, + { + Serialize::serialize(&datetime_to_timestamp(self.0), s) + } +} + +impl Empty for Timestamp { + fn is_empty(&self) -> bool { + false + } +} + +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for Timestamp { + fn schema_name() -> String { + "Timestamp".to_owned() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + /// Can be a ISO-8601 formatted string or a unix timestamp in seconds (floating point + /// values allowed). + /// + /// Must be UTC. + #[derive(schemars::JsonSchema)] + #[cfg_attr(feature = "jsonschema", schemars(untagged))] + #[allow(unused)] + enum Helper { + UnixTimestamp(f64), + IsoTimestamp(String), + } + + Helper::json_schema(gen) + } +} + #[test] fn test_values_serialization() { let value = Annotated::new(Values { @@ -876,3 +1196,65 @@ fn test_ip_addr() { Annotated::::from_json("\"clearly invalid value\"").unwrap() ); } + +#[test] +fn test_timestamp_year_out_of_range() { + #[derive(Debug, FromValue, Default, Empty, ToValue)] + struct Helper { + foo: Annotated, + } + + let x: Annotated = Annotated::from_json(r#"{"foo": 1562770897893}"#).unwrap(); + assert_eq_str!( + x.to_json_pretty().unwrap(), + r#"{ + "foo": null, + "_meta": { + "foo": { + "": { + "err": [ + [ + "invalid_data", + { + "reason": "timestamp out of range" + } + ] + ], + "val": 1562770897893.0 + } + } + } +}"# + ); +} + +#[test] +fn test_timestamp_completely_out_of_range() { + #[derive(Debug, FromValue, Default, Empty, ToValue)] + struct Helper { + foo: Annotated, + } + + let x: Annotated = Annotated::from_json(r#"{"foo": -10000000000000000.0}"#).unwrap(); + assert_eq_str!( + x.to_json_pretty().unwrap(), + r#"{ + "foo": null, + "_meta": { + "foo": { + "": { + "err": [ + [ + "invalid_data", + { + "reason": "timestamp out of range" + } + ] + ], + "val": -1e16 + } + } + } +}"# + ); +} diff --git a/relay-general/src/protocol/user.rs b/relay-general/src/protocol/user.rs index 0414a26793..b86d35a17d 100644 --- a/relay-general/src/protocol/user.rs +++ b/relay-general/src/protocol/user.rs @@ -3,6 +3,7 @@ use crate::types::{Annotated, Object, Value}; /// Geographical location of the end user or device. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_geo")] pub struct Geo { /// Two-letter country code (ISO 3166-1 alpha-2). @@ -24,6 +25,7 @@ pub struct Geo { /// Information about the user who triggered an event. #[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, ToValue, ProcessValue)] +#[cfg_attr(feature = "jsonschema", derive(JsonSchema))] #[metastructure(process_func = "process_user", value_type = "User")] pub struct User { /// Unique identifier of the user. diff --git a/relay-general/src/store/clock_drift.rs b/relay-general/src/store/clock_drift.rs index f00c82d52c..596f215a7a 100644 --- a/relay-general/src/store/clock_drift.rs +++ b/relay-general/src/store/clock_drift.rs @@ -3,8 +3,8 @@ use std::time::Duration; use chrono::{DateTime, Duration as SignedDuration, Utc}; use crate::processor::{ProcessValue, ProcessingState, Processor}; -use crate::protocol::{Event, SessionUpdate}; -use crate::types::{Error, ErrorKind, Meta, ProcessingResult, Timestamp}; +use crate::protocol::{Event, SessionUpdate, Timestamp}; +use crate::types::{Error, ErrorKind, Meta, ProcessingResult}; /// A signed correction that contains the sender's timestamp as well as the drift to the receiver. #[derive(Clone, Copy, Debug)] @@ -150,8 +150,8 @@ mod tests { fn create_transaction(start: DateTime, end: DateTime) -> Annotated { Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(end), - start_timestamp: Annotated::new(start), + timestamp: Annotated::new(end.into()), + start_timestamp: Annotated::new(start.into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( diff --git a/relay-general/src/store/legacy.rs b/relay-general/src/store/legacy.rs index 817f860894..ef2121adf2 100644 --- a/relay-general/src/store/legacy.rs +++ b/relay-general/src/store/legacy.rs @@ -1,9 +1,7 @@ use std::mem; -use debugid::DebugId; - use crate::processor::{ProcessingState, Processor}; -use crate::protocol::{DebugImage, NativeDebugImage}; +use crate::protocol::{DebugId, DebugImage, NativeDebugImage}; use crate::types::{Annotated, Meta, Object, ProcessingResult}; /// Converts legacy data structures to current format. diff --git a/relay-general/src/store/normalize.rs b/relay-general/src/store/normalize.rs index 7477627e41..6bfc40136a 100644 --- a/relay-general/src/store/normalize.rs +++ b/relay-general/src/store/normalize.rs @@ -149,15 +149,15 @@ impl<'a> NormalizeProcessor<'a> { Ok(()) })?; - ClockDriftProcessor::new(sent_at, received_at) + ClockDriftProcessor::new(sent_at.map(|x| *x), received_at) .error_kind(error_kind) .process_event(event, meta, state)?; // Apply this after clock drift correction, otherwise we will malform it. - event.received = Annotated::new(received_at); + event.received = Annotated::new(received_at.into()); if event.timestamp.value().is_none() { - event.timestamp.set_value(Some(received_at)); + event.timestamp.set_value(Some(received_at.into())); } event @@ -1503,7 +1503,7 @@ fn test_future_timestamp() { use insta::assert_ron_snapshot; let mut event = Annotated::new(Event { - timestamp: Annotated::new(Utc.ymd(2000, 1, 3).and_hms(0, 2, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 3).and_hms(0, 2, 0).into()), ..Default::default() }); @@ -1556,7 +1556,7 @@ fn test_past_timestamp() { use insta::assert_ron_snapshot; let mut event = Annotated::new(Event { - timestamp: Annotated::new(Utc.ymd(2000, 1, 3).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 3).and_hms(0, 0, 0).into()), ..Default::default() }); diff --git a/relay-general/src/store/transactions.rs b/relay-general/src/store/transactions.rs index 605fd64d2d..46d4076303 100644 --- a/relay-general/src/store/transactions.rs +++ b/relay-general/src/store/transactions.rs @@ -166,8 +166,8 @@ mod tests { Annotated::new(Event { ty: Annotated::new(EventType::Transaction), transaction: Annotated::new("/".to_owned()), - start_timestamp: Annotated::new(start), - timestamp: Annotated::new(end), + start_timestamp: Annotated::new(start.into()), + timestamp: Annotated::new(end.into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -184,8 +184,8 @@ mod tests { contexts })), spans: Annotated::new(vec![Annotated::new(Span { - start_timestamp: Annotated::new(start), - timestamp: Annotated::new(end), + start_timestamp: Annotated::new(start.into()), + timestamp: Annotated::new(end.into()), trace_id: Annotated::new(TraceId("4c79f60c11214eb38604f4ae0781bfb2".into())), span_id: Annotated::new(SpanId("fa90fdead5f74053".into())), op: Annotated::new("db.statement".to_owned()), @@ -230,7 +230,7 @@ mod tests { fn test_discards_when_missing_start_timestamp() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), ..Default::default() }); @@ -250,8 +250,8 @@ mod tests { fn test_discards_on_missing_contexts_map() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), ..Default::default() }); @@ -271,8 +271,8 @@ mod tests { fn test_discards_on_missing_context() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts(Object::new())), ..Default::default() }); @@ -293,8 +293,8 @@ mod tests { fn test_discards_on_null_context() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert("trace".to_owned(), Annotated::empty()); @@ -319,8 +319,8 @@ mod tests { fn test_discards_on_missing_trace_id_in_context() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -350,8 +350,8 @@ mod tests { fn test_discards_on_missing_span_id_in_context() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -388,8 +388,8 @@ mod tests { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), transaction: Annotated::new("/".to_owned()), - timestamp: Annotated::new(end), - start_timestamp: Annotated::new(start), + timestamp: Annotated::new(end.into()), + start_timestamp: Annotated::new(start.into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -437,8 +437,8 @@ mod tests { fn test_allows_transaction_event_without_span_list() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -470,8 +470,8 @@ mod tests { fn test_allows_transaction_event_with_empty_span_list() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -541,8 +541,8 @@ mod tests { fn test_discards_transaction_event_with_nulled_out_span() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -578,8 +578,8 @@ mod tests { fn test_discards_transaction_event_with_span_with_missing_timestamp() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -617,8 +617,8 @@ mod tests { fn test_discards_transaction_event_with_span_with_missing_start_timestamp() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -635,7 +635,7 @@ mod tests { contexts })), spans: Annotated::new(vec![Annotated::new(Span { - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), ..Default::default() })]), ..Default::default() @@ -657,8 +657,8 @@ mod tests { fn test_discards_transaction_event_with_span_with_missing_trace_id() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -675,8 +675,8 @@ mod tests { contexts })), spans: Annotated::new(vec![Annotated::new(Span { - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), ..Default::default() })]), ..Default::default() @@ -698,8 +698,8 @@ mod tests { fn test_discards_transaction_event_with_span_with_missing_span_id() { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -716,8 +716,8 @@ mod tests { contexts })), spans: Annotated::new(vec![Annotated::new(Span { - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), trace_id: Annotated::new(TraceId("4c79f60c11214eb38604f4ae0781bfb2".into())), ..Default::default() })]), @@ -744,8 +744,8 @@ mod tests { let mut event = Annotated::new(Event { ty: Annotated::new(EventType::Transaction), transaction: Annotated::new("/".to_owned()), - timestamp: Annotated::new(end), - start_timestamp: Annotated::new(start), + timestamp: Annotated::new(end.into()), + start_timestamp: Annotated::new(start.into()), contexts: Annotated::new(Contexts({ let mut contexts = Object::new(); contexts.insert( @@ -762,8 +762,8 @@ mod tests { contexts })), spans: Annotated::new(vec![Annotated::new(Span { - timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 10)), - start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0)), + timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 10).into()), + start_timestamp: Annotated::new(Utc.ymd(2000, 1, 1).and_hms(0, 0, 0).into()), trace_id: Annotated::new(TraceId("4c79f60c11214eb38604f4ae0781bfb2".into())), span_id: Annotated::new(SpanId("fa90fdead5f74053".into())), diff --git a/relay-general/src/types/annotated.rs b/relay-general/src/types/annotated.rs index aa2c7743ca..e706f29615 100644 --- a/relay-general/src/types/annotated.rs +++ b/relay-general/src/types/annotated.rs @@ -2,6 +2,11 @@ use std::fmt; use failure::Fail; +#[cfg(feature = "jsonschema")] +use schemars::gen::SchemaGenerator; +#[cfg(feature = "jsonschema")] +use schemars::schema::Schema; + use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -82,6 +87,15 @@ pub struct Annotated(pub Option, pub Meta); /// An utility to serialize annotated objects with payload. pub struct SerializableAnnotated<'a, T>(pub &'a Annotated); +impl<'a, T: ToValue> Serialize for SerializableAnnotated<'a, T> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize_with_meta(serializer) + } +} + impl fmt::Debug for Annotated { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { @@ -402,12 +416,24 @@ impl Default for Annotated { } } -impl<'a, T: ToValue> Serialize for SerializableAnnotated<'a, T> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.0.serialize_with_meta(serializer) +// This hack is needed to make our custom derive for JsonSchema simpler. However, Serialize should +// not be implemented on Annotated as one should usually use ToValue directly, or +// SerializableAnnotated explicitly if really needed (eg: tests) +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for Annotated +where + T: schemars::JsonSchema, +{ + fn schema_name() -> String { + format!("Annotated_{}", T::schema_name()) + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + Option::::json_schema(gen) + } + + fn is_referenceable() -> bool { + false } } diff --git a/relay-general/src/types/impls.rs b/relay-general/src/types/impls.rs index 27522343e7..d34b077084 100644 --- a/relay-general/src/types/impls.rs +++ b/relay-general/src/types/impls.rs @@ -1,4 +1,3 @@ -use chrono::{DateTime, Datelike, LocalResult, NaiveDateTime, TimeZone, Utc}; use serde::ser::{SerializeMap, SerializeSeq}; use serde::{Serialize, Serializer}; use uuid::Uuid; @@ -402,122 +401,6 @@ impl ToValue for Value { } } -pub fn datetime_to_timestamp(dt: DateTime) -> f64 { - // f64s cannot store nanoseconds. To verify this just try to fit the current timestamp in - // nanoseconds into a 52-bit number (which is the significand of a double). - // - // Round off to microseconds to not show more decimal points than we know are correct. Anything - // else might trick the user into thinking the nanoseconds in those timestamps mean anything. - // - // This needs to be done regardless of whether the input value was a ISO-formatted string or a - // number because it all ends up as a f64 on serialization. - // - // If we want to support nanoseconds at some point we will probably have to start using strings - // everywhere. Even then it's unclear how to deal with it in Python code as a `datetime` cannot - // store nanoseconds. - // - // We use `timestamp_subsec_nanos` instead of `timestamp_subsec_micros` anyway to get better - // rounding behavior. - let micros = (f64::from(dt.timestamp_subsec_nanos()) / 1_000f64).round(); - dt.timestamp() as f64 + (micros / 1_000_000f64) -} - -fn utc_result_to_annotated( - result: LocalResult>, - original_value: V, - mut meta: Meta, -) -> Annotated> { - match result { - LocalResult::Single(value) => Annotated(Some(value), meta), - LocalResult::Ambiguous(_, _) => { - meta.add_error(Error::expected("ambiguous timestamp")); - meta.set_original_value(Some(original_value)); - Annotated(None, meta) - } - LocalResult::None => { - meta.add_error(Error::invalid("timestamp out of range")); - meta.set_original_value(Some(original_value)); - Annotated(None, meta) - } - } -} - -impl FromValue for DateTime { - fn from_value(value: Annotated) -> Annotated { - let rv = match value { - Annotated(Some(Value::String(value)), mut meta) => { - let parsed = match value.parse::() { - Ok(dt) => Ok(DateTime::from_utc(dt, Utc)), - Err(_) => value.parse(), - }; - match parsed { - Ok(value) => Annotated(Some(value), meta), - Err(err) => { - meta.add_error(Error::invalid(err)); - meta.set_original_value(Some(value)); - Annotated(None, meta) - } - } - } - Annotated(Some(Value::U64(ts)), meta) => { - utc_result_to_annotated(Utc.timestamp_opt(ts as i64, 0), ts, meta) - } - Annotated(Some(Value::I64(ts)), meta) => { - utc_result_to_annotated(Utc.timestamp_opt(ts, 0), ts, meta) - } - Annotated(Some(Value::F64(ts)), meta) => { - let secs = ts as i64; - // at this point we probably already lose nanosecond precision, but we deal with - // this in `datetime_to_timestamp`. - let nanos = (ts.fract() * 1_000_000_000f64) as u32; - utc_result_to_annotated(Utc.timestamp_opt(secs, nanos), ts, meta) - } - Annotated(None, meta) => Annotated(None, meta), - Annotated(Some(value), mut meta) => { - meta.add_error(Error::expected("a timestamp")); - meta.set_original_value(Some(value)); - Annotated(None, meta) - } - }; - - match rv { - Annotated(Some(value), mut meta) => { - if value.year() > 9999 { - // We need to enforce this because Python has a max value for year and - // otherwise crashes. Also this is probably nicer UX than silently showing the - // wrong value. - meta.add_error(Error::invalid("timestamp out of range")); - meta.set_original_value(Some(value)); - Annotated(None, meta) - } else { - Annotated(Some(value), meta) - } - } - x => x, - } - } -} - -impl ToValue for DateTime { - fn to_value(self) -> Value { - Value::F64(datetime_to_timestamp(self)) - } - - fn serialize_payload(&self, s: S, _behavior: SkipSerialization) -> Result - where - Self: Sized, - S: Serializer, - { - Serialize::serialize(&datetime_to_timestamp(*self), s) - } -} - -impl Empty for DateTime { - fn is_empty(&self) -> bool { - false - } -} - impl FromValue for Box where T: FromValue, @@ -1157,65 +1040,3 @@ fn test_skip_serialization_on_regular_structs() { assert_eq_str!(helper.to_json().unwrap(), r#"{"foo":{}}"#); } - -#[test] -fn test_timestamp_year_out_of_range() { - #[derive(Debug, FromValue, Default, Empty, ToValue)] - struct Helper { - foo: Annotated>, - } - - let x: Annotated = Annotated::from_json(r#"{"foo": 1562770897893}"#).unwrap(); - assert_eq_str!( - x.to_json_pretty().unwrap(), - r#"{ - "foo": null, - "_meta": { - "foo": { - "": { - "err": [ - [ - "invalid_data", - { - "reason": "timestamp out of range" - } - ] - ], - "val": 1562770897893.0 - } - } - } -}"# - ); -} - -#[test] -fn test_timestamp_completely_out_of_range() { - #[derive(Debug, FromValue, Default, Empty, ToValue)] - struct Helper { - foo: Annotated>, - } - - let x: Annotated = Annotated::from_json(r#"{"foo": -10000000000000000.0}"#).unwrap(); - assert_eq_str!( - x.to_json_pretty().unwrap(), - r#"{ - "foo": null, - "_meta": { - "foo": { - "": { - "err": [ - [ - "invalid_data", - { - "reason": "timestamp out of range" - } - ] - ], - "val": -1e16 - } - } - } -}"# - ); -} diff --git a/relay-general/src/types/mod.rs b/relay-general/src/types/mod.rs index 258968fe64..ac32895c1a 100644 --- a/relay-general/src/types/mod.rs +++ b/relay-general/src/types/mod.rs @@ -13,7 +13,7 @@ mod value; pub use self::annotated::{ Annotated, MetaMap, MetaTree, ProcessingAction, ProcessingResult, SerializableAnnotated, }; -pub use self::impls::{datetime_to_timestamp, SerializePayload}; +pub use self::impls::SerializePayload; pub use self::meta::{Error, ErrorKind, Meta, Range, Remark, RemarkType}; pub use self::traits::{Empty, FromValue, SkipSerialization, ToValue}; -pub use self::value::{to_value, Array, Map, Object, Timestamp, Value, ValueDescription}; +pub use self::value::{to_value, Array, Map, Object, Value, ValueDescription}; diff --git a/relay-general/src/types/value.rs b/relay-general/src/types/value.rs index bbf5b597bf..fffe0666d2 100644 --- a/relay-general/src/types/value.rs +++ b/relay-general/src/types/value.rs @@ -2,6 +2,11 @@ use std::collections::BTreeMap; use std::fmt; use std::str; +#[cfg(feature = "jsonschema")] +use schemars::gen::SchemaGenerator; +#[cfg(feature = "jsonschema")] +use schemars::schema::Schema; + use serde::de::{Deserialize, MapAccess, SeqAccess, Visitor}; use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer}; @@ -16,9 +21,6 @@ pub type Map = BTreeMap; /// Alias for typed objects. pub type Object = Map>; -/// Alias for datetimes. -pub type Timestamp = chrono::DateTime; - /// Represents a boxed value. #[derive(Debug, Clone, PartialEq, ProcessValue)] #[metastructure(process_func = "process_value")] @@ -32,6 +34,21 @@ pub enum Value { Object(Object), } +#[cfg(feature = "jsonschema")] +impl schemars::JsonSchema for Value { + fn schema_name() -> String { + "Value".to_owned() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + Schema::Bool(true) + } + + fn is_referenceable() -> bool { + false + } +} + /// Helper type that renders out a description of the value. pub struct ValueDescription<'a>(&'a Value); diff --git a/relay-general/tests/snapshots/test_fixtures__event_schema.snap b/relay-general/tests/snapshots/test_fixtures__event_schema.snap new file mode 100644 index 0000000000..e29a5065d0 --- /dev/null +++ b/relay-general/tests/snapshots/test_fixtures__event_schema.snap @@ -0,0 +1,2935 @@ +--- +source: relay-general/tests/test_fixtures.rs +expression: event_json_schema() +--- +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Event", + "anyOf": [ + { + "type": "object", + "properties": { + "breadcrumbs": { + "description": "List of breadcrumbs recorded before this event.", + "default": null, + "type": [ + "object", + "null" + ], + "required": [ + "values" + ], + "properties": { + "values": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Breadcrumb" + }, + { + "type": "null" + } + ] + } + } + } + }, + "contexts": { + "description": "Contexts describing the environment (e.g. device, os or browser).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Contexts" + }, + { + "type": "null" + } + ] + }, + "culprit": { + "description": "Custom culprit of the event.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "debug_meta": { + "description": "Meta data for event processing and debugging.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/DebugMeta" + }, + { + "type": "null" + } + ] + }, + "dist": { + "description": "Program's distribution identifier.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "environment": { + "description": "Environment the environment was generated in (\"production\" or \"development\").", + "default": null, + "type": [ + "string", + "null" + ] + }, + "errors": { + "description": "Errors encountered during processing. Intended to be phased out in favor of annotation/metadata system.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/EventProcessingError" + }, + { + "type": "null" + } + ] + } + }, + "event_id": { + "description": "Unique identifier of this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/EventId" + }, + { + "type": "null" + } + ] + }, + "exception": { + "description": "One or multiple chained (nested) exceptions.", + "default": null, + "type": [ + "object", + "null" + ], + "required": [ + "values" + ], + "properties": { + "values": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Exception" + }, + { + "type": "null" + } + ] + } + } + } + }, + "extra": { + "description": "Arbitrary extra information set by the user.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "fingerprint": { + "description": "Manual fingerprint override.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Fingerprint" + }, + { + "type": "null" + } + ] + }, + "key_id": { + "description": "Project key which sent this event.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "level": { + "description": "Severity level of the event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Level" + }, + { + "type": "null" + } + ] + }, + "logentry": { + "description": "Custom parameterized message for this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/LogEntry" + }, + { + "type": "null" + } + ] + }, + "logger": { + "description": "Logger that created the event.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "modules": { + "description": "Name and versions of installed modules.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + "platform": { + "description": "Platform identifier of this event (defaults to \"other\").", + "default": null, + "type": [ + "string", + "null" + ] + }, + "project": { + "description": "Project which sent this event.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "received": { + "description": "Timestamp when the event has been received by Sentry.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "release": { + "description": "Program's release identifier.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "request": { + "description": "Information about a web request that occurred during the event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Request" + }, + { + "type": "null" + } + ] + }, + "sdk": { + "description": "Information about the Sentry SDK that generated this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ClientSdkInfo" + }, + { + "type": "null" + } + ] + }, + "server_name": { + "description": "Server or device name the event was generated on.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "site": { + "description": "Deprecated in favor of tags.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "spans": { + "description": "Spans for tracing.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Span" + }, + { + "type": "null" + } + ] + } + }, + "stacktrace": { + "description": "Event stacktrace.\n\nDEPRECATED: Prefer `threads` or `exception` depending on which is more appropriate.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Stacktrace" + }, + { + "type": "null" + } + ] + }, + "start_timestamp": { + "description": "Timestamp when the event has started (relevant for event type = \"transaction\")", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "tags": { + "description": "Custom tags for this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Tags" + }, + { + "type": "null" + } + ] + }, + "threads": { + "description": "Threads that were active when the event occurred.", + "default": null, + "type": [ + "object", + "null" + ], + "required": [ + "values" + ], + "properties": { + "values": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Thread" + }, + { + "type": "null" + } + ] + } + } + } + }, + "time_spent": { + "description": "Time since the start of the transaction until the error occurred.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "timestamp": { + "description": "Timestamp when the event was created.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "transaction": { + "description": "Transaction name of the event.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "Type of event: error, csp, default", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/EventType" + }, + { + "type": "null" + } + ] + }, + "user": { + "description": "Information about the user who triggered this event.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/User" + }, + { + "type": "null" + } + ] + }, + "version": { + "description": "Version", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ], + "definitions": { + "Addr": { + "type": "string" + }, + "AppContext": { + "anyOf": [ + { + "type": "object", + "properties": { + "app_build": { + "description": "Internal build ID as it appears on the platform.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_identifier": { + "description": "App identifier (dotted bundle id).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_name": { + "description": "Application name as it appears on the platform.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_start_time": { + "description": "Start time of the app.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "app_version": { + "description": "Application version as it appears on the platform.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "build_type": { + "description": "Build identicator.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "device_app_hash": { + "description": "Device app hash (app specific device ID)", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "AppleDebugImage": { + "anyOf": [ + { + "type": "object", + "required": [ + "image_addr", + "image_size", + "name", + "uuid" + ], + "properties": { + "arch": { + "description": "CPU architecture target.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "cpu_subtype": { + "description": "MachO CPU subtype identifier.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "cpu_type": { + "description": "MachO CPU type identifier.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "image_addr": { + "description": "Starting memory address of the image (required).", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "image_size": { + "description": "Size of the image in bytes (required).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "image_vmaddr": { + "description": "Loading address in virtual memory.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "Path and name of the debug image (required).", + "type": [ + "string", + "null" + ] + }, + "uuid": { + "description": "The unique UUID of the image.", + "type": [ + "string", + "null" + ], + "format": "uuid" + } + } + } + ] + }, + "Breadcrumb": { + "anyOf": [ + { + "type": "object", + "properties": { + "category": { + "description": "The optional category of the breadcrumb.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "data": { + "description": "Custom user-defined data of this breadcrumb.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "level": { + "description": "Severity level of the breadcrumb.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Level" + }, + { + "type": "null" + } + ] + }, + "message": { + "description": "Human readable message for the breadcrumb.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "timestamp": { + "description": "The timestamp of the breadcrumb.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "The type of the breadcrumb.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "BrowserContext": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "description": "Runtime name.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "Runtime version.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "CError": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "description": "Optional name of the errno constant.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "number": { + "description": "The error code as specified by ISO C99, POSIX.1-2001 or POSIX.1-2008.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + } + ] + }, + "ClientSdkInfo": { + "anyOf": [ + { + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "client_ip": { + "description": "IP Address of sender??? Seems unused.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/String" + }, + { + "type": "null" + } + ] + }, + "integrations": { + "description": "List of integrations that are enabled in the SDK.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "name": { + "description": "Unique SDK name.", + "type": [ + "string", + "null" + ] + }, + "packages": { + "description": "List of installed and loaded SDK packages.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/ClientSdkPackage" + }, + { + "type": "null" + } + ] + } + }, + "version": { + "description": "SDK version.", + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "ClientSdkPackage": { + "anyOf": [ + { + "type": "object", + "properties": { + "name": { + "description": "Name of the package.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "Version of the package.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "CodeId": { + "type": "string" + }, + "Context": { + "anyOf": [ + { + "$ref": "#/definitions/DeviceContext" + }, + { + "$ref": "#/definitions/OsContext" + }, + { + "$ref": "#/definitions/RuntimeContext" + }, + { + "$ref": "#/definitions/AppContext" + }, + { + "$ref": "#/definitions/BrowserContext" + }, + { + "$ref": "#/definitions/GpuContext" + }, + { + "$ref": "#/definitions/TraceContext" + }, + { + "$ref": "#/definitions/MonitorContext" + }, + { + "type": "object", + "additionalProperties": true + } + ] + }, + "ContextInner": { + "anyOf": [ + { + "$ref": "#/definitions/Context" + } + ] + }, + "Contexts": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/ContextInner" + }, + { + "type": "null" + } + ] + } + } + ] + }, + "Cookies": { + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + { + "type": "array", + "items": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "string", + "null" + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + } + ] + }, + "DebugId": { + "type": "string" + }, + "DebugImage": { + "anyOf": [ + { + "$ref": "#/definitions/AppleDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/NativeDebugImage" + }, + { + "$ref": "#/definitions/ProguardDebugImage" + }, + { + "type": "object", + "additionalProperties": true + } + ] + }, + "DebugMeta": { + "anyOf": [ + { + "type": "object", + "properties": { + "images": { + "description": "List of debug information files (debug images).", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/DebugImage" + }, + { + "type": "null" + } + ] + } + }, + "sdk_info": { + "description": "Information about the system SDK (e.g. iOS SDK).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/SystemSdkInfo" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "DeviceContext": { + "anyOf": [ + { + "type": "object", + "properties": { + "arch": { + "description": "Native cpu architecture of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "battery_level": { + "description": "Current battery level (0-100).", + "default": null, + "type": [ + "number", + "null" + ], + "format": "double" + }, + "boot_time": { + "description": "Indicator when the device was booted.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "brand": { + "description": "Brand of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "charging": { + "description": "Whether the device was charging or not.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "external_free_storage": { + "description": "Free size of the attached external storage in bytes (eg: android SDK card).", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "external_storage_size": { + "description": "Total size of the attached external storage in bytes (eg: android SDK card).", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "family": { + "description": "Family of the device model.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "free_memory": { + "description": "How much memory is still available in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "free_storage": { + "description": "How much storage is free in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "low_memory": { + "description": "Whether the device was low on memory.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "manufacturer": { + "description": "Manufacturer of the device", + "default": null, + "type": [ + "string", + "null" + ] + }, + "memory_size": { + "description": "Total memory available in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "model": { + "description": "Device model (human readable).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "model_id": { + "description": "Device model (internal identifier).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Name of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "online": { + "description": "Whether the device was online or not.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "orientation": { + "description": "Current screen orientation.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "screen_density": { + "description": "Device screen density.", + "default": null, + "type": [ + "number", + "null" + ], + "format": "double" + }, + "screen_dpi": { + "description": "Screen density as dots-per-inch.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "screen_resolution": { + "description": "Device screen resolution.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "simulator": { + "description": "Simulator/prod indicator.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "storage_size": { + "description": "Total storage size of the device in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "timezone": { + "description": "Timezone of the device.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "usable_memory": { + "description": "How much memory is usable for the app in bytes.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + ] + }, + "EventId": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + "EventProcessingError": { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "name": { + "description": "Affected key or deep path.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "type": { + "description": "The error kind.", + "type": [ + "string", + "null" + ] + }, + "value": { + "description": "The original value causing this error.", + "default": null + } + } + } + ] + }, + "EventType": { + "description": "The type of an event.", + "type": "string", + "enum": [ + "error", + "csp", + "hpkp", + "expectct", + "expectstaple", + "transaction", + "default" + ] + }, + "Exception": { + "anyOf": [ + { + "type": "object", + "properties": { + "mechanism": { + "description": "Mechanism by which this exception was generated and handled.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Mechanism" + }, + { + "type": "null" + } + ] + }, + "module": { + "description": "Module name of this exception.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "raw_stacktrace": { + "description": "Optional unprocessed stack trace.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/RawStacktrace" + }, + { + "type": "null" + } + ] + }, + "stacktrace": { + "description": "Stack trace containing frames of this exception.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Stacktrace" + }, + { + "type": "null" + } + ] + }, + "thread_id": { + "description": "Identifier of the thread this exception occurred in.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ThreadId" + }, + { + "type": "null" + } + ] + }, + "type": { + "description": "Exception type. One of value or exception is required, checked in StoreNormalizeProcessor", + "default": null, + "type": [ + "string", + "null" + ] + }, + "value": { + "description": "Human readable display value.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/JsonLenientString" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "Fingerprint": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "Frame": { + "anyOf": [ + { + "type": "object", + "properties": { + "abs_path": { + "description": "Absolute path to the source file.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "colno": { + "description": "Column number within the source file.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "context_line": { + "description": "Source code of the current line.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "data": { + "description": "Auxiliary information about the frame that is platform specific.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/FrameData" + }, + { + "type": "null" + } + ] + }, + "filename": { + "description": "The source file name (basename only).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "function": { + "description": "Name of the frame's function. This might include the name of a class.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "image_addr": { + "description": "Start address of the containing code module (image).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "in_app": { + "description": "Override whether this frame should be considered in-app.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "instruction_addr": { + "description": "Absolute address of the frame's CPU instruction.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "lang": { + "description": "The language of the frame if it overrides the stacktrace language.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "lineno": { + "description": "Line number within the source file.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "module": { + "description": "Name of the module the frame is contained in.\n\nNote that this might also include a class name if that is something the language natively considers to be part of the stack (for instance in Java).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "package": { + "description": "Name of the package that contains the frame.\n\nFor instance this can be a dylib for native languages, the name of the jar or .NET assembly.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "platform": { + "description": "Which platform this frame is from.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "post_context": { + "description": "Source code of the lines after the current line.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "pre_context": { + "description": "Source code leading up to the current line.", + "default": null, + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ] + } + }, + "raw_function": { + "description": "A raw (but potentially truncated) function value.\n\nIf this has the same value as `function` it's best to be omitted. This exists because on many platforms the function itself contains additional information like overload specifies or a lot of generics which can make it exceed the maximum limit we provide for the field. In those cases then we cannot reliably trim down the function any more at a later point because the more valuable information has been removed.\n\nThe logic to be applied is that an intelligently trimmed function name should be stored in `function` and the value before trimming is stored in this field instead. However also this field will be capped at 256 characters at the moment which often means that not the entire original value can be stored.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "symbol": { + "description": "Potentially mangled name of the symbol as it appears in an executable.\n\nThis is different from a function name by generally being the mangled name that appears natively in the binary. This is relevant for languages like Swift, C++ or Rust.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "symbol_addr": { + "description": "Start address of the frame's function.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "trust": { + "description": "Used for native crashes to indicate how much we can \"trust\" the instruction_addr", + "default": null, + "type": [ + "string", + "null" + ] + }, + "vars": { + "description": "Local variables in a convenient format.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/FrameVars" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "FrameData": { + "anyOf": [ + { + "type": "object", + "properties": { + "orig_colno": { + "description": "The original column number.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "orig_filename": { + "description": "The original minified filename.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "orig_function": { + "description": "The original function name before it was resolved.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "orig_in_app": { + "description": "The original value of the in_app flag before grouping enhancers ran.\n\nBecause we need to handle more cases the following values are used:\n\n- missing / `null`: information not available - `-1`: in_app was set to `null` - `0`: in_app was set to `false` - `1`: in_app was set to `true`", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "orig_lineno": { + "description": "The original line number.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "sourcemap": { + "description": "A reference to the sourcemap used.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "FrameVars": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + } + ] + }, + "Geo": { + "anyOf": [ + { + "type": "object", + "properties": { + "city": { + "description": "Human readable city name.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "country_code": { + "description": "Two-letter country code (ISO 3166-1 alpha-2).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "region": { + "description": "Human readable region name or code.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "GpuContext": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + } + ] + }, + "HeaderName": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "HeaderValue": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "Headers": { + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/HeaderValue" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "array", + "items": { + "type": [ + "array", + "null" + ], + "items": [ + { + "anyOf": [ + { + "$ref": "#/definitions/HeaderName" + }, + { + "type": "null" + } + ] + }, + { + "anyOf": [ + { + "$ref": "#/definitions/HeaderValue" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + } + ] + }, + "JsonLenientString": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "Level": { + "description": "Severity level of an event or breadcrumb.", + "type": "string", + "enum": [ + "debug", + "info", + "warning", + "error", + "fatal" + ] + }, + "LogEntry": { + "anyOf": [ + { + "type": "object", + "properties": { + "formatted": { + "description": "The formatted message", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Message" + }, + { + "type": "null" + } + ] + }, + "message": { + "description": "The log message with parameter placeholders.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Message" + }, + { + "type": "null" + } + ] + }, + "params": { + "description": "Positional parameters to be interpolated into the log message.", + "default": null + } + } + } + ] + }, + "MachException": { + "anyOf": [ + { + "type": "object", + "properties": { + "code": { + "description": "The mach exception code.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "exception": { + "description": "The mach exception type.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "name": { + "description": "Optional name of the mach exception.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "subcode": { + "description": "The mach exception subcode.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + ] + }, + "Mechanism": { + "anyOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "data": { + "description": "Additional attributes depending on the mechanism type.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "description": { + "description": "Human readable detail description.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "handled": { + "description": "Flag indicating whether this exception was handled.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "help_link": { + "description": "Link to online resources describing this error.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "meta": { + "description": "Operating system or runtime meta information.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/MechanismMeta" + }, + { + "type": "null" + } + ] + }, + "synthetic": { + "description": "If this is set then the exception is not a real exception but some form of synthetic error for instance from a signal handler, a hard segfault or similar where type and value are not useful for grouping or display purposes.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "type": { + "description": "Mechanism type (required).", + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "MechanismMeta": { + "anyOf": [ + { + "type": "object", + "properties": { + "errno": { + "description": "Optional ISO C standard error code.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/CError" + }, + { + "type": "null" + } + ] + }, + "mach_exception": { + "description": "Optional mach exception information.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/MachException" + }, + { + "type": "null" + } + ] + }, + "signal": { + "description": "Optional POSIX signal number.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/PosixSignal" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "Message": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "MonitorContext": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + } + ] + }, + "NativeDebugImage": { + "anyOf": [ + { + "type": "object", + "required": [ + "code_file", + "debug_id", + "image_addr", + "image_size" + ], + "properties": { + "arch": { + "description": "CPU architecture target.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "code_file": { + "description": "Path and name of the image file (required).", + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "code_id": { + "description": "Optional identifier of the code file.\n\nIf not specified, it is assumed to be identical to the debug identifier.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/CodeId" + }, + { + "type": "null" + } + ] + }, + "debug_file": { + "description": "Path and name of the debug companion file (required).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/NativeImagePath" + }, + { + "type": "null" + } + ] + }, + "debug_id": { + "description": "Unique debug identifier of the image.", + "anyOf": [ + { + "$ref": "#/definitions/DebugId" + }, + { + "type": "null" + } + ] + }, + "image_addr": { + "description": "Starting memory address of the image (required).", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "image_size": { + "description": "Size of the image in bytes (required).", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "image_vmaddr": { + "description": "Loading address in virtual memory.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "NativeImagePath": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "OsContext": { + "anyOf": [ + { + "type": "object", + "properties": { + "build": { + "description": "Internal build number of the operating system.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "kernel_version": { + "description": "Current kernel version.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Name of the operating system.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "raw_description": { + "description": "Unprocessed operating system info.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "rooted": { + "description": "Indicator if the OS is rooted (mobile mostly).", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "version": { + "description": "Version of the operating system.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "PosixSignal": { + "anyOf": [ + { + "type": "object", + "properties": { + "code": { + "description": "An optional signal code present on Apple systems.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + }, + "code_name": { + "description": "Optional name of the errno constant.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Optional name of the errno constant.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "number": { + "description": "The POSIX signal number.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + } + ] + }, + "ProguardDebugImage": { + "anyOf": [ + { + "type": "object", + "required": [ + "uuid" + ], + "properties": { + "uuid": { + "description": "UUID computed from the file contents.", + "type": [ + "string", + "null" + ], + "format": "uuid" + } + } + } + ] + }, + "Query": { + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/JsonLenientString" + }, + { + "type": "null" + } + ] + } + }, + { + "type": "array", + "items": { + "type": [ + "array", + "null" + ], + "items": [ + { + "type": [ + "string", + "null" + ] + }, + { + "anyOf": [ + { + "$ref": "#/definitions/JsonLenientString" + }, + { + "type": "null" + } + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + } + ] + } + ] + }, + "RawStacktrace": { + "anyOf": [ + { + "type": "object", + "required": [ + "frames" + ], + "properties": { + "frames": { + "type": [ + "array", + "null" + ], + "items": { + "anyOf": [ + { + "$ref": "#/definitions/Frame" + }, + { + "type": "null" + } + ] + } + }, + "lang": { + "description": "The language of the stacktrace.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "registers": { + "description": "Register values of the thread (top frame).", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/RegVal" + }, + { + "type": "null" + } + ] + } + } + } + } + ] + }, + "RegVal": { + "type": "string" + }, + "Request": { + "anyOf": [ + { + "type": "object", + "properties": { + "cookies": { + "description": "URL encoded contents of the Cookie header.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Cookies" + }, + { + "type": "null" + } + ] + }, + "data": { + "description": "Request data in any format that makes sense.", + "default": null + }, + "env": { + "description": "Server environment data, such as CGI/WSGI.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "fragment": { + "description": "The fragment of the request URL.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "headers": { + "description": "HTTP request headers.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Headers" + }, + { + "type": "null" + } + ] + }, + "inferred_content_type": { + "description": "The inferred content type of the request payload.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "method": { + "description": "HTTP request method.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "query_string": { + "description": "URL encoded HTTP query string.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Query" + }, + { + "type": "null" + } + ] + }, + "url": { + "description": "URL of the request.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "RuntimeContext": { + "anyOf": [ + { + "type": "object", + "properties": { + "build": { + "description": "Application build string, if it is separate from the version.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "Runtime name.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "raw_description": { + "description": "Unprocessed runtime info.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "Runtime version string.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + }, + "Span": { + "anyOf": [ + { + "type": "object", + "required": [ + "span_id", + "start_timestamp", + "timestamp", + "trace_id" + ], + "properties": { + "description": { + "description": "Human readable description of a span (e.g. method URL).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "op": { + "description": "Span type (see `OperationType` docs).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "parent_span_id": { + "description": "The ID of the span enclosing this span.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/SpanId" + }, + { + "type": "null" + } + ] + }, + "span_id": { + "description": "The Span id.", + "anyOf": [ + { + "$ref": "#/definitions/SpanId" + }, + { + "type": "null" + } + ] + }, + "start_timestamp": { + "description": "Timestamp when the span started.", + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "status": { + "description": "The status of a span", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/SpanStatus" + }, + { + "type": "null" + } + ] + }, + "timestamp": { + "description": "Timestamp when the span was ended.", + "anyOf": [ + { + "$ref": "#/definitions/Timestamp" + }, + { + "type": "null" + } + ] + }, + "trace_id": { + "description": "The ID of the trace the span belongs to.", + "anyOf": [ + { + "$ref": "#/definitions/TraceId" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "SpanId": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "SpanStatus": { + "description": "Trace status.\n\nValues from https://github.com/open-telemetry/opentelemetry-specification/blob/8fb6c14e4709e75a9aaa64b0dbbdf02a6067682a/specification/api-tracing.md#status Mapping to HTTP from https://github.com/open-telemetry/opentelemetry-specification/blob/8fb6c14e4709e75a9aaa64b0dbbdf02a6067682a/specification/data-http.md#status", + "type": "string", + "enum": [ + "ok", + "cancelled", + "unknown", + "invalid_argument", + "deadline_exceeded", + "not_found", + "already_exists", + "permission_denied", + "resource_exhausted", + "failed_precondition", + "aborted", + "out_of_range", + "unimplemented", + "internal_error", + "unavailable", + "data_loss", + "unauthenticated" + ] + }, + "Stacktrace": { + "anyOf": [ + { + "$ref": "#/definitions/RawStacktrace" + } + ] + }, + "String": { + "type": "string" + }, + "SystemSdkInfo": { + "anyOf": [ + { + "type": "object", + "properties": { + "sdk_name": { + "description": "The internal name of the SDK.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "version_major": { + "description": "The major version of the SDK as integer or 0.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "version_minor": { + "description": "The minor version of the SDK as integer or 0.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "version_patchlevel": { + "description": "The patch version of the SDK as integer or 0.", + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + ] + }, + "TagEntry": { + "anyOf": [ + { + "type": "array", + "items": [ + { + "type": [ + "string", + "null" + ] + }, + { + "type": [ + "string", + "null" + ] + } + ], + "maxItems": 2, + "minItems": 2 + } + ] + }, + "Tags": { + "anyOf": [ + { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/TagEntry" + }, + { + "type": "null" + } + ] + } + } + ] + } + ] + }, + "Thread": { + "anyOf": [ + { + "type": "object", + "properties": { + "crashed": { + "description": "Indicates that this thread requested the event (usually by crashing).", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "current": { + "description": "Indicates that the thread was not suspended when the event was created.", + "default": null, + "type": [ + "boolean", + "null" + ] + }, + "id": { + "description": "Identifier of this thread within the process (usually an integer).", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/ThreadId" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "Display name of this thread.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "raw_stacktrace": { + "description": "Optional unprocessed stack trace.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/RawStacktrace" + }, + { + "type": "null" + } + ] + }, + "stacktrace": { + "description": "Stack trace containing frames of this exception.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Stacktrace" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "ThreadId": { + "anyOf": [ + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + { + "type": "string" + } + ] + }, + "Timestamp": { + "description": "Can be a ISO-8601 formatted string or a unix timestamp in seconds (floating point values allowed).\n\nMust be UTC.", + "anyOf": [ + { + "type": "number", + "format": "double" + }, + { + "type": "string" + } + ] + }, + "TraceContext": { + "anyOf": [ + { + "type": "object", + "required": [ + "span_id", + "status", + "trace_id" + ], + "properties": { + "op": { + "description": "Span type (see `OperationType` docs).", + "default": null, + "type": [ + "string", + "null" + ] + }, + "parent_span_id": { + "description": "The ID of the span enclosing this span.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/SpanId" + }, + { + "type": "null" + } + ] + }, + "span_id": { + "description": "The ID of the span.", + "anyOf": [ + { + "$ref": "#/definitions/SpanId" + }, + { + "type": "null" + } + ] + }, + "status": { + "description": "Whether the trace failed or succeeded. Currently only used to indicate status of individual transactions.", + "anyOf": [ + { + "$ref": "#/definitions/SpanStatus" + }, + { + "type": "null" + } + ] + }, + "trace_id": { + "description": "The trace ID.", + "anyOf": [ + { + "$ref": "#/definitions/TraceId" + }, + { + "type": "null" + } + ] + } + } + } + ] + }, + "TraceId": { + "anyOf": [ + { + "type": "string" + } + ] + }, + "User": { + "anyOf": [ + { + "type": "object", + "properties": { + "data": { + "description": "Additional arbitrary fields, as stored in the database (and sometimes as sent by clients). All data from `self.other` should end up here after store normalization.", + "default": null, + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "email": { + "description": "Email address of the user.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "geo": { + "description": "Approximate geographical location of the end user or device.", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/Geo" + }, + { + "type": "null" + } + ] + }, + "id": { + "description": "Unique identifier of the user.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "ip_address": { + "description": "Remote IP address of the user. Defaults to \"{{auto}}\".", + "default": null, + "anyOf": [ + { + "$ref": "#/definitions/String" + }, + { + "type": "null" + } + ] + }, + "name": { + "description": "Human readable name of the user.", + "default": null, + "type": [ + "string", + "null" + ] + }, + "username": { + "description": "Username of the user.", + "default": null, + "type": [ + "string", + "null" + ] + } + } + } + ] + } + } +} diff --git a/relay-general/tests/test_fixtures.rs b/relay-general/tests/test_fixtures.rs index 3d24ac06a0..761b2d648d 100644 --- a/relay-general/tests/test_fixtures.rs +++ b/relay-general/tests/test_fixtures.rs @@ -8,6 +8,9 @@ use relay_general::types::{Annotated, SerializableAnnotated}; use insta::assert_yaml_snapshot; +#[cfg(feature = "jsonschema")] +use {insta::assert_json_snapshot, relay_general::protocol::event_json_schema}; + macro_rules! event_snapshot { ($id:ident) => { mod $id { @@ -80,6 +83,17 @@ macro_rules! event_snapshot { ".received" => "[received]", ".timestamp" => "[timestamp]" }); + + #[cfg(feature = "jsonschema")] + { + let event_schema = serde_json::to_value(event_json_schema()).unwrap(); + let event = serde_json::to_value(SerializableAnnotated(&event)).unwrap(); + let mut scope = valico::json_schema::Scope::new(); + let schema = scope.compile_and_return(event_schema, false).unwrap(); + let validation_state = schema.validate(&event); + dbg!(&validation_state.errors); + assert!(validation_state.is_valid()); + } } } } @@ -91,3 +105,9 @@ event_snapshot!(cordova); event_snapshot!(dotnet); event_snapshot!(legacy_python); event_snapshot!(legacy_node_exception); + +#[test] +#[cfg(feature = "jsonschema")] +fn test_event_schema_snapshot() { + assert_json_snapshot!("event_schema", event_json_schema()); +} diff --git a/relay-server/src/actors/events.rs b/relay-server/src/actors/events.rs index 7716cb083c..9ac8404f50 100644 --- a/relay-server/src/actors/events.rs +++ b/relay-server/src/actors/events.rs @@ -17,7 +17,7 @@ use relay_general::pii::PiiProcessor; use relay_general::processor::{process_value, ProcessingState}; use relay_general::protocol::{ Breadcrumb, Csp, Event, EventId, EventType, ExpectCt, ExpectStaple, Hpkp, LenientString, - Metrics, SecurityReportType, SessionUpdate, Values, + Metrics, SecurityReportType, SessionUpdate, Timestamp, Values, }; use relay_general::store::ClockDriftProcessor; use relay_general::types::{Annotated, Array, Object, ProcessingAction, Value}; @@ -764,7 +764,7 @@ impl EventProcessor { } }; let timestamp = Utc.timestamp(minidump.header.time_date_stamp.into(), 0); - event.timestamp.set_value(Some(timestamp)); + event.timestamp.set_value(Some(timestamp.into())); } /// Adds processing placeholders for special attachments. @@ -836,7 +836,7 @@ impl EventProcessor { // should be removed as soon as legacy ingestion has been removed. let sent_at = match envelope.sent_at() { Some(sent_at) => Some(sent_at), - None if is_transaction => event.timestamp.value().copied(), + None if is_transaction => event.timestamp.value().copied().map(Timestamp::into_inner), None => None, }; diff --git a/relay-server/src/actors/store.rs b/relay-server/src/actors/store.rs index c8b57d0f5a..c01f2ee8bb 100644 --- a/relay-server/src/actors/store.rs +++ b/relay-server/src/actors/store.rs @@ -15,8 +15,7 @@ use serde::{ser::Error, Serialize}; use relay_common::{metric, ProjectId, UnixTimestamp, Uuid}; use relay_config::{Config, KafkaTopic}; -use relay_general::protocol::{EventId, SessionStatus, SessionUpdate}; -use relay_general::types; +use relay_general::protocol::{self, EventId, SessionStatus, SessionUpdate}; use relay_quotas::Scoping; use crate::envelope::{AttachmentType, Envelope, Item, ItemType}; @@ -176,8 +175,8 @@ impl StoreForwarder { .map(make_distinct_id) .unwrap_or_default(), seq: if session.init { 0 } else { session.sequence }, - received: types::datetime_to_timestamp(session.timestamp), - started: types::datetime_to_timestamp(session.started), + received: protocol::datetime_to_timestamp(session.timestamp), + started: protocol::datetime_to_timestamp(session.started), duration: session.duration, status: session.status, errors: session diff --git a/relay-server/src/utils/unreal.rs b/relay-server/src/utils/unreal.rs index a00e829b9c..29da85c22d 100644 --- a/relay-server/src/utils/unreal.rs +++ b/relay-server/src/utils/unreal.rs @@ -4,7 +4,8 @@ use symbolic::unreal::{ use relay_general::protocol::{ AsPair, Breadcrumb, Context, Contexts, DeviceContext, Event, EventId, GpuContext, - LenientString, LogEntry, Message, OsContext, TagEntry, Tags, User, UserReport, Values, + LenientString, LogEntry, Message, OsContext, TagEntry, Tags, Timestamp, User, UserReport, + Values, }; use relay_general::types::{self, Annotated, Array, Object, Value}; @@ -97,7 +98,7 @@ fn merge_unreal_logs(event: &mut Event, data: &[u8]) -> Result<(), Unreal4Error> for log in logs { breadcrumbs.push(Annotated::new(Breadcrumb { - timestamp: Annotated::from(log.timestamp), + timestamp: Annotated::from(log.timestamp.map(Timestamp)), category: Annotated::from(log.component), message: Annotated::new(log.message), ..Breadcrumb::default() diff --git a/requirements-doc.txt b/requirements-doc.txt index c6c2d35eaf..0eddf16bf4 100644 --- a/requirements-doc.txt +++ b/requirements-doc.txt @@ -4,3 +4,5 @@ mkdocs-material==4.6.2 mkdocs==1.0.4 pygments==2.5.2 pymdown-extensions==6.3 +mkdocs-macros-plugin==0.4.6 +mkdocs-exclude==1.0.2 diff --git a/src/cli.rs b/src/cli.rs index 79037e86fa..19715770af 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,23 +37,29 @@ pub fn execute() -> Result<(), Error> { let matches = app.get_matches(); let config_path = matches.value_of("config").unwrap_or(".relay"); - // config init is special because it does not yet have a config. + // Commands that do not need to load the config: if let Some(matches) = matches.subcommand_matches("config") { if let Some(matches) = matches.subcommand_matches("init") { return init_config(&config_path, &matches); } - // likewise completions generation does not need the config. } else if let Some(matches) = matches.subcommand_matches("generate-completions") { return generate_completions(&matches); - // we also do not read the config for offline event processing } else if let Some(matches) = matches.subcommand_matches("process-event") { return process_event(&matches); + } else if let Some(_matches) = matches.subcommand_matches("event-json-schema") { + #[cfg(feature = "jsonschema")] + return event_json_schema(&_matches); + + #[cfg(not(feature = "jsonschema"))] + failure::bail!("Relay needs to be compiled with the 'jsonschema' feature for this command to be available."); } + // Commands that need a loaded config: let mut config = Config::from_path(&config_path)?; // override file config with environment variables let env_config = extract_config_env_vars(); config.apply_override(env_config)?; + setup::init_logging(&config); if let Some(matches) = matches.subcommand_matches("config") { manage_config(&config, &matches) @@ -390,6 +396,15 @@ pub fn process_event<'a>(matches: &ArgMatches<'a>) -> Result<(), Error> { Ok(()) } +#[cfg(feature = "jsonschema")] +pub fn event_json_schema<'a>(_matches: &ArgMatches<'a>) -> Result<(), Error> { + serde_json::to_writer( + &mut io::stdout().lock(), + &relay_general::protocol::event_json_schema(), + )?; + Ok(()) +} + pub fn run<'a>(config: Config, _matches: &ArgMatches<'a>) -> Result<(), Error> { setup::dump_spawn_infos(&config); setup::check_config(&config)?; diff --git a/src/cliapp.rs b/src/cliapp.rs index a512982873..224c53e468 100644 --- a/src/cliapp.rs +++ b/src/cliapp.rs @@ -248,6 +248,7 @@ pub fn make_app() -> App<'static, 'static> { ) .subcommand( App::new("process-event") + .setting(AppSettings::Hidden) .about("Processes a single event piped in") .after_help( "This takes an event on stdin and puts the processed event to stdout. \ @@ -277,6 +278,11 @@ pub fn make_app() -> App<'static, 'static> { .help("Run through store normalization"), ), ) + .subcommand( + App::new("event-json-schema") + .about("Dump JSON schema representation of event schema to stdout.") + .setting(AppSettings::Hidden), + ) .subcommand( App::new("generate-completions") .about("Generate shell completion file")