From f656979b96c74aa92dc2939fb6fd76b0443da644 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 18 Apr 2023 11:23:49 +0100 Subject: [PATCH 001/249] cargo new --bin capture --- .gitignore | 1 + Cargo.toml | 8 ++++++++ src/main.rs | 3 +++ 3 files changed, 12 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000..ea8c4bf7f35f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000..7021e12acfb29 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "capture" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000000000..e7a11a969c037 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From cdc04583071c0d1fc453e1da9138ec40396ddad9 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 18 Apr 2023 11:36:27 +0100 Subject: [PATCH 002/249] Add README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000000..cae8981ac9c48 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# capture + +This is a rewrite of [capture.py](https://github.com/PostHog/posthog/blob/master/posthog/api/capture.py), in Rust. + +## Why? + +Capture is very simple. It takes some JSON, checks a key in Redis, and then pushes onto Kafka. It's mostly IO bound. + +We currently use far too much compute to run this service, and it could be more efficient. This effort should not take too long to complete, but should massively reduce our CPU usage - and therefore spend. + +## How? + +I'm trying to ensure the rewrite at least vaguely resembles the Python version. This will both minimize accidental regressions, but also serve as a "rosetta stone" for engineers at PostHog who have not written Rust before. From 86ebcf95b22afe4b9c546e140b71262c44952456 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 18 Apr 2023 11:41:51 +0100 Subject: [PATCH 003/249] Add docker image and workflow --- .github/workflows/docker.yml | 61 ++++++++++++++++++++++++++++++++++++ Dockerfile | 26 +++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000000000..f6b8ebb7bb44a --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,61 @@ +name: Build docker image + +on: + push: + branches: + - 'main' + +permissions: + packages: write + +jobs: + build: + name: build and publish capture image + runs-on: ubuntu-latest + steps: + + - name: Check Out Repo + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v4 + with: + context: ./ + file: ./Dockerfile + builder: ${{ steps.buildx.outputs.name }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/amd64, linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: RUST_BACKTRACE=1 + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000..f9e1ce8920971 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM lukemathwalker/cargo-chef:latest-rust-1.68.0 AS chef +WORKDIR app + +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder + +# Ensure working C compile setup (not installed by default in arm64 images) +RUN apt update && apt install build-essential -y + +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json + +COPY . . +RUN cargo build --release --bin capture + +FROM debian:bullseye-20230320-slim AS runtime + +WORKDIR app + +USER nobody + +COPY --from=builder /app/target/release/capture /usr/local/bin +ENTRYPOINT ["/usr/local/bin/capture"] From d03328a7056d19731c8bf2cc09d81aa972789ee8 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 18 Apr 2023 12:02:36 +0100 Subject: [PATCH 004/249] Add Rust workflow and lockfile --- .github/workflows/rust.yml | 96 ++++++++++++++++++++++++++++++++++++++ Cargo.lock | 7 +++ 2 files changed, 103 insertions(+) create mode 100644 .github/workflows/rust.yml create mode 100644 Cargo.lock diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000000000..97d1b9642c203 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,96 @@ +name: Rust + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo build + run: cargo build --all --locked --release && strip target/release/capture + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${ runner.os }-cargo-debug-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo test + run: cargo test --all-features + + - name: Run cargo check + run: cargo check --all-features + + clippy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install latest rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy + + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} + + - name: Run clippy + run: cargo clippy -- -D warnings + + format: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install latest rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt + + - name: Format + run: cargo fmt -- --check diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000000..dcaf2fce1e801 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "capture" +version = "0.1.0" From bb556b5ec3bb6f3f2ac0a7822bf9fb87d1081ed9 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 18 Apr 2023 12:26:32 +0100 Subject: [PATCH 005/249] Add basic server (#3) * Add dependencies Tokio for async Axum for HTTP things Tracing for... tracing Serde for serialization * Setup simple hello world server * fmt --- Cargo.lock | 774 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 + src/capture.rs | 3 + src/main.rs | 24 +- 4 files changed, 805 insertions(+), 2 deletions(-) create mode 100644 src/capture.rs diff --git a/Cargo.lock b/Cargo.lock index dcaf2fce1e801..eef95c83bc083 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,780 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "async-trait" +version = "0.1.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b32c5ea3aabaf4deb5f5ced2d688ec0844c881c9e6c696a8b769a05fc691e62" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + [[package]] name = "capture" version = "0.1.0" +dependencies = [ + "axum", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tokio" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +dependencies = [ + "autocfg", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml index 7021e12acfb29..fc2f48689b4b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = "0.6.15" +tokio = { version = "1.0", features = ["full"] } +tracing = "0.1" +tracing-subscriber = "0.3" +serde = { version = "1.0.160", features = ["derive"] } +serde_json = "1.0.96" diff --git a/src/capture.rs b/src/capture.rs new file mode 100644 index 0000000000000..8f5f0d9cb154a --- /dev/null +++ b/src/capture.rs @@ -0,0 +1,3 @@ +pub async fn root() -> &'static str { + "Hello, World!" +} diff --git a/src/main.rs b/src/main.rs index e7a11a969c037..fefb0fabdbba8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,23 @@ -fn main() { - println!("Hello, world!"); +use axum::{routing::get, Router}; +use std::net::SocketAddr; + +mod capture; + +#[tokio::main] +async fn main() { + // initialize tracing + tracing_subscriber::fmt::init(); + + let app = Router::new().route("/", get(capture::root)); + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + + tracing::debug!("listening on {}", addr); + + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .await + .unwrap(); } From 585ac6654aaeb8ae955abf0434bf51908b89522b Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 16 May 2023 15:23:05 +0100 Subject: [PATCH 006/249] Add token shape checking (#4) * Add basic server (#3) * Add dependencies Tokio for async Axum for HTTP things Tracing for... tracing Serde for serialization * Setup simple hello world server * fmt * Server coming up, validating tokens * Spent ages trying to figure out the schema, which is currenly very fluid --- Cargo.lock | 545 +++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 + bin/send_event.sh | 2 + src/api.rs | 22 ++ src/capture.rs | 26 ++- src/main.rs | 14 +- src/token.rs | 99 +++++++++ src/utils.rs | 4 + 8 files changed, 714 insertions(+), 5 deletions(-) create mode 100755 bin/send_event.sh create mode 100644 src/api.rs create mode 100644 src/token.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index eef95c83bc083..1da53544d855d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +dependencies = [ + "memchr", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -74,6 +83,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" + [[package]] name = "bytes" version = "1.4.0" @@ -85,9 +100,14 @@ name = "capture" version = "0.1.0" dependencies = [ "axum", + "governor", + "mockall", "serde", "serde_json", + "time", "tokio", + "tower-http", + "tower_governor", "tracing", "tracing-subscriber", ] @@ -98,6 +118,55 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -113,6 +182,37 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -120,6 +220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -128,24 +229,105 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + [[package]] name = "futures-task" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "governor" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "quanta", + "rand", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hermit-abi" version = "0.2.6" @@ -177,6 +359,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + [[package]] name = "httparse" version = "1.8.0" @@ -212,12 +400,30 @@ dependencies = [ "want", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -249,6 +455,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "matchit" version = "0.7.0" @@ -275,10 +490,61 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -289,6 +555,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -372,6 +647,42 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.56" @@ -381,6 +692,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quanta" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" +dependencies = [ + "crossbeam-utils", + "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.2+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.26" @@ -390,6 +717,45 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -399,6 +765,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + [[package]] name = "rustversion" version = "1.0.12" @@ -487,6 +870,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -531,6 +923,32 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "thread_local" version = "1.1.7" @@ -541,6 +959,22 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "tokio" version = "1.27.0" @@ -587,6 +1021,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.2" @@ -599,6 +1052,26 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +[[package]] +name = "tower_governor" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6be418f6d18863291f0a7fa1da1de71495a19a54b5fb44969136f731a47e86" +dependencies = [ + "axum", + "forwarded-header-value", + "futures", + "futures-core", + "governor", + "http", + "pin-project", + "thiserror", + "tokio", + "tower", + "tower-layer", + "tracing", +] + [[package]] name = "tracing" version = "0.1.37" @@ -686,12 +1159,82 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "web-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index fc2f48689b4b5..c5b992c62c914 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,10 @@ tracing = "0.1" tracing-subscriber = "0.3" serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" +governor = "0.5.1" +tower_governor = "0.0.4" +time = "0.3.20" +tower-http = {version = "0.4.0", features = ["trace"]} + +[dev-dependencies] +mockall = "0.11.2" diff --git a/bin/send_event.sh b/bin/send_event.sh new file mode 100755 index 0000000000000..4885cf12db376 --- /dev/null +++ b/bin/send_event.sh @@ -0,0 +1,2 @@ +# Send an event to a test server +curl http://localhost:3000/capture -X POST -H "Content-Type: application/json" --data '{"token": "ferrisisbae"}' diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000000000..f647afacf0440 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +// Define the API interface for capture here. +// This is used for serializing responses and deserializing requests + +// LATER ME +// Trying to figure out wtf the schema for this is. Turns out we have about +// a million special cases and properties all over the place + +Also - what are the possible types for a property value? Account for those. +#[derive(Debug, Deserialize, Serialize)] +pub struct CaptureRequest{ + #[serde(alias = "$token", alias = "api_key")] + pub token: String, + + pub event: String, + pub properties: HashMap +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CaptureResponse{} diff --git a/src/capture.rs b/src/capture.rs index 8f5f0d9cb154a..db079d1fefa47 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,3 +1,25 @@ -pub async fn root() -> &'static str { - "Hello, World!" +use axum::{Json, http::StatusCode}; +use crate::{api::{CaptureRequest, CaptureResponse}, token}; + +/// A single event +/// Does not yet support everything the old method does - we expect to be able to deserialize the +/// entire POST body, and not keep checking for form attributes. +/// +/// TODO: Switch on this between two handlers. DO NOT branch in the code. +/// TODO: Add error responses in the same format as capture.py. Probs custom extractor. +pub async fn event(req: Json) -> Result, (StatusCode, String)> { + tracing::info!("new event of type {}", req.token); + + // I wanted to do some sort of middleware that pulled the token out of the headers, but... The + // token isn't usually in the headers, but in the body :( + // Could move token parsing into the middleware at some point + if let Err(invalid) = token::validate_token(req.token.as_str()){ + return Err((StatusCode::BAD_REQUEST, invalid.reason().to_string())); + } + + Ok(Json(CaptureResponse{})) } + +// A group of events! There is no limit here, though our HTTP stack will reject anything above +// 20mb. +pub async fn batch() -> &'static str {"No batching for you!"} diff --git a/src/main.rs b/src/main.rs index fefb0fabdbba8..422c588bdfba2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,24 @@ -use axum::{routing::get, Router}; +use axum::{ + routing::get, + Router, +}; + +use axum::{routing::get, routing::post, Router}; use std::net::SocketAddr; +use tower_http::trace::TraceLayer; mod capture; +mod utils; +mod api; +mod token; #[tokio::main] async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - let app = Router::new().route("/", get(capture::root)); + let app = Router::new().route("/capture", post(capture::event)).route("/batch", post(capture::batch)).layer(TraceLayer::new_for_http()); + // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/src/token.rs b/src/token.rs new file mode 100644 index 0000000000000..133a3c44f8a11 --- /dev/null +++ b/src/token.rs @@ -0,0 +1,99 @@ +use std::error::Error; +use std::fmt::Display; + +/// Validate that a token is the correct shape + +#[derive(Debug, PartialEq)] +pub enum InvalidTokenReason { + IsEmpty, + + // ignoring for now, as serde and the type system save us but we need to error properly + IsNotString, + + IsTooLong, + IsNotAscii, + IsPersonalApiKey +} + +impl InvalidTokenReason { + pub fn reason(&self) -> &str { + match *self { + Self::IsEmpty => "empty", + Self::IsNotAscii => "not_ascii", + Self::IsNotString => "not_string", + Self::IsTooLong => "too_long", + Self::IsPersonalApiKey => "personal_api_key", + } + } +} + +impl Display for InvalidTokenReason{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.reason()) + } +} + +impl Error for InvalidTokenReason { + fn description(&self) -> &str { + self.reason() + } +} + +/// Check if a token is the right shape. It may not actually be a valid token! We don't validate +/// these at the edge yet. +pub fn validate_token(token: &str) -> Result<(), InvalidTokenReason> { + if token.is_empty() { + return Err(InvalidTokenReason::IsEmpty); + } + + if token.len() > 64 { + return Err(InvalidTokenReason::IsTooLong); + } + + if !token.is_ascii() { + return Err(InvalidTokenReason::IsNotAscii); + } + + if token.starts_with("phx_") { + return Err(InvalidTokenReason::IsPersonalApiKey); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::token::{validate_token, InvalidTokenReason}; + + #[test] + fn blocks_empty_tokens() { + let valid = validate_token(""); + + assert!(valid.is_err()); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsEmpty); + } + + #[test] + fn blocks_too_long_tokens() { + let valid = validate_token("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + + assert!(valid.is_err()); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsTooLong); + } + + #[test] + fn blocks_invalid_ascii() { + let valid = validate_token("🦀"); + + assert!(valid.is_err()); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsNotAscii); + } + + #[test] + fn blocks_personal_api_key() { + let valid = validate_token("phx_hellothere"); + + assert!(valid.is_err()); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsPersonalApiKey); + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000000000..1dad9ba3dd46a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,4 @@ +/// This is translated from posthog/utils.py +/// It might make more sense to do it as a middleware, but keep it here for now +/// as an example +pub fn cors_response(){} From be03ad45fcd92db93708da0e4b901fd1c3fe30f2 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 16 May 2023 15:51:41 +0100 Subject: [PATCH 007/249] Fix typo (#5) * Fix typo * Format * I really messed up rebasing on the plane huh * Clippy is now a happy bunny * Fmt --- src/api.rs | 11 ++++------- src/capture.rs | 19 +++++++++++++------ src/main.rs | 16 ++++++---------- src/token.rs | 44 ++++++++++++++++++++++---------------------- src/utils.rs | 4 ---- 5 files changed, 45 insertions(+), 49 deletions(-) delete mode 100644 src/utils.rs diff --git a/src/api.rs b/src/api.rs index f647afacf0440..82770f20ca0f1 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use serde::{Deserialize, Serialize}; // Define the API interface for capture here. // This is used for serializing responses and deserializing requests @@ -7,16 +5,15 @@ use serde::{Deserialize, Serialize}; // LATER ME // Trying to figure out wtf the schema for this is. Turns out we have about // a million special cases and properties all over the place - -Also - what are the possible types for a property value? Account for those. +// Also - what are the possible types for a property value? Account for those. #[derive(Debug, Deserialize, Serialize)] -pub struct CaptureRequest{ +pub struct CaptureRequest { #[serde(alias = "$token", alias = "api_key")] pub token: String, pub event: String, - pub properties: HashMap + // pub properties: HashMap, } #[derive(Debug, Deserialize, Serialize)] -pub struct CaptureResponse{} +pub struct CaptureResponse {} diff --git a/src/capture.rs b/src/capture.rs index db079d1fefa47..a77061089088e 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,5 +1,8 @@ -use axum::{Json, http::StatusCode}; -use crate::{api::{CaptureRequest, CaptureResponse}, token}; +use crate::{ + api::{CaptureRequest, CaptureResponse}, + token, +}; +use axum::{http::StatusCode, Json}; /// A single event /// Does not yet support everything the old method does - we expect to be able to deserialize the @@ -7,19 +10,23 @@ use crate::{api::{CaptureRequest, CaptureResponse}, token}; /// /// TODO: Switch on this between two handlers. DO NOT branch in the code. /// TODO: Add error responses in the same format as capture.py. Probs custom extractor. -pub async fn event(req: Json) -> Result, (StatusCode, String)> { +pub async fn event( + req: Json, +) -> Result, (StatusCode, String)> { tracing::info!("new event of type {}", req.token); // I wanted to do some sort of middleware that pulled the token out of the headers, but... The // token isn't usually in the headers, but in the body :( // Could move token parsing into the middleware at some point - if let Err(invalid) = token::validate_token(req.token.as_str()){ + if let Err(invalid) = token::validate_token(req.token.as_str()) { return Err((StatusCode::BAD_REQUEST, invalid.reason().to_string())); } - Ok(Json(CaptureResponse{})) + Ok(Json(CaptureResponse {})) } // A group of events! There is no limit here, though our HTTP stack will reject anything above // 20mb. -pub async fn batch() -> &'static str {"No batching for you!"} +pub async fn batch() -> &'static str { + "No batching for you!" +} diff --git a/src/main.rs b/src/main.rs index 422c588bdfba2..865a1b865f62c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,9 @@ -use axum::{ - routing::get, - Router, -}; - -use axum::{routing::get, routing::post, Router}; +use axum::{routing::post, Router}; use std::net::SocketAddr; use tower_http::trace::TraceLayer; -mod capture; -mod utils; mod api; +mod capture; mod token; #[tokio::main] @@ -17,8 +11,10 @@ async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - let app = Router::new().route("/capture", post(capture::event)).route("/batch", post(capture::batch)).layer(TraceLayer::new_for_http()); - + let app = Router::new() + .route("/capture", post(capture::event)) + .route("/batch", post(capture::batch)) + .layer(TraceLayer::new_for_http()); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/src/token.rs b/src/token.rs index 133a3c44f8a11..7924cc9511485 100644 --- a/src/token.rs +++ b/src/token.rs @@ -5,29 +5,28 @@ use std::fmt::Display; #[derive(Debug, PartialEq)] pub enum InvalidTokenReason { - IsEmpty, + Empty, // ignoring for now, as serde and the type system save us but we need to error properly - IsNotString, - - IsTooLong, - IsNotAscii, - IsPersonalApiKey + // IsNotString, + TooLong, + NotAscii, + PersonalApiKey, } impl InvalidTokenReason { pub fn reason(&self) -> &str { match *self { - Self::IsEmpty => "empty", - Self::IsNotAscii => "not_ascii", - Self::IsNotString => "not_string", - Self::IsTooLong => "too_long", - Self::IsPersonalApiKey => "personal_api_key", + Self::Empty => "empty", + Self::NotAscii => "not_ascii", + // Self::IsNotString => "not_string", + Self::TooLong => "too_long", + Self::PersonalApiKey => "personal_api_key", } } } -impl Display for InvalidTokenReason{ +impl Display for InvalidTokenReason { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.reason()) } @@ -43,19 +42,19 @@ impl Error for InvalidTokenReason { /// these at the edge yet. pub fn validate_token(token: &str) -> Result<(), InvalidTokenReason> { if token.is_empty() { - return Err(InvalidTokenReason::IsEmpty); + return Err(InvalidTokenReason::Empty); } - + if token.len() > 64 { - return Err(InvalidTokenReason::IsTooLong); + return Err(InvalidTokenReason::TooLong); } if !token.is_ascii() { - return Err(InvalidTokenReason::IsNotAscii); + return Err(InvalidTokenReason::NotAscii); } if token.starts_with("phx_") { - return Err(InvalidTokenReason::IsPersonalApiKey); + return Err(InvalidTokenReason::PersonalApiKey); } Ok(()) @@ -70,15 +69,16 @@ mod tests { let valid = validate_token(""); assert!(valid.is_err()); - assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsEmpty); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::Empty); } #[test] fn blocks_too_long_tokens() { - let valid = validate_token("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); + let valid = + validate_token("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"); assert!(valid.is_err()); - assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsTooLong); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::TooLong); } #[test] @@ -86,7 +86,7 @@ mod tests { let valid = validate_token("🦀"); assert!(valid.is_err()); - assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsNotAscii); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::NotAscii); } #[test] @@ -94,6 +94,6 @@ mod tests { let valid = validate_token("phx_hellothere"); assert!(valid.is_err()); - assert_eq!(valid.unwrap_err(), InvalidTokenReason::IsPersonalApiKey); + assert_eq!(valid.unwrap_err(), InvalidTokenReason::PersonalApiKey); } } diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 1dad9ba3dd46a..0000000000000 --- a/src/utils.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// This is translated from posthog/utils.py -/// It might make more sense to do it as a middleware, but keep it here for now -/// as an example -pub fn cors_response(){} From 96b9d59a7d8152eccd7f1ce240f88a88281c1869 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 22 May 2023 14:53:17 +0100 Subject: [PATCH 008/249] Add event decompress and deserialize (#6) * Add event decompress and deserialize * Try buildjet * Get api key from properties if not on event * fmt * fmt again * All events have the same token --- .github/workflows/rust.yml | 8 +-- Cargo.lock | 50 ++++++++++++++++ Cargo.toml | 4 ++ src/api.rs | 11 ++-- src/capture.rs | 118 ++++++++++++++++++++++++++++++++----- src/event.rs | 69 ++++++++++++++++++++++ src/main.rs | 1 + 7 files changed, 235 insertions(+), 26 deletions(-) create mode 100644 src/event.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 97d1b9642c203..dbb689dc8e72a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,7 +11,7 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 @@ -33,7 +33,7 @@ jobs: run: cargo build --all --locked --release && strip target/release/capture test: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 @@ -58,7 +58,7 @@ jobs: run: cargo check --all-features clippy: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 @@ -81,7 +81,7 @@ jobs: run: cargo clippy -- -D warnings format: - runs-on: ubuntu-latest + runs-on: buildjet-4vcpu-ubuntu-2204 steps: - uses: actions/checkout@v3 diff --git a/Cargo.lock b/Cargo.lock index 1da53544d855d..5b42fb031ec7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.0.1" @@ -11,6 +17,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anyhow" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" + [[package]] name = "async-trait" version = "0.1.68" @@ -77,6 +89,12 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base64" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" + [[package]] name = "bitflags" version = "1.3.2" @@ -99,7 +117,11 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" name = "capture" version = "0.1.0" dependencies = [ + "anyhow", "axum", + "base64", + "bytes", + "flate2", "governor", "mockall", "serde", @@ -118,6 +140,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.15" @@ -158,6 +189,16 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -482,6 +523,15 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.6" diff --git a/Cargo.toml b/Cargo.toml index c5b992c62c914..4de875b24442f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ governor = "0.5.1" tower_governor = "0.0.4" time = "0.3.20" tower-http = {version = "0.4.0", features = ["trace"]} +bytes = "1" +anyhow = "1.0" +flate2 = "1.0" +base64 = "0.21.1" [dev-dependencies] mockall = "0.11.2" diff --git a/src/api.rs b/src/api.rs index 82770f20ca0f1..0b3b3ed3e248f 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,18 +1,15 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; -// Define the API interface for capture here. -// This is used for serializing responses and deserializing requests +use serde_json::Value; -// LATER ME -// Trying to figure out wtf the schema for this is. Turns out we have about -// a million special cases and properties all over the place -// Also - what are the possible types for a property value? Account for those. #[derive(Debug, Deserialize, Serialize)] pub struct CaptureRequest { #[serde(alias = "$token", alias = "api_key")] pub token: String, pub event: String, - // pub properties: HashMap, + pub properties: HashMap, } #[derive(Debug, Deserialize, Serialize)] diff --git a/src/capture.rs b/src/capture.rs index a77061089088e..b13613e6d5bfb 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,32 +1,120 @@ +use std::collections::HashSet; + +use bytes::Bytes; + +use axum::{http::StatusCode, Json}; +// TODO: stream this instead +use axum::extract::Query; + use crate::{ - api::{CaptureRequest, CaptureResponse}, + api::CaptureResponse, + event::{Event, EventQuery}, token, }; -use axum::{http::StatusCode, Json}; -/// A single event -/// Does not yet support everything the old method does - we expect to be able to deserialize the -/// entire POST body, and not keep checking for form attributes. -/// -/// TODO: Switch on this between two handlers. DO NOT branch in the code. -/// TODO: Add error responses in the same format as capture.py. Probs custom extractor. pub async fn event( - req: Json, + meta: Query, + body: Bytes, ) -> Result, (StatusCode, String)> { - tracing::info!("new event of type {}", req.token); + let events = Event::from_bytes(&meta, body); + + let events = match events { + Ok(events) => events, + Err(_) => { + return Err(( + StatusCode::BAD_REQUEST, + String::from("Failed to decode event"), + )) + } + }; + + if events.is_empty() { + return Err((StatusCode::BAD_REQUEST, String::from("No events in batch"))); + } + + let processed = process_events(&events); - // I wanted to do some sort of middleware that pulled the token out of the headers, but... The - // token isn't usually in the headers, but in the body :( - // Could move token parsing into the middleware at some point - if let Err(invalid) = token::validate_token(req.token.as_str()) { - return Err((StatusCode::BAD_REQUEST, invalid.reason().to_string())); + if let Err(msg) = processed { + return Err((StatusCode::BAD_REQUEST, msg)); } Ok(Json(CaptureResponse {})) } +pub fn process_events(events: &[Event]) -> Result<(), String> { + let mut distinct_tokens = HashSet::new(); + + // 1. Tokens are all valid + for event in events { + let token = event.token.clone().unwrap_or_else(|| { + event + .properties + .get("token") + .map_or(String::new(), |t| String::from(t.as_str().unwrap())) + }); + + if let Err(invalid) = token::validate_token(token.as_str()) { + return Err(invalid.reason().to_string()); + } + + distinct_tokens.insert(token); + } + + if distinct_tokens.len() > 1 { + return Err(String::from("Number of distinct tokens in batch > 1")); + } + + Ok(()) +} + // A group of events! There is no limit here, though our HTTP stack will reject anything above // 20mb. pub async fn batch() -> &'static str { "No batching for you!" } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use serde_json::json; + + use super::process_events; + use crate::event::Event; + + #[test] + fn all_events_have_same_token() { + let events = vec![ + Event { + token: Some(String::from("hello")), + event: String::new(), + properties: HashMap::new(), + }, + Event { + token: None, + event: String::new(), + properties: HashMap::from([(String::from("token"), json!("hello"))]), + }, + ]; + + assert_eq!(process_events(&events).is_ok(), true); + } + + #[test] + fn all_events_have_different_token() { + let events = vec![ + Event { + token: Some(String::from("hello")), + event: String::new(), + properties: HashMap::new(), + }, + Event { + token: None, + event: String::new(), + properties: HashMap::from([(String::from("token"), json!("goodbye"))]), + }, + ]; + + assert_eq!(process_events(&events).is_err(), true); + } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000000000..57a34289aed59 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use anyhow::Result; +use bytes::{Buf, Bytes}; +use flate2::read::GzDecoder; + +#[derive(Deserialize, Default)] +pub enum Compression { + #[default] + #[serde(rename = "gzip-js")] + GzipJs, +} + +#[allow(dead_code)] // until they are used +#[derive(Deserialize, Default)] +pub struct EventQuery { + compression: Compression, + + #[serde(alias = "ver")] + version: String, + + #[serde(alias = "_")] + sent_at: i64, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct Event { + #[serde(alias = "$token", alias = "api_key")] + pub token: Option, + + pub event: String, + pub properties: HashMap, +} + +impl Event { + /// We post up _at least one_ event, so when decompressiong and deserializing there + /// could be more than one. Hence this function has to return a Vec. + /// TODO: Use an axum extractor for this + pub fn from_bytes(_: &EventQuery, bytes: Bytes) -> Result> { + let d = GzDecoder::new(bytes.reader()); + let event = serde_json::from_reader(d)?; + + Ok(event) + } +} + +#[cfg(test)] +mod tests { + use base64::Engine as _; + use bytes::Bytes; + + use super::{Event, EventQuery}; + + #[test] + fn decode_bytes() { + let horrible_blob = "H4sIAAAAAAAAA31T207cMBD9lSrikSy+5bIrVX2g4oWWUlEqBEKRY08Sg4mD4+xCEf/e8XLZBSGeEp+ZOWOfmXPxkMAS+pAskp1BtmBBLiHZTQbvBvDBwJgsHpIdh5/kp1Rffp18OcMwAtUS/GhcjwFKZjSbkYjX3q1G8AgeGA+Nu4ughqVRUIX7ATDwHcbr4IYYUJP32LyavMVAF8Kw2NuzTknbuTEsSkIIHlvTf+vhLnzdizUxgslvs2JgkKHr5U1s8VS0dZ/NZSnlW7CVfTvhs7EG+vT0JJaMygP0VQem7bDTvBAbcGV06JAkIwTBpYHV4Hx4zS1FJH+FX7IFj7A1NbZZQR2b4GFbwFlWzFjETY/XCpXRiN538yt/S9mdnm7bSa+lDCY+kOalKDJGs/msZMVuos0YTK+e62hZciHqes7LnDcpoVmTg+TAaqnKMhWUaaa4TllBoCDpJn2uYK3k87xeyFjZFHWdzxmdq5Q0IstBzRXlDMiHbM/5kgnerKfs+tFZqHAolQflvDZ9W0Evawu6wveiENVoND4s+Ami2jBGZbayn/42g3xblizX4skp4FYMYfJQoSQf8DfSjrGBVMEsoWpArpMbK1vc8ItLDG1j1SDvrZM6muBxN/Eg7U1cVFw70KmyRl13bhqjYeBGGrtuFqWTSzzF/q8tRyvV9SfxHXQLoBuidXY0ekeF+KQnNCqgHXaIy7KJBncNERk6VUFhhB33j8zv5uhQ/rCTvbq9/9seH5Pj3Bf/TsuzYf9g2j+3h9N6yZ8Vfpmx4KSguSY5S0lOqc5LmgmhidoMmOaixoFvktFKOo9kK9Nrt3rPxViWk5RwIhtJykZzXohP2DjmZ08+bnH/4B1fkUnGSp2SMmNlIYTguS5ga//eERZZTSVeD8cWPTMGeTMgHSOMpyRLGftDyUKwBV9b6Dx5vPwPzQHjFwsFAAA="; + let decoded_horrible_blob = base64::engine::general_purpose::STANDARD + .decode(horrible_blob) + .unwrap(); + + let bytes = Bytes::from(decoded_horrible_blob); + let events = Event::from_bytes(&EventQuery::default(), bytes); + + assert_eq!(events.is_ok(), true); + } +} diff --git a/src/main.rs b/src/main.rs index 865a1b865f62c..52e1e9efa3dbd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use tower_http::trace::TraceLayer; mod api; mod capture; +mod event; mod token; #[tokio::main] From 3271f8f1dfda351e98dd423d74911f947fa61b8c Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 22 May 2023 15:14:07 +0100 Subject: [PATCH 009/249] Expose router so we can test it (#8) --- src/main.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 52e1e9efa3dbd..08d1b775b8a56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,15 +7,19 @@ mod capture; mod event; mod token; +pub fn router() -> Router { + Router::new() + .route("/capture", post(capture::event)) + .route("/batch", post(capture::batch)) + .layer(TraceLayer::new_for_http()) +} + #[tokio::main] async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - let app = Router::new() - .route("/capture", post(capture::event)) - .route("/batch", post(capture::batch)) - .layer(TraceLayer::new_for_http()); + let app = router(); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` From 605e9978cf76479d4d6d3eae71fc5d12dbcad3f3 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 22 May 2023 15:19:19 +0100 Subject: [PATCH 010/249] Make router a module (#9) --- src/main.rs | 12 ++---------- src/router.rs | 10 ++++++++++ 2 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 src/router.rs diff --git a/src/main.rs b/src/main.rs index 08d1b775b8a56..3fcd2574b7aef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,17 @@ -use axum::{routing::post, Router}; use std::net::SocketAddr; -use tower_http::trace::TraceLayer; mod api; mod capture; mod event; +mod router; mod token; -pub fn router() -> Router { - Router::new() - .route("/capture", post(capture::event)) - .route("/batch", post(capture::batch)) - .layer(TraceLayer::new_for_http()) -} - #[tokio::main] async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - let app = router(); + let app = router::router(); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000000000..db838ea0ca3f4 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,10 @@ +use crate::capture; +use axum::{routing::post, Router}; +use tower_http::trace::TraceLayer; + +pub fn router() -> Router { + Router::new() + .route("/capture", post(capture::event)) + .route("/batch", post(capture::batch)) + .layer(TraceLayer::new_for_http()) +} From 0892ff6428544b0ac612d4b39edb68a97b80973c Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 22 May 2023 16:35:29 +0200 Subject: [PATCH 011/249] Add ProcessedEvent draft, export modules for testing (#10) * add django_compat test skeleton * add ProcessedEvent * remove tests * wip * fmt --- Cargo.lock | 11 +++++++++++ Cargo.toml | 3 ++- src/event.rs | 13 +++++++++++++ src/lib.rs | 5 +++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5b42fb031ec7b..ae3dd836bd117 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,6 +132,7 @@ dependencies = [ "tower_governor", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -1193,6 +1194,16 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "uuid" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +dependencies = [ + "getrandom", + "serde", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 4de875b24442f..74b6872d5eaf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,12 @@ serde_json = "1.0.96" governor = "0.5.1" tower_governor = "0.0.4" time = "0.3.20" -tower-http = {version = "0.4.0", features = ["trace"]} +tower-http = { version = "0.4.0", features = ["trace"] } bytes = "1" anyhow = "1.0" flate2 = "1.0" base64 = "0.21.1" +uuid = { version = "1.3.3", features = ["serde", "v4"] } [dev-dependencies] mockall = "0.11.2" diff --git a/src/event.rs b/src/event.rs index 57a34289aed59..deed63578e00b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -6,6 +6,7 @@ use serde_json::Value; use anyhow::Result; use bytes::{Buf, Bytes}; use flate2::read::GzDecoder; +use uuid::Uuid; #[derive(Deserialize, Default)] pub enum Compression { @@ -47,6 +48,18 @@ impl Event { } } +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct ProcessedEvent { + uuid: Uuid, + distinct_id: String, + ip: String, + site_url: String, + data: String, + now: String, + sent_at: String, + token: String, +} + #[cfg(test)] mod tests { use base64::Engine as _; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000000..b07a9eb167f1d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +mod api; +mod capture; +pub mod event; +pub mod router; +mod token; From c6f5aa3d58cfa4d59dfd81fc8d04e781ec1771b8 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 22 May 2023 17:31:41 +0200 Subject: [PATCH 012/249] Add django_compat test skeleton (#7) --- Cargo.lock | 225 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/api.rs | 9 +- src/capture.rs | 11 +- src/event.rs | 29 +++-- src/router.rs | 9 +- tests/django_compat.rs | 57 ++++++++++ tests/requests_dump.jsonl | 21 ++++ 8 files changed, 344 insertions(+), 18 deletions(-) create mode 100644 tests/django_compat.rs create mode 100644 tests/requests_dump.jsonl diff --git a/Cargo.lock b/Cargo.lock index ae3dd836bd117..98ca254f338bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-test-helper" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d349b3174ceac58442ea1f768233c817e59447c0343be2584fca9f0ed71d3a" +dependencies = [ + "axum", + "bytes", + "http", + "http-body", + "hyper", + "reqwest", + "serde", + "tokio", + "tower", + "tower-service", +] + [[package]] name = "base64" version = "0.21.1" @@ -119,6 +137,7 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "axum-test-helper", "base64", "bytes", "flate2", @@ -190,6 +209,15 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + [[package]] name = "flate2" version = "1.0.26" @@ -364,6 +392,25 @@ dependencies = [ "smallvec", ] +[[package]] +name = "h2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -429,6 +476,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "httparse", @@ -442,6 +490,32 @@ dependencies = [ "want", ] +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" + [[package]] name = "itertools" version = "0.10.5" @@ -524,6 +598,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -833,6 +917,43 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -1026,6 +1147,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.27.0" @@ -1056,6 +1192,20 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -1188,12 +1338,47 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "uuid" version = "1.3.3" @@ -1210,6 +1395,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.0" @@ -1257,6 +1448,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.84" @@ -1286,6 +1489,19 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.61" @@ -1383,3 +1599,12 @@ name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml index 74b6872d5eaf6..5f73604d06415 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,5 @@ base64 = "0.21.1" uuid = { version = "1.3.3", features = ["serde", "v4"] } [dev-dependencies] +axum-test-helper = "0.2.0" mockall = "0.11.2" diff --git a/src/api.rs b/src/api.rs index 0b3b3ed3e248f..b3a18e696c105 100644 --- a/src/api.rs +++ b/src/api.rs @@ -13,4 +13,11 @@ pub struct CaptureRequest { } #[derive(Debug, Deserialize, Serialize)] -pub struct CaptureResponse {} +pub enum CaptureResponseCode { + Ok = 1, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct CaptureResponse { + pub status: CaptureResponseCode, +} diff --git a/src/capture.rs b/src/capture.rs index b13613e6d5bfb..42b2718b29799 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -6,6 +6,7 @@ use axum::{http::StatusCode, Json}; // TODO: stream this instead use axum::extract::Query; +use crate::api::CaptureResponseCode; use crate::{ api::CaptureResponse, event::{Event, EventQuery}, @@ -38,7 +39,9 @@ pub async fn event( return Err((StatusCode::BAD_REQUEST, msg)); } - Ok(Json(CaptureResponse {})) + Ok(Json(CaptureResponse { + status: CaptureResponseCode::Ok, + })) } pub fn process_events(events: &[Event]) -> Result<(), String> { @@ -67,12 +70,6 @@ pub fn process_events(events: &[Event]) -> Result<(), String> { Ok(()) } -// A group of events! There is no limit here, though our HTTP stack will reject anything above -// 20mb. -pub async fn batch() -> &'static str { - "No batching for you!" -} - #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/src/event.rs b/src/event.rs index deed63578e00b..04e6c8ee571b2 100644 --- a/src/event.rs +++ b/src/event.rs @@ -18,13 +18,13 @@ pub enum Compression { #[allow(dead_code)] // until they are used #[derive(Deserialize, Default)] pub struct EventQuery { - compression: Compression, + compression: Option, #[serde(alias = "ver")] - version: String, + version: Option, #[serde(alias = "_")] - sent_at: i64, + sent_at: Option, } #[derive(Default, Debug, Deserialize, Serialize)] @@ -40,11 +40,14 @@ impl Event { /// We post up _at least one_ event, so when decompressiong and deserializing there /// could be more than one. Hence this function has to return a Vec. /// TODO: Use an axum extractor for this - pub fn from_bytes(_: &EventQuery, bytes: Bytes) -> Result> { - let d = GzDecoder::new(bytes.reader()); - let event = serde_json::from_reader(d)?; - - Ok(event) + pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result> { + match query.compression { + Some(Compression::GzipJs) => { + let d = GzDecoder::new(bytes.reader()); + Ok(serde_json::from_reader(d)?) + } + None => Ok(serde_json::from_reader(bytes.reader())?), + } } } @@ -62,6 +65,7 @@ pub struct ProcessedEvent { #[cfg(test)] mod tests { + use super::Compression; use base64::Engine as _; use bytes::Bytes; @@ -75,7 +79,14 @@ mod tests { .unwrap(); let bytes = Bytes::from(decoded_horrible_blob); - let events = Event::from_bytes(&EventQuery::default(), bytes); + let events = Event::from_bytes( + &EventQuery { + compression: Some(Compression::GzipJs), + version: None, + sent_at: None, + }, + bytes, + ); assert_eq!(events.is_ok(), true); } diff --git a/src/router.rs b/src/router.rs index db838ea0ca3f4..52478b634d59e 100644 --- a/src/router.rs +++ b/src/router.rs @@ -4,7 +4,14 @@ use tower_http::trace::TraceLayer; pub fn router() -> Router { Router::new() + // TODO: use NormalizePathLayer::trim_trailing_slash .route("/capture", post(capture::event)) - .route("/batch", post(capture::batch)) + .route("/capture/", post(capture::event)) + .route("/batch", post(capture::event)) + .route("/batch/", post(capture::event)) + .route("/e", post(capture::event)) + .route("/e/", post(capture::event)) + .route("/engage", post(capture::event)) + .route("/engage/", post(capture::event)) .layer(TraceLayer::new_for_http()) } diff --git a/tests/django_compat.rs b/tests/django_compat.rs new file mode 100644 index 0000000000000..f76fea0ba8a24 --- /dev/null +++ b/tests/django_compat.rs @@ -0,0 +1,57 @@ +use axum::http::StatusCode; +use axum_test_helper::TestClient; +use capture::event::ProcessedEvent; +use capture::router::router; +use serde::Deserialize; +use serde_json::Value; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use time::OffsetDateTime; + +/* + "path": request.get_full_path(), + "method": request.method, + "content-encoding": request.META.get("content-encoding", ""), + "ip": request.META.get("HTTP_X_FORWARDED_FOR", request.META.get("REMOTE_ADDR")), + "now": now.isoformat(), + "body": base64.b64encode(request.body).decode(encoding="ascii"), + "output": [], +*/ + +#[derive(Debug, Deserialize)] +struct RequestDump { + path: String, + method: String, + #[serde(alias = "content-encoding")] + content_encoding: String, + ip: String, + now: String, + body: String, + output: Vec, +} + +static REQUESTS_DUMP_FILE_NAME: &str = "tests/requests_dump.jsonl"; + +#[ignore] +#[tokio::test] +async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { + let file = File::open(REQUESTS_DUMP_FILE_NAME)?; + let reader = BufReader::new(file); + for line in reader.lines() { + let request: RequestDump = serde_json::from_str(&line?)?; + + if request.path.starts_with("/s") { + println!("Skipping {} dump", &request.path); + continue; + } + + println!("{:?}", &request); + // TODO: massage data + + let app = router(); + let client = TestClient::new(app); + let res = client.post("/e/").send().await; + assert_eq!(res.status(), StatusCode::OK, "{}", res.text().await); + } + Ok(()) +} diff --git a/tests/requests_dump.jsonl b/tests/requests_dump.jsonl new file mode 100644 index 0000000000000..ee8ba3d5e8e50 --- /dev/null +++ b/tests/requests_dump.jsonl @@ -0,0 +1,21 @@ +{"path":"/s/?compression=gzip-js&ip=1&_=1684752074919&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:01.993704+00:00","body":"H4sIAAAAAAAAA+2Xb0vcMBjAv0oJvphgvfxrmvbV2M0xcOgNdI6pHLkmtdXa1iT1dhO/+9KxTdlgR+WmHJdXpcmTp3maX35NT++AulW1BSnYMrVoTdFYsANa3bRK21IZkN499EylsKJvsYtWgZTtgF8NbdVdlLXLovVczUZZU5umUq9Rn0ssqkbIPqpyz6pckNK60a7LapG5PKegqadfCz390R68Kqxt09GoajJRFY2xKceEjowVtsxGWdHVV+He2/H45PjwZPfSpBGKY5Yisu0yHupRS3ebWishF/0IlRWivlBPSMoQTAnbBuePSjgFZ+CNkMH7o6NJ0I/tTBrA4AyA8/t7V095rVzrdQtSxDiNIwxjlGDuuraMMqZ0dZYuDUCcU+L6mIQMh5AhJBlHEaUSZiFFWOKMyBAxOoM8fAhGiEjmqtyal7Vs5n/mwjhiMIQEilxAnktCYvqPbMTFR/0qNFeqX7q2yKYqLmllxgflx/xgX3youuryZvHpYjKBE6bjb8f8czt+142/VPudGylLY8s6s78nQiidzRLCGclDiKKcKUEUnomM84eJ4BiqGPYT+RmeZAoC946aPDfKoYgT7l7Zi5A5F7p+DOatqLonwYN5iqL/QGRC/yZyr77pVKdkkIuychet3L2xQe42k1ZWL4KyDhiEcAmnied0XTjdJIOSBDFP5iAyk8TtZS/QlQqU8iUGdZz6L/26cLphAvVgDgXTn0BXLVBMlwoUQ8/pQE5jL9BnEKgHczCY7szuBbpSgSbL/uEdp8hzuiacbphAPZhDwYy8QF/gBIo9p2vC6YYJ1IM5FEzqBbpigSK8XKDEczqY0/Pv4oS7EYgfAAA=","output":[{"uuid":"0188430d-40cb-0000-82bb-5cab205fc9d8","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40ca-0000-c581-b136488335da\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+2XXS8DURCG35+y6RUJarfVD1fiI3HHBXGBSFVRSqu2aMRfxzNn28YNkdhoxWRydqczc+Zjz7tvtm+vh3pWQamG6qmFtqpIFS1wLehMDTyNkdXieupooAu1dTuyFtRHHtl7qqKa6uK559rBsqYYf5arR54h1i73sw8ZLe6B1Znka4WMXdZ4b4reIPe4v0PuVudET7rEd/JhT6Q5bCnSI7aIWM0m+zvYu/SWYq8pUUllvPdhwpSJmqH/S+a71bUWtaVNbSAH2tcO1yVdEb2qFaaqIhX0mCzzkz53qF+kbplY669PXzbtcFKlFSo08F2g/06vFdYyegnNej3+4kTsyR6x1oMt0rb2kF20ce1ByBuRMQqRlu8FGZ9UWzdMlkXfUCPLa13UmKFKRwl7q1jqaLXRzlnD4CMR/ZD/cwQ+hHMa/OI52vMyzK38GczVyfFdzG2R9y48T1uGvnOi2uF07JdNlvmtJ/Nm77vZ7XyG6IYJw85ykJ/jsz6j+HSO/A8cWQJ/ZnWOdI7MmyPL9P9zlswQWnOWdJacMkv6l6SzZP4smVA1H5Y0zVnSWXKaLDm7GHSW/MssWc/lH3eG0NhZ0llyyiwZO0s6S870t2TiLOksOWWWTJwlnSVzZ8k4YCwfliyx81jvS12b9+QeAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752071928, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752071929, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073916, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073918, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073918, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073920, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073920, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073921, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073921, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073922, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073922, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073923, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2988}","now":"2023-05-22T10:42:01.993704+00:00","sent_at":"2023-05-22T10:41:14.919000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684752098970&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:01.995810+00:00","body":"H4sIAAAAAAAAA+2Z207bMBiAXyWyuBgSoT7FcXI1rWOaxASdBGMaoMqNHRIISbAdug7x7nOmbaBNWteqCJX6Kor9+48PXz458ekdULeqtiAFW6YWrSkaC3ZAq5tWaVsqA9K7h5qxFFb0JXbWKpCyHfCroK26i7J2WbSeqskga2rTVOo16nOJWdUI2UdV7lmVC1JaN9pVWS0yl+cUNPX4a6HHP8qDV4W1bToYVE0mqqIxNuWY0IGxwpbZICu6+ircezscnhwfnuxemjRCccxSRLZdxkM9aOluU2sl5KxvobJC1BdqiaQMwZSwbXD+aAin4Ay8ETJ4f3Q0Cvq2nUkDGJwBcH5/78ZTXitXet2CFDFO4wjDJEri2FVtGWVM6cZZujQAcU4JjBGTkOEQMoQk4yiiVMIspAhLnBEZIkYnkIcPwQgRydwot6ZlLZvpn7kwjhgMIYEiF5DnkpCY/iMbcfFRvwrNleqXri2ysYpLWpnhQfkxP9gXH6quuryZfboYjeCI6fjbMf/cDt91wy/VfudaytLYss7s744QSieThHBG8hCiKGdKEIUnIuP8oSM4hiqGfUd+hieZgsDNUZPnRjkUcZKg+51nIXMqdP0YzFtRdUvBg3mKoicgMqF/E7lX33SqUzLIRVm5i1bu3tggdy+TVlbPgrIOGIRwDqfcc7oop/CZON0kg8YJJZ7MhchEEEfeoCs2KOVzFOpApR7UdQF1wxTqyVyYTDdlXqErVWgybxfqQHU+8KCuB6gbplBP5tqQ+XIV+j+7UOZBXRRU94XpFfr0CvVkLkwm9gpdsUIxna9Q/89+bUDdMIV6Mhcm058mrVqhCM9XqD9OWgLU8++WAqIwkh8AAA==","output":[{"uuid":"0188430d-40cd-0000-7103-c248d33d28b5","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40cc-0000-fcd3-63fc3328fd1f\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+1ZXU/CUAw9P2XhSROVr30wn4xK4hs+aHxAQhAQ0MEmDJAY/7p6egeEF40JC8PYNHcrbe9pu9udLOHzo4435BBjgQhdaqew4OKI1xw6aNHTWlolLkKAKXoYYLS05jCmzLn3AXm0EdIz4TWg5QxF+hOsiDgLWkPeOxuIEjfjCtZ4XYMYcq32xtRbxF7VV+dd8jTxij59zY09Fg5oiykRY/MUydnm/oD2kLXFtFdQQhk2vRPTYcyO2qb+Pvsb4RnHqOISF5Q73KLG6wmeGH0Kh115FJd6kSiH6zprzJ9nXpuxUt+YdUm3i3WWrsnQoq9HfTe1ulwF6mVqUmvjhxORJ3vPdW5sFq5wQ7mmtso9NbgWES0TKXjvlNVJDTBkZ0n0kDkSXKmiwh48VlTiXp933zzFZOe+zeCcEWOD//0Ezsw5TXd4jiViyMw5f2bmfGL8duaqxH0xz1OWTN8jowbmdOSXdJb4pSbxJu+72OV8FtRlJmR2Cka2n8/Kns6ncuR/4EjPvD9l5UjlyNQ50mb927NkMqG2sqSyZMYsaStLKkumzpJ+Kt+SyYQ6ypLKkhmzpKMsqSy519+SrrKksmTGLOkqSypLps6SJWZNiyU9ZUllyYxZUv+7UZZMnyWLZsbSYUn596aBL0Ml6tzkHgAA\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752095977, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752095978, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097943, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097944, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097944, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097945, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097945, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097946, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097946, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097947, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097947, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097948, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2991}","now":"2023-05-22T10:42:01.995810+00:00","sent_at":"2023-05-22T10:41:38.970000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684752114005&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:02.050911+00:00","body":"H4sIAAAAAAAAA+2Y3WrbMBSAX8WIXqxQN/qzLPtqLOsYdLQZtOtYW4JiybVb13YluVlW8u6Tx7aUDZYlZIwQXRlLR8c60ucPocsnoB5VbUEK9kwtWlM0FhyAVjet0rZUBqRPi56xFFb0LXbWKpCyA/Cjoa26m7J2WbSeqskga2rTVOol6nOJWdUI2UdV7luVC1JaN9p1WS0yl+cSNPX4c6HH39qDF4W1bToYVE0mqqIxNuWY0IGxwpbZICu6+i48ej0cXpyfXhzemjRCccxSRPZdxlM9aOlhU2sl5KwfobJC1DdqjaQMwZSwfXD9rIRLcAVeCRm8PTsbBf3YzqQBDK4AuJ7PXT3lvXKt9y1IEeM0jjBCCELouvaMMqZ0dZYuDUCcUwJjxCRkOIQMIck4iiiVMAspwhJnRIaI0Qnk4SIYISKZq3JvWtaymf6aC+OIwRASKHIBeS4JiekfshEXH/W70NypfuvaIhuruKSVGZ6U7/OTY/Gu6qrbh9mHm9EIjpiOv5zzj+3wTTf8VB13bqQsjS3rzP6cCKF0MkkIZyQPIYpypgRReCIyzhcTwTFUMewn8j08yRQEbo2aPDfKoUggxPOD/0LmVOj6OZiPourWggfzFEX/gMiE/k7kUf3QqU7JIBdl5R5auXdjg9z9TFpZPQvKOmCOwyWcIs/ptnC6SwbFCfdkrkYmgtgtmTfoRg2KErzEoY5UJwRP6naQumMO9WSuTKY7uHuHbtShybJjqAOVeFC3BdQdU6gnc1UyUeIVumGFUr5codSDui2g7phCPZlbQ6ZXqAd1K0DdMYVGnsxVyeReoZu+DP2Lu1AP6hqgXn8FVjpm4JMfAAA=","output":[{"uuid":"0188430d-40cc-0004-71c9-b5d5762c946c","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40cc-0003-1759-ec61e7e52da5\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+1Yy07CUBA9n9Kw0kTFlvJcGZXEHS40LpCQCghoeQgFJMZfV8/cSwkbiYmVR5xMbjudmTsP7ukJ6edHFW9IIcIcQ7SoleAghyNeU2gioCdYWCVuiBATtNFFf2FNYUSZce8D0mhgQM+Y15CWM7j021xD5pnTOuC9uZJR4qZc4TJfy2QccMV7I+oBc8f9VXmXOnW8okNffWWPgwPaIsqQsWmK1Gxwf0j7gL1FtBfgIQOf3rGZMOJEDdN/h/P18YxjlHGJC8odblHh9QRPjC4hy6nylBx1l1kOl31WWD/Nuj5jpb8R+5Jp58sqLVMhoK9NfTO95rhOqWeoSa+1NSciv+w917mxObjCDeWaWlx7YvI6zOiYSMn3TolPqoseJ7PRPdaweaWLAmfIsyOPT67pScTu3DUMzhgxMvm/R+DUnNNkg+foMYdgLrs3mCsyx08xV2beF/N7yhL0PTKqa05HnmQy65eexGvfd7HL+cypCyYEOxZbv8enu6P4VI78Dxzp8f0p7CwGlSP3mSNd1vUS4EmLUU95UnlyyzzpKU8qTybOk8VE/k1ahGaUJZUlt8ySGWVJZcnEWdJn/0mxpK8sqSy5ZZb0lSWVJZUllSWVJddgMKssqSz5B18mk/suKQit4QuIGGIM5h4AAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752111000, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752111001, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112981, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112982, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112982, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112983, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112983, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112984, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112984, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112984, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112985, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112985, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 3002}","now":"2023-05-22T10:42:02.050911+00:00","sent_at":"2023-05-22T10:41:54.005000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684752101977&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:02.048295+00:00","body":"H4sIAAAAAAAAA+2Ub0vcMBjAv0oJvphgvfxrmvbV2M0xcOgNdI6pHLkmtdWa1CT1dhO/+1LZpmzgcXKbDPuq9MnzPM2T/Po7vgHqWmkPcrDhtGhdZTzYAq01rbK+Vg7kN/crUym86CN+0SqQsy3wM9A23VmtQxdr52o2Kox2plGvUd9LLBojZJ/VhG81IUlZa2xY8lYUoc8xMHr6tbLTu3j0qvK+zUejxhSiqYzzOceEjpwXvi5GRdXpi3jn7Xh8dLh/tH3u8gSlKcsR2Qwd9+2opdtGWyXkoq9QRSX0mXpCU4ZgTtgmOH0wwjE4AW+EjN4fHEyivrZzeQSjEwBOb2/DPPWlCtHLFuSIcZomGGY8S5OwtOGUc3WYsw5tAOKcEpgiJiHDMWQIScZRQqmERUwRlrggMkaMziCP75MRIpKFKTfmtZZm/nsvjBMGY0igKAXkpSQkpY90IyE/6W/BXKj+6tqqmKq0po0b79Ufy71d8aHpmvOrxaezyQROmE2/HfLP7fhdN/7S7HahUtbO17rwvzZCKJ3NMsIZKWOIkpIpQRSeiYLz+43gFKoU9hv5kZ4VCoJwRqYsnQooEgjh7dazkDkXVj8E81o03ZPgwTxHyV8gMqN/ErmjrzrVKRmVom7Cw6rw7nxUhp/JKm8XUa0jFg51Cads4HQlTnGWZc/E6csxKIIwY0EHA5krkIlC6WDQNRuU8kcVegcqGkBdFVQ6KPRfKBQPZK5KJhkUumaFYrpcoQOo/w2og0IHMh8nMxzZoNC1KhTh5QoNPhhAXRXU0+/5Yx7EDBUAAA==","output":[{"uuid":"0188430d-40cc-0002-a8da-245e5d68b7b7","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40cc-0001-801c-85fd950326c8\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+2XQU8CQQyF30/ZcNJEhV1wEU9GJfGGB40HJAYRFUVBWFRi/Ovq11lBLhoTCCKZNLPbaTuv7U55Ce9vVb0oo0RDddVE21agWGs8M7pQHU/902pxXbU10JVauv+0ZtRDnjh7rqwa6uDp82xj2VGIP8XqgjPE2uF9MYFocY+s9hiv6RA7rNHZBL0O9qi+Km/Lc6ZnXeM7mzgTaAVbgnSJzSKWs8H5NvYOtSXYtxQprwLevuswoaOGq/+a/u51q3WVta895ETHqvDc0A3R29qkqyISo4egrI7rrJA/S94CsVZfj7qs2+E4S9NlqOO7Qp9PrTErh55Hs1prP9yIfdlT1q6zBTrQEXKINso9cLgBiIGLNLxXZHRTLd3RWRp9R44U16rYoociFUWcLbEruV16ctFm8ImInsP/fgIf3T0N5niPERg2c5v/ZuZKYPx25srgPrjvacum75Kolrsd21lnqd9qMm/6eze73c8Q3WbCZifnZPr5jBd0Pj1HLjtHhm6CS26WPUd6jpw1RxaofzqW/JrQ0LOkZ8k/ZsnIs6RnyZmzZETWWbFk5FnSs6RnSc+SS8eSofvPPBuWzHOypg9uMIiYmBQAAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752098975, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752098976, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752100960, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752100961, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752100962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752100962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752100962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752100963, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 3000}","now":"2023-05-22T10:42:02.048295+00:00","sent_at":"2023-05-22T10:41:41.977000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684752123024&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:03.027729+00:00","body":"H4sIAAAAAAAAA+2UXU/VMBjHv8rScCEJ4/RtXbcr4xFjgoFjAmIEctKzdqxQ2tF2HI+E725nVIgmEoiGC8/Vsuflv+e//voc3wB1rWwENdgIVvShcxFsgd67XvmoVQD1zV1mLkUUYySuegVqtgV+BHoznGmbVLxfqsWkcTY4o16iUUusjBNyrDLpWyYVKe+dT6noRZN0joGz88+dn3+LZy+6GPt6MjGuEaZzIdYcEzoJUUTdTJpusBf5zuvp9Ohw/2j7PNQFKktWI7KZFPf9pKfbznol5GrsUE0n7Jl6gihDsCZsE5zes3AMTsArIbO3BwezbOwdQp3B7ASA09vb5EdfqhS97EGNGKdlgRGGEJOU2ggqBJ186iQDEOeUwBIxCRnOIUNIMo4KSiVscoqwxA2ROWJ0AXl+V4wQkSy53FhqK93yVy2MCwZzSKBoBeStJKSkf1Ajqb4YT8FdqPHo+q6Zq1JTE6Z7+n27tyvemcGcX60+nM1mcMZ8+eWQf+ynb4bpJ7M7pE6pQ9S2iT8HIZQuFhXhjLQ5REXLlCAKL0TD+d0guISqhOMg38urRkGQ/pFr26ASiriqytutZyFzKby9D+a1MMOT4MG8RsU/ILKivxO5Y68GNSiZtUKb9PAqvYeYtekyeRX9KtM2YxDCBzgt1pw+llP2TJz+TxsUVbxak/koMlGysN6gf3mDVg+tUFRVaA3q40E9/QqvsD61hgoAAA==","output":[{"uuid":"0188430d-4494-0001-7d1f-eae0ac044b9f","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4494-0000-2fde-272e9466a2a0\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPtGa2QC/+2Uy04CUQyG/0eZsNJE5X5dGZXEHS40LtCQERDQ4SIMIDG8uvr1DBA2GhMJwUianun08redds7He1VviinUTAM1kUrylNMRZ0wN+Vj8hdb8Bgo0Vksd9RbamIbQlNgHxVVXH8uIM0BzqiT2CGsAzgxtn2djDdH8JnCwwms6xD68jA2RfbCX9VV5Wp6aXtXGVluL8XSALoQG+MYhy1knPkDfp7YQfUEppZXBOnIdhnRUd/W36a+nZx2rrAudQ7e6UYXzRE94l5SlqzyUQ06Ccriqs0L+OHkz+Fp9Q+qybmerLE2XwcfWQt5OrTk4gZxGslrvv5mIfdk7+MzpPF3qGrpCWuYeO1wPRM95Gt4cWk6qoy6dRd5dckS4VkWBHvJUlOItRXzC9RVF7toOTvEYOvyvN3Di5jTe4hxTYNjOZf/MzhXB+OnOlcF9cd/T2LbvEa+Om469WWeR3Woya/S/m97mM0O2nbDdSTj6/X5md3Q/93fkf7gjk/w/BXh/R+7vyE3fkcWN3JK2oUXOOfGfu7RZCEwKAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752120023, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752120025, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752121989, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752121991, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2997}","now":"2023-05-22T10:42:03.027729+00:00","sent_at":"2023-05-22T10:42:03.024000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684751932581&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.993934+00:00","body":"H4sIAAAAAAAAA+2UX0/VMBjGv8rScCEJ47Rr13W7Mh4xJhg4JiBGICc9a8cKpR1tx/FI+O52RoVookIkhLCrZe+fZ++7/vocXgF5KU0AFVjzhne+tQFsgM7ZTrqgpAfV1U1mLnjgQySsOgkqugF+BDrdnygTVZxbysWktsZbLV+iQYuvtOViqNLxWzoWSeesi6ngeB11DoE188+tm3+LJy/aELpqMtG25rq1PlQsw2TiAw+qntRtb87SrdfT6cH+7sHmqa9yVBS0Qng9Ku66SUc2rXGSi9XQIeuWmxN5D1GKYIXpOji+tcIhOAKvuEje7u3NkqG391UCkyMAjq+v4z7qXMboeQcqRBkpclRmZc5QTK156b2Ke6ooAxBjBMMCUQFplkKKkKAM5YQIWKcEZSKrsUgRJQvI0ptihLCgccu1pTLCLn/VyrKcwhRiyBsOWSMwLsgf1HCsz4dTsGdyOLqureeyUET76Y563+xs83e616cXqw8nsxmcUVd82Wcfu+mbfvpJb/exUygflKnDz0EwIYtFiRnFTQpR3lDJscwWvGbsZpCsgLKAwyDfy8taQhD/kW0aLyOKWVmW1xuPQuaSO3MbzEuu+3vBk7EK5Q9AZEl+J3LLXPSylyJpuNLx4WR89yFp4mVyMrhVokxSUgjhX0DNRlDvCip7JFCfmYWOZD4ZMkcLHUF9EqA+MwvFI5l3JbMYLfQ/W+g/OOjI6T04Pf4KKs0GxMkPAAA=","output":[{"uuid":"0188430d-4c42-0001-5c53-b092a3eae230","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c42-0000-208e-16cab7bc0a1c\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPxGa2QC/+2US08CUQyFz0+ZsNJERV7DY2VUEne40LhAQhAQUF7CABLjX1e/3gHCRmIiIRgnzZ0pbe9pOy3n86OsN8UUaK6hmmgFefJ1xDOmhmp4agurxQ3V1UQtddRfWGMaITPuPiiuugZ4xjy7WM6UwB9iDcGZYx3wbqwhWtyU013hNR3igLO8G6DXwF7WV+Zteap6VRtfde2OpwNsATIkNo5Yzjr3u9gH1BZgzymplNJ4x67DgI7qrv42/fX1rGMVdakL5E63KvE80RPRBWXoKov46AlQDld1lsgfJ2+aWKtvRF3W7XyVpeky1PC10HdTq885RU+hWa2VDROxL3vPOXc2T1e6Qa7RlrknDtcD0XORhveOLCfVUY/OwugeOUJcqyJHD1lXUZ6O8mg59PDmvu3gjIiRw/9+A6duTpMdzjEJhu1c5s/sXB6Mn+5cEdwX9z3t2PY9EtVx07Ff1lnot5rMG/7fzW7zmaPbTnhk9clu8vsNTe7phkYs+X9YMhmxZMSSEUtGLBmx5IYdTEUsGbHk1llyexxp+1nRF6KoW4VyDwAA\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684751929581, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751929582, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751929582, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751929582, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751929583, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751929583, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2999}","now":"2023-05-22T10:42:04.993934+00:00","sent_at":"2023-05-22T10:38:52.581000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684751935588&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.995438+00:00","body":"H4sIAAAAAAAAA+2WYU/VMBSG/8rS8EESxm3Xruv2yXjFmGDgmoAYgdz0rh0rlHa0Hdcr4b/bGRWiiQSCGnSflp2evjunffquh1dAXkoTQAXWvOGdb20AG6BztpMuKOlBdXUzMhc88CESVp0EFd0A3wKd7k+UiSrOLeViUlvjrZbP0aDFV9pyMWTp+C0dk6Rz1sWh4HgddQ6BNfOPrZt/iSfP2hC6ajLRtua6tT5ULMNk4gMPqp7UbW/O0q2X0+nB/u7B5qmvclQUtEJ4PSruuklHNq1xkovVMEPWLTcn8gGiFMEK03VwfKuFQ3AEXnCRvN7bmyXD3N5XCUyOADi+vo79qHMZo+cdqBBlpMhRibOc0Ti05qX3KvapogxAjBEMC0QFpFkKKUKCMpQTImCdEpSJrMYiRZQsIEtvkhHCgsYu15bKCLv8USvLcgpTiCFvOGSNwLggv1DDMT8fdsGeyWHruraey0IR7ac76m2zs83f6F6fXqzencxmcEZd8Wmfve+mr/rpB73dx5lC+aBMHb4XgglZLErMKG5SiPKGSo5ltuA1YzeFZAWUBRwK+Zpe1hKCuEa2abyMKGII4fXGXyFzyZ25DeYl1/2D4MlYhfLfQGRJfiZyy1z0spciabjS8eFkfPchaeJhcjK4VaJMUtK4qneAWoygPhVQ/zMLHcl8MmT+uxaKstFCHxvUrCzL0UL/hIWykcz7khkP82ihj2qhd19Cy/jfGjm9L6fHnwGF59KmyQ8AAA==","output":[{"uuid":"0188430d-4c44-0001-b56a-cc6e82a03b21","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c44-0000-1533-2d9329721e64\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPxGa2QC/+2WS08CUQyFz0+ZsNJERV7DY2VUEne40LhAQxAQUF7CABLDX1e/3gHCRmMiIaiT5t4pbe/p6dxOw/tbWa+KKdBMAzXQCvLk64A9prqqeKoLq8UN1NFYTbXVW1hjGiJTzt4rrpr6eEbsHSwnSuAPsQbgzLD2edbXEC1uwuqs8BoOsc9ang3Qq2Av+ZV5Wp6KXtTCV1k742kPW4AMiI0jlrPG+Q72PtwC7DkllVIa78hVGFBRzfFvUV9PTzpUUec6Q250rRL7kR6JLihDVVnER0+Asr/iWSJ/nLxpYo3fEF5W7WyVpeEyVPE10bfD1Wcdo6fQjOvdFzdib/aWdepsni50hVyiLXOPHa4HouciDW+OLG+qrS6VhdFdcoS4xiJHDVnHKA+XJFoOa3hy13pwSsTQ4X/egRN3T+Mt3mMSDOu5zK/puTwY3+25IrjP7n3asu57IKrtbsd+WWWh3ziZN/zezW73M0O3nvDI6pPd5Ocdmt3RDo2m5P+ZktloSkZTcuNTMgH7aEpGU/KvTMlcNCWjKbnxKbmpf5J5MOac/wAjkc3Fcg8AAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684751932586, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751932587, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751932587, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751932587, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751932588, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751932590, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 3000}","now":"2023-05-22T10:42:04.995438+00:00","sent_at":"2023-05-22T10:38:55.588000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684752077923&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.997482+00:00","body":"H4sIAAAAAAAAA+2XW0vcQBiG/0oYvKhg3Dllcrgq3VoKFt2C1lKVZTYzMdFxJs5M3G7F/95JabvSQmVlW6nmKuQ7vPm+zJMXcnwD5LXUHhRgw2neutp4sAVaa1ppfSMdKG6WmangnvcRv2glKNgW+BFoVXfW6KBi7VzORqXRzij5EvVafKEMF32VCs9SoUhaa2xIecvLoHMMjJ5+ru30Wzx6UXvfFqORMiVXtXG+yDChI+e5b8pRWXf6It55PR4fHe4fbZ+7IkFpygpENoPivh21dNtoK7lY9B2yrLk+kw8QZQgWhG2C0zsrHIMT8IqL6O3BwSTqeztXRDA6AeD09jbs01zKEL1sQYFYRtMEw5TmOAupDSeda8KeTZABKMsogSliAjIcQ4aQYBlKKBWwjCnCApdExIjRGcziZTFCRLCw5ca80cLMf9XCOGEwhgTyisOsEoSk9A9qJNQn/SmYC9kfXVuXU5k2VLnxXvO+2tvl71Snzq8WH84mEzhhNv1ymH1sx2+68Se124VO0Tjf6NL/HIRQOpvlJGOkiiFKKiY5kXjGyyxbDoJTKFPYD/K9PC8lBOEdmapyMqCI8zy53XoUMufc6rtgXnPVPQgenBUo+QtE5vR3Inf0VSc7KaKKNypcrAz3zkdV+Jis9HYRNTpiEMKB03VzSh+J0+fkoCxHZCBzJTIRRHBw0DU7aH6fhQZQgx8MoK4EKswHC/0XFjqQ+d+Q+XQtFNPBQp8QqM/MQsNP6UDmamSmg4Wu2UIRvt9C2QDq6qCefgX0iVnZDBUAAA==","output":[{"uuid":"0188430d-4c46-0001-9d63-c33d1ad5a8bc","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c46-0000-7b0c-6e3bf34bf14a\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPxGa2QC/+2USU8CQRCF30+ZcNJEZRGG5WRcEm940HhAYhBRUDZhECfGv65+1QPEi8bDxLVT6Z6iqvpVVXfxXp4belRGkWKN1UGrKVCoDfaMLtXC01pYLW6svma6Vk/DhTWjCTLn7IWyamuEZ8rex7KjPP4EawxOjHXE9/INosXds/orvI5DHLGWZyP0FtjL+hp8Lc+5HtTFd/7mTKA1bBEyJjaLWM425/vYR9QWYa+ooG0V8U5dhxEdtV39Xfob6labOtC+9pBTnajOvqUbomsq0VUZCdHzoKyv6qyTP0veIrFW34S6rNt4laXjMrTwXaN/Ta0hK4e+jWa1Nj94EbvZM9auswU61DFyhLbMPXO4AYiBizS8J2T5Uj0N6CyJHpAjwbUqKvRQpqICZ8voVbTK4uRPm8E5EROH//4E3rt3mn3hO9p92cyVfs3MVcH47MwdgHvn7tOWTd8VUT33OvbLOkv8VpN5k/+72e19YnSbCZudnJO/O5+eI/8DR4bMoN2e50jPkWlzZDUVlkwmtOhZ0rPkN7Nk0bOkZ8nUWbJAVs+SniX/CkuWPEt6lkydJfNuxtJhyZCTTb0CdWfxy5gUAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752074928, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752074928, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752076913, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752076914, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752076914, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752076914, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752076915, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752076916, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2995}","now":"2023-05-22T10:42:04.997482+00:00","sent_at":"2023-05-22T10:41:17.923000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684752104985&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.999014+00:00","body":"H4sIAAAAAAAAA+2ZbUvcMBzAv0oJvphgvTw1Tftq7OYYOPQGOsdUjlyT2mpta5J6u4nffenYdrKBxx0nclxelSb//JuHX3+E5PwBqHtVW5CCHVOL1hSNBXug1U2rtC2VAenDvGYshRV9iZ21CqRsD/wpaKvuqqxdFq2najLImto0lXqL+lxiVjVC9lGV+1blgpTWjXZVVovM5TkHTT3+Xujxr/LgTWFtmw4GVZOJqmiMTTkmdGCssGU2yIquvgkP3g+HZ6fHZ/vXJo1QHLMUkV2X8VgPWrrf1FoJOetbqKwQ9ZVaISlDMCVsF1w+GcI5uADvhAw+npyMgr5tZ9IABhcAXD4+uvGUt8qV3rYgRYzTOMIIooTHrmrHKGNKN87SpQGIc0pgjJiEDIeQISQZRxGlEmYhRVjijMgQMTqBPJwHI0Qkc6PcmZa1bKb/5sI4YjCEBIpcQJ5LQmL6TDbi4qN+FZob1S9dW2RjFZe0MsOj8nN+dCg+VV11fTf7cjUawRHT8Y9T/rUdfuiG36rDzrWUpbFlndm/HSGUTiYJ4YzkIURRzpQgCk9Exvm8IziGKoZ9R36HJ5mCwM1Rk+dGORRxktDHvVchcyp0/RTMe1F1K8GDeYqiFyAyof8TeVDfdapTMshFWbmHVu7d2CB3P5NWVs+Csg4YhHABp4nndFM43SaDkoQhT+ZSZCKI3ZR5g67VoJQvUKgDFXtQNwXULVMo8WQuSSZy+yGv0LUqNFm0C3Wgui2VB3UzQN0yhUaezGXJdEcfXqFrVSjCixXKPKibAuqWKdSTuTSZbsq8QteqUEwXK9Sf2S8NqtseeYW+vEK5J3NZMv1t0mso1F8nrQDq5U9Nk6Owkh8AAA==","output":[{"uuid":"0188430d-4c49-0001-a3b5-5cbaee834ee0","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c49-0000-e27d-3ad69e097d43\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAP1Ga2QC/+2ZTU8iQRCG358y4bQmrsgAA3gyfiTe9LBmD0oIIgsoCuKgks3+9dWneoB4WWPiRDBb6fRQVFXXB108IeH575l+q6BUM43VRdpRpESbPAu6VBtLe641v7GGmqqngW7n2oImrEfOXqiojkZY7nkO0eyqhD2LNSbODO2I18tXEc3vgT1cxuuGiCP24myK3Cb2or4zXi1PS0/qY2u9OhPpG7qUNca3yLKcHc4P0Y+oLUVfV6yyKljvQ4cpHXVC/X36u9W1vutQB9pn/dSpjnlu6QrvHVXpqsZKkEtE2VjWeUz+Inkr+Fp9E+qybmfLLN2QoY2th/w5tSbsbeQyktXafONG7JM9Z+8FXaQj/WCdIC1yT0PciIhR8LR4f1iLmxrohs4y7xtyZHGtijo91KgoDvWU1EBTm59ctxl8xGMS4v97Ah/CPU0/8R5jYtjMVb/MzDWI8d6ZOyTuXfg8bdv0/cJrEG7H3llnmd1qMmv2fTe93c8M2WbCZmc7rI/PZ2NN59MZ+T8wssz8WWXOSGdk3oysUP/HKZlNaOyUdEqumJJlp6RTMndKNnL5LZlNaMUp6ZRcMSWrTkmnZO6ULFF9XpRMnJJOyRVTMnFKOiVzp2RM1rwoWXNKOiVXTMm6U9IpudaUtH9vmnoBGOaMiuQeAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752101987, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752101989, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103961, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103963, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103964, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103965, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103966, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103966, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103967, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103968, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103969, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2994}","now":"2023-05-22T10:42:04.999014+00:00","sent_at":"2023-05-22T10:41:44.985000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} +{"path":"/e/?ip=1&_=1684752184509&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.525363+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ3WVdkbGRtbGxkeUlzSW5CeWIzQmxjblJwWlhNaU9uc2lKRzl6SWpvaVRXRmpJRTlUSUZnaUxDSWtiM05mZG1WeWMybHZiaUk2SWpFd0xqRTFMakFpTENJa1luSnZkM05sY2lJNklrWnBjbVZtYjNnaUxDSWtaR1YyYVdObFgzUjVjR1VpT2lKRVpYTnJkRzl3SWl3aUpHTjFjbkpsYm5SZmRYSnNJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0x5SXNJaVJvYjNOMElqb2liRzlqWVd4b2IzTjBPamd3TURBaUxDSWtjR0YwYUc1aGJXVWlPaUl2SWl3aUpHSnliM2R6WlhKZmRtVnljMmx2YmlJNk1URXpMQ0lrWW5KdmQzTmxjbDlzWVc1bmRXRm5aU0k2SW1WdUxWVlRJaXdpSkhOamNtVmxibDlvWldsbmFIUWlPamszTkN3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRVd01Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPRFF6TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFME16TXNJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpVM0xqSWlMQ0lrYVc1elpYSjBYMmxrSWpvaWMzRXhhWEppT1dRME0ySmhZbXBxTWlJc0lpUjBhVzFsSWpveE5qZzBOelV5TVRnMExqVXdPU3dpWkdsemRHbHVZM1JmYVdRaU9pSXhPRGcwTXpCbE16UmlOVE5qWXkwd1pETmpObU0zTURrME5ERTFaRGd0TkRFeVpESmpNMlF0TVRZMFlqQTRMVEU0T0RRek1HVXpOR0kyWkRNNElpd2lKR1JsZG1salpWOXBaQ0k2SWpFNE9EUXpNR1V6TkdJMU0yTmpMVEJrTTJNMll6Y3dPVFEwTVRWa09DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqWmtNemdpTENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW5SdmEyVnVJam9pY0doalgycElZMFIxTjIwelduWnVTVzV3YTJKNGJVcHlTMFZpZVVwMWEzbEJXa042ZVV0bFREQnpWSGhDTTJzaUxDSWtjMlZ6YzJsdmJsOXBaQ0k2SWpFNE9EUXpNR1V6TkdJM04yWXdMVEF6TXpCa05XUXlPRE00WmpObE9DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqZ3lPRE16SWl3aUpIZHBibVJ2ZDE5cFpDSTZJakU0T0RRek1HVXpOR0k1TXpBMU5pMHdORGRoT1RCaE1HSXdNVE5pTkRndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdKaE1qVmxOaUlzSWlSd1lXZGxkbWxsZDE5cFpDSTZJakU0T0RRek1HVXpOR0ppTVRZM1lpMHdOekJrTVRrMVpqVTVNak0yTmpndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdKak1qazRNQ0o5TENKMGFXMWxjM1JoYlhBaU9pSXlNREl6TFRBMUxUSXlWREV3T2pRek9qQTBMalV3T1ZvaWZRJTNEJTNE","output":[{"uuid":"0188430e-34ce-0000-0e70-879b9262bb17","distinct_id":"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"sq1irb9d43babjj2\", \"$time\": 1684752184.509, \"distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"timestamp\": \"2023-05-22T10:43:04.509Z\"}","now":"2023-05-22T10:43:04.525363+00:00","sent_at":"2023-05-22T10:43:04.509000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?ip=1&_=1684752184509&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.515071+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ2Y0hSZmFXNGlMQ0p3Y205d1pYSjBhV1Z6SWpwN0lpUnZjeUk2SWsxaFl5QlBVeUJZSWl3aUpHOXpYM1psY25OcGIyNGlPaUl4TUM0eE5TNHdJaXdpSkdKeWIzZHpaWElpT2lKR2FYSmxabTk0SWl3aUpHUmxkbWxqWlY5MGVYQmxJam9pUkdWemEzUnZjQ0lzSWlSamRYSnlaVzUwWDNWeWJDSTZJbWgwZEhBNkx5OXNiMk5oYkdodmMzUTZPREF3TUM4aUxDSWthRzl6ZENJNklteHZZMkZzYUc5emREbzRNREF3SWl3aUpIQmhkR2h1WVcxbElqb2lMeUlzSWlSaWNtOTNjMlZ5WDNabGNuTnBiMjRpT2pFeE15d2lKR0p5YjNkelpYSmZiR0Z1WjNWaFoyVWlPaUpsYmkxVlV5SXNJaVJ6WTNKbFpXNWZhR1ZwWjJoMElqbzVOelFzSWlSelkzSmxaVzVmZDJsa2RHZ2lPakUxTURBc0lpUjJhV1YzY0c5eWRGOW9aV2xuYUhRaU9qZzBNeXdpSkhacFpYZHdiM0owWDNkcFpIUm9Jam94TkRNekxDSWtiR2xpSWpvaWQyVmlJaXdpSkd4cFlsOTJaWEp6YVc5dUlqb2lNUzQxTnk0eUlpd2lKR2x1YzJWeWRGOXBaQ0k2SWpsbmVYTnpkM1JtZEdFNE1IVjNkbmdpTENJa2RHbHRaU0k2TVRZNE5EYzFNakU0TkM0MU1Ea3NJbVJwYzNScGJtTjBYMmxrSWpvaU1UZzRORE13WlRNMFlqVXpZMk10TUdRell6WmpOekE1TkRReE5XUTRMVFF4TW1ReVl6TmtMVEUyTkdJd09DMHhPRGcwTXpCbE16UmlObVF6T0NJc0lpUmtaWFpwWTJWZmFXUWlPaUl4T0RnME16QmxNelJpTlROall5MHdaRE5qTm1NM01EazBOREUxWkRndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdJMlpETTRJaXdpSkhKbFptVnljbVZ5SWpvaWFIUjBjRG92TDJ4dlkyRnNhRzl6ZERvNE1EQXdMM05wWjI1MWNDSXNJaVJ5WldabGNuSnBibWRmWkc5dFlXbHVJam9pYkc5allXeG9iM04wT2pnd01EQWlMQ0owYjJ0bGJpSTZJbkJvWTE5cVNHTkVkVGR0TTFwMmJrbHVjR3RpZUcxS2NrdEZZbmxLZFd0NVFWcERlbmxMWlV3d2MxUjRRak5ySWl3aUpITmxjM05wYjI1ZmFXUWlPaUl4T0RnME16QmxNelJpTnpkbU1DMHdNek13WkRWa01qZ3pPR1l6WlRndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdJNE1qZ3pNeUlzSWlSM2FXNWtiM2RmYVdRaU9pSXhPRGcwTXpCbE16UmlPVE13TlRZdE1EUTNZVGt3WVRCaU1ERXpZalE0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME16QmxNelJpWVRJMVpUWWlMQ0lrY0dGblpYWnBaWGRmYVdRaU9pSXhPRGcwTXpCbE16UmlZakUyTjJJdE1EY3daREU1TldZMU9USXpOalk0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME16QmxNelJpWXpJNU9EQWlmU3dpZEdsdFpYTjBZVzF3SWpvaU1qQXlNeTB3TlMweU1sUXhNRG8wTXpvd05DNDFNRGxhSW4wJTNE","output":[{"uuid":"0188430e-34c8-0000-5a19-0714f9887930","distinct_id":"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"9gysswtfta80uwvx\", \"$time\": 1684752184.509, \"distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"timestamp\": \"2023-05-22T10:43:04.509Z\"}","now":"2023-05-22T10:43:04.515071+00:00","sent_at":"2023-05-22T10:43:04.509000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?ip=1&_=1684752184514&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.529988+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJwWkdWdWRHbG1lU0lzSW5CeWIzQmxjblJwWlhNaU9uc2lKRzl6SWpvaVRXRmpJRTlUSUZnaUxDSWtiM05mZG1WeWMybHZiaUk2SWpFd0xqRTFMakFpTENJa1luSnZkM05sY2lJNklrWnBjbVZtYjNnaUxDSWtaR1YyYVdObFgzUjVjR1VpT2lKRVpYTnJkRzl3SWl3aUpHTjFjbkpsYm5SZmRYSnNJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0x5SXNJaVJvYjNOMElqb2liRzlqWVd4b2IzTjBPamd3TURBaUxDSWtjR0YwYUc1aGJXVWlPaUl2SWl3aUpHSnliM2R6WlhKZmRtVnljMmx2YmlJNk1URXpMQ0lrWW5KdmQzTmxjbDlzWVc1bmRXRm5aU0k2SW1WdUxWVlRJaXdpSkhOamNtVmxibDlvWldsbmFIUWlPamszTkN3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRVd01Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPRFF6TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFME16TXNJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpVM0xqSWlMQ0lrYVc1elpYSjBYMmxrSWpvaWVXVnViamx0YVhRNE4yNXNaR1J4YUNJc0lpUjBhVzFsSWpveE5qZzBOelV5TVRnMExqVXhOQ3dpWkdsemRHbHVZM1JmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadElpd2lKR1JsZG1salpWOXBaQ0k2SWpFNE9EUXpNR1V6TkdJMU0yTmpMVEJrTTJNMll6Y3dPVFEwTVRWa09DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqWmtNemdpTENJa2RYTmxjbDlwWkNJNkltZFJSRWhzY0hoeFEwaG5WV3RvTlhNelRFOVdWVkUxUVdOT2RERnJiVlpzTUhoMlQyRnFNbk5tTm0waUxDSWtjbVZtWlhKeVpYSWlPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPamd3TURBdmMybG5iblZ3SWl3aUpISmxabVZ5Y21sdVoxOWtiMjFoYVc0aU9pSnNiMk5oYkdodmMzUTZPREF3TUNJc0lpUmhibTl1WDJScGMzUnBibU4wWDJsa0lqb2lNVGc0TkRNd1pUTTBZalV6WTJNdE1HUXpZelpqTnpBNU5EUXhOV1E0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME16QmxNelJpTm1Rek9DSXNJblJ2YTJWdUlqb2ljR2hqWDJwSVkwUjFOMjB6V25adVNXNXdhMko0YlVweVMwVmllVXAxYTNsQldrTjZlVXRsVERCelZIaENNMnNpTENJa2MyVnpjMmx2Ymw5cFpDSTZJakU0T0RRek1HVXpOR0kzTjJZd0xUQXpNekJrTldReU9ETTRaak5sT0MwME1USmtNbU16WkMweE5qUmlNRGd0TVRnNE5ETXdaVE0wWWpneU9ETXpJaXdpSkhkcGJtUnZkMTlwWkNJNklqRTRPRFF6TUdVek5HSTVNekExTmkwd05EZGhPVEJoTUdJd01UTmlORGd0TkRFeVpESmpNMlF0TVRZMFlqQTRMVEU0T0RRek1HVXpOR0poTWpWbE5pSXNJaVJ3WVdkbGRtbGxkMTlwWkNJNklqRTRPRFF6TUdVek5HSmlNVFkzWWkwd056QmtNVGsxWmpVNU1qTTJOamd0TkRFeVpESmpNMlF0TVRZMFlqQTRMVEU0T0RRek1HVXpOR0pqTWprNE1DSjlMQ0lrYzJWMElqcDdmU3dpSkhObGRGOXZibU5sSWpwN2ZTd2lkR2x0WlhOMFlXMXdJam9pTWpBeU15MHdOUzB5TWxReE1EbzBNem93TkM0MU1UUmFJbjAlM0Q=","output":[{"uuid":"0188430e-34d2-0000-99e4-4cb75151aa19","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$identify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yenn9mit87nlddqh\", \"$time\": 1684752184.514, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$anon_distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"$set\": {}, \"$set_once\": {}, \"timestamp\": \"2023-05-22T10:43:04.514Z\"}","now":"2023-05-22T10:43:04.529988+00:00","sent_at":"2023-05-22T10:43:04.514000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/engage/?ip=1&_=1684752184515&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.531659+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW1WdFlXbHNJam9pZUdGMmFXVnlRSEJ2YzNSb2IyY3VZMjl0SW4wc0lpUjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkdScGMzUnBibU4wWDJsa0lqb2laMUZFU0d4d2VIRkRTR2RWYTJnMWN6Tk1UMVpWVVRWQlkwNTBNV3R0Vm13d2VIWlBZV295YzJZMmJTSXNJaVJrWlhacFkyVmZhV1FpT2lJeE9EZzBNekJsTXpSaU5UTmpZeTB3WkROak5tTTNNRGswTkRFMVpEZ3ROREV5WkRKak0yUXRNVFkwWWpBNExURTRPRFF6TUdVek5HSTJaRE00SWl3aUpIVnpaWEpmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadEluMCUzRA==","output":[{"uuid":"0188430e-34d4-0000-387d-e634d8ec56a6","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T10:43:04.531659+00:00","sent_at":"2023-05-22T10:43:04.515000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684752187560&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:07.564872+00:00","body":"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","output":[{"uuid":"0188430e-40ad-0000-e548-fc5de1421f3b","distinct_id":"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"7d1mrlsrey6apuzb\", \"$time\": 1684752184.51, \"distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"session-reset-on-load\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"offset\": 3044}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0001-b527-acee69cb829b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"56mza3obi7hco2vh\", \"$time\": 1684752184.515, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\"}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": false, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"offset\": 3039}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0002-e60d-ac4f19b25506","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"nqru46xqy512vurl\", \"$time\": 1684752184.515, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"offset\": 3039}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0003-e8e3-5965b1f00c95","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"621xmy2vdcpezwlh\", \"$time\": 1684752184.55, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 3004}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0004-b960-e71c6531c083","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"lihlwwsc66al5e1i\", \"$time\": 1684752184.555, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"posthog-3000\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2999}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0005-4907-567be483079e","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"nt2osnfl5abzmq8y\", \"$time\": 1684752184.555, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"enable-prompts\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2999}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0006-2957-27a39ce38568","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"cvfdres92ldvucw0\", \"$time\": 1684752184.559, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"hackathon-apm\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2995}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0007-9951-26b9cabcd2ab","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yqzwbgtdjdnmdwbf\", \"$time\": 1684752184.56, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"early-access-feature\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2994}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0008-80c4-46772bf66fc5","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"sv6bufbuqcbvtvdu\", \"$time\": 1684752184.56, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"hogql\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2994}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0009-5175-65c70ceabd9d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"eyoa5pfu7ddy9m5m\", \"$time\": 1684752184.56, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"feedback-scene\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2994}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000a-5dc4-9d624d8dc3fa","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"no1by5vd7x64u3sx\", \"$time\": 1684752184.586, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"onboarding-v2-demo\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2968}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000b-0f86-e81af913f30b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"jvedtjd1wp8xwwkw\", \"$time\": 1684752184.599, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"notebooks\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2955}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000c-fb88-5d95e7940f12","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"ingestion landing seen\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ib9n3revlo62eca3\", \"$time\": 1684752184.603, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2951}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000d-fec3-693237c3318e","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"9vunv0ozv84m22me\", \"$time\": 1684752184.621, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"instance\", \"$group_key\": \"http://localhost:8000\", \"$group_set\": {\"site_url\": \"http://localhost:8000\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2933}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684752190565&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:10.567715+00:00","body":"H4sIAAAAAAAAA+1YWXPbOAz+KxpNH6uE1GU5b027O712u51uOzvtdDSUCEmMZVGRKB/p9L8vKB+x5KNNkwdn6qfEOIiPIAAB+PLNhAkUyrwwn7BGyZiVqqnAfGqWlSyhUgJq8+Kb+UTiH/MvFhvvPhj/IRsJ4QSqWsgCGZScUe+MaHpUyWkNFRL/FBUkcqaJHCYihlDNS0DGC6hHSpaaETdVhebDpsqRkSlVXpyf5wgjz2StLgJCyLkoUqiVNoQKmoySXRHNKJnKCjbW53c1loBu0VLqbJBzVqQNS7UeFNbHD1qljiuAIsxApBlaGw7cW+JUcJXhIR4hSJwImJayUmvZwNWHr8kradfR5FxEaGYKkTaCPzY9eOYNzmxNFwXCUqHgSJ1AOvfoPOXFZEqLeXtRJfQlqR+4A8+mQXBGneFTkwu8cBEv9dL3L17m5ez6+cv04yjzauftu08f33vP4r8VHY0/5WQ2eceu7DrxxxvP06riia5DwHEjz4lji3An9uMBGbou9XhgudTmduxwi/puRALrVtznTqAPa7RXfwGFqEMOYxli4F1BjK5MWF4DHphWsinbKFyzTLK0aznUBwtDgFgeZb6VcN/1PTfwh0QHsaxSVogb1sbChpY9JMOFlsuBWsOhNxjYjBLXczWSolasiHVI7IxI8zui2siWEJ3Pohx4iFfHJw1rwVF5hZ/FSkwgTIC1wknOUrzNl6/I2qSFJZvnknF9UzRQorVMpp0QcZ02xypg+ViDQxHgVpyLeJRJ9DvyYMxE3uLQL8om+EtDW6OpcxaPDvCfYM4C5qRO4N3pWIu0aNrkXYhisoVcolmNcTsv2/KyyvwWapv3sJGPWiqHMcppv3wzFTpjmcraTpyzutZl6Is5jizbRMcxpaowbBko1FKfmoXKwjgTOYYeHql/yWRpeGEiVDDTwfPKiFlhMM4NZsSSg1EXoixBGUoa47mBUcabWJ3hM3ewcDHpoVGWDhc0T3aAQqbRsg4j6xmpS6ar1oaVt5gUxWWjlCzwdFkoXa63zO2UupPlqFVun2ztqb1IkLHxy7LKSoxZNe+TMY9UU+/j5qzCutsjJk2eW4uy2eNkrLZ0ZlkC77fwu3vriGWErW+x1z3GLuBdYhd2l9eC7pJuIXfpHcA6FnS03OFFtgIuyUF/TvUfK5b5wgf+djBoAWMlpQ37XcP4OesYtncavg/Sy0rwFP7B7+qBmN0l1DFr/xTOw2atacUwuaufMr8Wfrjbt4XxoO1W4qEMIv3Vqvt5vvZp1sboIRhGX8tY6tzvQZYRu89+G6gPa2jRpm1myT6rC8l1ntzzCfQrWhg8eNTK630SVpt8ZzT0BY19mh2I2FF2IDo/gvgBa9El08G9/C/E7ncuG411ScGiJThH/21hXAoYPdXV77XiL3jxzgoLZG2PWUnZKxp9t2x95yTXX6LlVyPDvkP3LW33fjAE6Xf0iZIjvCP2JVkcXr2MXzSDsfN5UrwqylE0G7+u3vwRzV83o/mzz89v5m/gLan/nV06bcuDj6D7nX6XPRgkxCKOQ7jH7cAJEgcOddkByjj6uKkouJz2Txs6xPMt4g7YkDASEepE7qHjmO2B/jjg+JTiEAD9A8EFSBDeEALwOXc5DgX7jwPPJkmg22OZJDVgB2G7duv/9ZC5snOcE+Y5pplKZNVORXcbNTuqj3nmdLLihmZpnsUQTHKvjd3tmdP1TjPnaeY8zpnz9yrSrg0JpRhgxLUZJI7nURI5zt4DXTshNIqcbpmmfqdMH/0u8FSpL0y3oqBEeQ1lNBgHs3Z236rUATltB0+V+kgr9T23g7tWVhHeAAp+VJurGMFDBRpUd4N5hEurFVa9NuptVe88cJ72Vad91WlfddpXnfZVp33VYxmFKPbNj2ljddvv3HUM2tB8zFOQmmV2dZVPy+ubpLC9691TED3tq05T0JFOQb9ZkfYCbg8xtJjtR2AnDh0CePtrtBcABUq7Ndr1v3/9H65UwSy8JgAA","output":[{"uuid":"0188430e-4c68-0000-2df4-40793d05388a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"vegy51ygdnvw1ny0\", \"$time\": 1684752188.139, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"p\", \"classes\": [\"mb-2\"], \"attr__class\": \"mb-2\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"I can add a code snippet to my product.\"}, {\"tag_name\": \"div\", \"classes\": [\"mt-4\", \"mb-0\"], \"attr__class\": \"mt-4 mb-0\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--has-side-icon\", \"mb-4\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--has-side-icon mb-4\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"mb-6\"], \"attr__class\": \"flex flex-col mb-6\", \"nth_child\": 4, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2421}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-4c68-0001-eb2a-54453fb06eec","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/platform\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/platform\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"3hnz1hglhce8vl5k\", \"$time\": 1684752188.145, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e42ef110e-042aef35510b338-412d2c3d-164b08-188430e42f01bb3\"}, \"offset\": 2416}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-4c68-0002-6ea4-3b0d05101aee","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/platform\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/platform\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"4r1etipqepb7m8xn\", \"$time\": 1684752188.809, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"backend\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"mb-6\"], \"attr__class\": \"flex flex-col mb-6\", \"nth_child\": 4, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e42ef110e-042aef35510b338-412d2c3d-164b08-188430e42f01bb3\"}, \"offset\": 1752}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-4c68-0003-3fe5-c2dfb803996d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"txh2rjlwpqzfn25q\", \"$time\": 1684752188.815, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e458d299-0a26be2f319ee58-412d2c3d-164b08-188430e458e1e11\"}, \"offset\": 1746}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684752193570&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:13.576236+00:00","body":"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","output":[{"uuid":"0188430e-582a-0000-2b14-ad7e278afb7b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"2mvrdgcqmvepz5u8\", \"$time\": 1684752191.57, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"PHP\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"attr__data-attr\": \"select-framework-PHP\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"div\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"FrameworkPanel\"], \"attr__class\": \"FrameworkPanel\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e458d299-0a26be2f319ee58-412d2c3d-164b08-188430e458e1e11\"}, \"offset\": 1997}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-582a-0001-7181-20c3ce2580fe","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend/php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend/php\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pin8vjsmr8mepxwj\", \"$time\": 1684752191.58, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e505ac40-0ebb044514cabf8-412d2c3d-164b08-188430e505b26e1\"}, \"offset\": 1987}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-582a-0002-bea7-d4b125bb0a9a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend/php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend/php\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"mtqwb993ug1ltj2o\", \"$time\": 1684752192.856, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"Continue\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"panel-footer\"], \"attr__class\": \"panel-footer\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 11, \"nth_of_type\": 5}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"InstructionsPanel\", \"mb-8\"], \"attr__class\": \"InstructionsPanel mb-8\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e505ac40-0ebb044514cabf8-412d2c3d-164b08-188430e505b26e1\"}, \"offset\": 710}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-582a-0003-cd42-5039c46a0c3b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/verify?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/verify\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"i2zgz88lw17xt1x0\", \"$time\": 1684752192.862, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e555c203-01d0ef0ffe354a-412d2c3d-164b08-188430e555d12ed\"}, \"offset\": 705}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/engage/?ip=1&_=1684752196089&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:16.094175+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW1WdFlXbHNJam9pZUdGMmFXVnlRSEJ2YzNSb2IyY3VZMjl0SW4wc0lpUjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkdScGMzUnBibU4wWDJsa0lqb2laMUZFU0d4d2VIRkRTR2RWYTJnMWN6Tk1UMVpWVVRWQlkwNTBNV3R0Vm13d2VIWlBZV295YzJZMmJTSXNJaVJrWlhacFkyVmZhV1FpT2lJeE9EZzBNekJsTXpSaU5UTmpZeTB3WkROak5tTTNNRGswTkRFMVpEZ3ROREV5WkRKak0yUXRNVFkwWWpBNExURTRPRFF6TUdVek5HSTJaRE00SWl3aUpIVnpaWEpmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadEluMCUzRA==","output":[{"uuid":"0188430e-61fe-0000-6f49-83e60118a598","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T10:43:16.094175+00:00","sent_at":"2023-05-22T10:43:16.089000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/engage/?ip=1&_=1684752196112&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:16.114065+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW1WdFlXbHNJam9pZUdGMmFXVnlRSEJ2YzNSb2IyY3VZMjl0SW4wc0lpUjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkdScGMzUnBibU4wWDJsa0lqb2laMUZFU0d4d2VIRkRTR2RWYTJnMWN6Tk1UMVpWVVRWQlkwNTBNV3R0Vm13d2VIWlBZV295YzJZMmJTSXNJaVJrWlhacFkyVmZhV1FpT2lJeE9EZzBNekJsTXpSaU5UTmpZeTB3WkROak5tTTNNRGswTkRFMVpEZ3ROREV5WkRKak0yUXRNVFkwWWpBNExURTRPRFF6TUdVek5HSTJaRE00SWl3aUpIVnpaWEpmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadEluMCUzRA==","output":[{"uuid":"0188430e-6212-0000-ba3f-5cbd5ec58bd2","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T10:43:16.114065+00:00","sent_at":"2023-05-22T10:43:16.112000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684752196576&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:16.582542+00:00","body":"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","output":[{"uuid":"0188430e-63e7-0000-35f7-77eaf3880b39","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/verify?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/verify\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pqx96n7zrbq30v6t\", \"$time\": 1684752194.578, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"or continue without verifying\"}, {\"tag_name\": \"button\", \"$el_text\": \"or continue without verifying\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-primary\", \"LemonButton--full-width\", \"LemonButton--centered\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-primary LemonButton--full-width LemonButton--centered\", \"nth_child\": 5, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"ingestion-listening-for-events\"], \"attr__class\": \"ingestion-listening-for-events\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"text-center\"], \"attr__class\": \"text-center\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e555c203-01d0ef0ffe354a-412d2c3d-164b08-188430e555d12ed\"}, \"offset\": 1993}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0001-3713-fae840c9d31f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ca1xryhb43tz0aqr\", \"$time\": 1684752194.59, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 1982}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0002-9a6f-fdc96fab0d9d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"ingestion continue without verifying\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"02dxxmdqmq1icsqc\", \"$time\": 1684752194.591, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 1980}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0003-c4bd-0cee63372ea6","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"o1vi2lr0d7efkvrd\", \"$time\": 1684752195.807, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"Complete\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"panel-footer\"], \"attr__class\": \"panel-footer\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 765}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0004-272b-0bdf0f14875d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"ingestion recordings turned off\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"wxw6rpbifx56jkkv\", \"$time\": 1684752195.811, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"session_recording_opt_in\": false, \"capture_console_log_opt_in\": false, \"capture_performance_opt_in\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 760}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0005-82ae-014d11c490fb","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"completed_snippet_onboarding team setting updated\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ghxomrt7jyayufsg\", \"$time\": 1684752195.936, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"setting\": \"completed_snippet_onboarding\", \"value\": true, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 636}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0006-a1b6-c15b0e084f14","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"activation sidebar shown\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"9ciqdwvb0gm7stqn\", \"$time\": 1684752195.955, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"active_tasks_count\": 5, \"completed_tasks_count\": 2, \"completion_percent_count\": 29, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 617}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0007-2bd1-96f9150acdd3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"89hf5t22vlawns70\", \"$time\": 1684752195.989, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"cloud-announcement\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 582}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0000-38d3-622f0b7db0b3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"g5s1ttbkbuqqjp1f\", \"$time\": 1684752196.006, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"require-email-verification\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 566}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0001-26c9-ed81e4d2f77a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"314lgzt3xfrozlwu\", \"$time\": 1684752196.054, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 518}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0002-e82f-aba2ca288e21","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"client_request_failure\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ps0zh0lhjw7ntghy\", \"$time\": 1684752196.07, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"pathname\": \"/api/billing-v2/\", \"method\": \"GET\", \"duration\": 65, \"status\": 404, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 502}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0003-e240-08053caca9aa","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"7muhndo38d1ixe0p\", \"$time\": 1684752196.089, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 483}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0004-ced5-5ad043dcd37a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"qiacra8lvetpncbm\", \"$time\": 1684752196.09, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 482}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0005-b2eb-104477bc52f8","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"xnwub6hl3xv00xkq\", \"$time\": 1684752196.112, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 460}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0006-74b6-493aaf93af42","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yxtxo6923bqvqrk0\", \"$time\": 1684752196.113, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 459}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684752239786&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:59.790498+00:00","body":"H4sIAAAAAAAAA61We2/bNhD/KoaRPyeHessBhqFpNqRttqDIUgwNCoEiKYkxJWoS5UeLfvfd0YptOY+2QwEDhu75ux/vjrz7MhVLUZvp2fSE9kYz2pi+FdNfpk2rG9EaKbrp2ZfpiYa/6Z+UTa5vJv+AGgTpUrSd1DUoXDJzwxlBedbqVSdaEP4hW5HrNQq5WEomUrNpBCguRLcwukEF69sW0qd9q0BRGtOcnZ4qgKFK3ZmzhBByagF2v+k607Tlsi5SpqtGCSP4r6btEewJWkOAsScqGmrKmlaYdgh0AHJfgev6B2JF66KnBTqJ2rm9QZeOtULUaSlkUUKqeRzshSvJTQlBQkJAuJRi1ejW7GyTAIPvxA/WgY9iJTNIsxIZJoGPQ1ZnYTzzUC5rgGVSyUEaqzLM1T13g1AnUq1QbyRW6EZJEIee50ezOIHYXHZG1mzwK95fXKpm/e/ry+J2UYadf3X94fZ9+Ir9ZdxF9UGR9fKa3ntdHlUHR2Zd3QRKIMIPstBnzCHcZxGLyTwI3JAnTuB63GM+d9woyEji7M0j7icYrEdW/wcK2aVcVDqFZrwXDKjMqeoEBCxa3Te2M3eqKRnyOr4bCQfOnzihSyMn51EQhUESzQn2im4LWsvP1GxJ3nl5czLfegVcuM58HsaxR10ShAEiqTtDa4Yt8WSXTr8CqoMJSoF8minBUygdjjTtJAfnB/yUGbkUaS6oNc4VLaCau0+gOpSlDd0oTTlWCgkayFbqYtQigW/nrhVUVQgOTAR3mJJsUWrgHXSiolJZHHiidAlfCG2HplOULV7Qn8AcC5hTHOqnR7STRd3bgd6a4pByDWkR4+OhtIP4sA0sVLsLxME8opUSlR3YM9hSBsgY5rhbFmDOFO06XE530ytokfoNAz/gjxrTpqnVgu1e9aAZpm/qCmywrexhUEfCXCrcSbWukcOtDGf4HFYatM2ETLwAfjvdulI1phwYWq1Ws5U/g2479SxFFvUQWrPedgeYW453GtpK6pSSc4HMDdutNmXKSqlgfoAX/NL5wN6Wp9SINcKHJhwz1VAs/Jiq894YXaepfJ6ykcnLCI5yZtbTnvIe13MYQHHw5TgGbxzabo7lMHum75ymldUT2lo7ANPgNXakKWnnjKscmm6H8tnaJ08CG0vHsMa6PaixfAfph1jlcjkm8RUuELvDbmCxnFNbhO5EOlT26Fi/5fDz4eyOpDEO3mLN2sFpaTK4J74P38Af+k/Qe7L1/clQvwPLOCXc/KOUwbdS7qPsalN0o3vzOPVgMDk2fBGA/ySAHyZpi8Re063WRznhPfHy3GuOgzlMWQmrG1e/3aujON6jOMCB0Qu78JqSpfeX7KKPK//jsn5TN4tsXb1t3/2ebd72i82rj68/b96JK9L9vT737a0BFOOVcfxQieOcOMT3CQ+5l/hJ7ouXHioJ2PgYbiVrrlfH0eY+CSOHBDGdE0oy4vpZ8FI46oUC+xSenwW8o8Q4YOTywIs5xMuCuSB+zHiUPP+OAnN4vWQhPjF0nncCVqoPV8rXT/8B8mE6/L4LAAA=","output":[{"uuid":"0188430f-0caf-0000-dc00-030ac4424349","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"7lh5fljd145o8ilw\", \"$time\": 1684752236.783, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"svg\", \"classes\": [\"LemonIcon\"], \"attr__class\": \"LemonIcon\", \"attr__width\": \"1em\", \"attr__height\": \"1em\", \"attr__fill\": \"none\", \"attr__viewBox\": \"0 0 24 24\", \"attr__xmlns\": \"http://www.w3.org/2000/svg\", \"attr__focusable\": \"false\", \"attr__aria-hidden\": \"true\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__icon\"], \"attr__class\": \"LemonButton__icon\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-primary\", \"LemonButton--no-content\", \"LemonButton--has-icon\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-primary LemonButton--no-content LemonButton--has-icon\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ActivationSideBar__close_button\"], \"attr__class\": \"ActivationSideBar__close_button\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ActivationSideBar__content\", \"pt-2\", \"px-4\", \"pb-16\"], \"attr__class\": \"ActivationSideBar__content pt-2 px-4 pb-16\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ActivationSideBar\"], \"attr__class\": \"ActivationSideBar\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\"], \"attr__class\": \"SideBar SideBar__layout\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 3000}","now":"2023-05-22T10:43:59.790498+00:00","sent_at":"2023-05-22T10:43:59.786000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684752248800&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:44:08.814768+00:00","body":"H4sIAAAAAAAAA51W607cOBR+lVHETwLOPUFarUrZFduyiyqWalVURY7tJGYSO7WdubTi3fc4E4bJMEDVX0zO7fvO8blw98NhCyaMc+Yc4d5IgjvTK+YcO52SHVOGM+2c/XCOJPxx/sZkdn0z+w/UIMgXTGkuBSg8dOJFJ8jKCyWXmikQ/skVK+XKCilbcMJys+4YKC6YnhvZWQXplQL4vFcNKGpjurPT0wZoNLXU5ixFCJ0OBPXvUhQSK8pFlRPZdg0zjP5mVG/JHllrCDD1tIoOm1rg1sKOgXZIPmXgecGOuMGi6nFlnZhwb2+siyaKMZHXjFc1QGVJ+CRccmpqCBIhBMIFZ8tOKrO1TUMbfCt+tA4DK254ATBLVlgQ+Nit6kmUnPhWzgXQMjmnIFUJWSA5/1bW82CFqt7qDbcZenEaJpHvh8kJQsmxQ7k2XJDRr/p0cdl0q2/vL6vbeR3p4Or68+2n6B35x3jz9nODVotrfO/rMm53nmxw9VJIAbEgLKKAEBfRgMQkQVkYehFN3dDzqU8C6npxWKDUfTKPaZDaYL2t6i+w4DqnrJU5NOM9I1DKEjeaQcBKyb4bOnOrctCI6wZezFx4f+RGHo7dksZhHIVpnCHbK1JVWPDv2GyKvPXyM5RtvELKPDfLoiTxsYfCKLRMhDZYENsSB7vUeQBWOxOUQ/Fx0TCaQ+rwpLnmFJwf+WNi+ILlJcODcdngCrK5+wqqXVne4XUjMbWZAkAHaLWsJi0SBsPcKYab1pIDE0Zd0nAyryXUHXSsxbwZeNgXxQv4stS2bHSDyfwV/RHMMYM5tUN9eEQ1r0Q/DPTG1A4plQBrOT4fymEQH7fBQHXYBWxnHq1Vw9phYM9gSxkoxjjHusMC7EmDtbbb6c65gh4R570xUuSwHISxGw1qiY1RILCG4HfQ6tgRps5JzRvoTkC1X7IcuW1Y5IatbH9dwUPMWqnYbFwkD8cTXsUQesjvZZ+XeINi58t1O8VbrNb7YmhC0+sdbbt27YS1K9c231PSY3G3nF4sxewQ7FQ4BZ1ZyNkj4KR+0X799ipE+WJagLEoz55qW6yNmGKDXftzq3LN0KGvokdvodv+dHHXuS+2zDOLCSCs7wlg8BbgDeyAc6xAOP7K4dSsZX8AeTSY7RtOCMAJ+gkCrzX4QYcNk82xkfKNpPdDFJLavhy7sIYRtitgOISTOP6zOFADI+fMroyuJvn9Jbnokzb4shB/iW5erNoP6uMfxfpDP1+/+/L++/oju0L639V5MGwPKLFdHfsHK0lK5KIgQDSifhqkZcBeO1gp2AQ23JILKpf70bIARbGLwgRnCKMCeUERvhYO+xGLN/+GVHBP2TRg7NHQTyjEK8KMoSAhNE5fvqdgDlesiOypkWWpGSwYL8nQw9f/AYmT89LGCQAA","output":[{"uuid":"0188430f-2fef-0000-25cb-b19f141897ba","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"r7cv0okqfhk3x0gu\", \"$time\": 1684752247.007, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Load more events\"}, {\"tag_name\": \"button\", \"$el_text\": \"Load more events\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"my-8\", \"mx-auto\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary my-8 mx-auto\", \"nth_child\": 5, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"events\"], \"attr__class\": \"events\", \"attr__data-attr\": \"events-table\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\"], \"attr__class\": \"SideBar SideBar__layout\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 1790}","now":"2023-05-22T10:44:08.814768+00:00","sent_at":"2023-05-22T10:44:08.800000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} From 8d57a3ab8ea7e1c82637874de6dbed9214dc5515 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 23 May 2023 11:59:30 +0100 Subject: [PATCH 013/249] Add processing sink for events (#11) * Add processing sink for events * fmt * Make tests async * Make clippy happy * Handle errors and batch events --- Cargo.lock | 1 + Cargo.toml | 1 + src/capture.rs | 86 +++++++++++++++++++++++++++++++++++++++++++------- src/event.rs | 32 ++++++++++++------- src/lib.rs | 6 ++-- src/main.rs | 1 + src/router.rs | 15 ++++++++- src/sink.rs | 31 ++++++++++++++++++ 8 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 src/sink.rs diff --git a/Cargo.lock b/Cargo.lock index 98ca254f338bd..0b9ede977c52f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,6 +136,7 @@ name = "capture" version = "0.1.0" dependencies = [ "anyhow", + "async-trait", "axum", "axum-test-helper", "base64", diff --git a/Cargo.toml b/Cargo.toml index 5f73604d06415..dfd2756ce7317 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ anyhow = "1.0" flate2 = "1.0" base64 = "0.21.1" uuid = { version = "1.3.3", features = ["serde", "v4"] } +async-trait = "0.1.68" [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/src/capture.rs b/src/capture.rs index 42b2718b29799..dc7a7fd960c8b 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,31 +1,40 @@ use std::collections::HashSet; +use std::sync::Arc; +use anyhow::Result; use bytes::Bytes; use axum::{http::StatusCode, Json}; // TODO: stream this instead -use axum::extract::Query; +use axum::extract::{Query, State}; +use uuid::Uuid; use crate::api::CaptureResponseCode; +use crate::event::ProcessedEvent; + use crate::{ api::CaptureResponse, event::{Event, EventQuery}, - token, + router, sink, token, }; pub async fn event( + state: State, meta: Query, body: Bytes, ) -> Result, (StatusCode, String)> { + tracing::debug!(len = body.len(), "new event request"); + let events = Event::from_bytes(&meta, body); let events = match events { Ok(events) => events, - Err(_) => { + Err(e) => { + tracing::error!("failed to decode event: {:?}", e); return Err(( StatusCode::BAD_REQUEST, String::from("Failed to decode event"), - )) + )); } }; @@ -33,7 +42,7 @@ pub async fn event( return Err((StatusCode::BAD_REQUEST, String::from("No events in batch"))); } - let processed = process_events(&events); + let processed = process_events(state.sink.clone(), &events).await; if let Err(msg) = processed { return Err((StatusCode::BAD_REQUEST, msg)); @@ -44,7 +53,24 @@ pub async fn event( })) } -pub fn process_events(events: &[Event]) -> Result<(), String> { +pub fn process_single_event(_event: &Event) -> Result { + // TODO: Put actual data in here and transform it properly + Ok(ProcessedEvent { + uuid: Uuid::new_v4(), + distinct_id: Uuid::new_v4().simple().to_string(), + ip: String::new(), + site_url: String::new(), + data: String::from("hallo I am some data 😊"), + now: String::new(), + sent_at: String::new(), + token: String::from("tokentokentoken"), + }) +} + +pub async fn process_events( + sink: Arc, + events: &[Event], +) -> Result<(), String> { let mut distinct_tokens = HashSet::new(); // 1. Tokens are all valid @@ -67,20 +93,50 @@ pub fn process_events(events: &[Event]) -> Result<(), String> { return Err(String::from("Number of distinct tokens in batch > 1")); } + let events: Vec = match events.iter().map(process_single_event).collect() { + Err(_) => return Err(String::from("Failed to process all events")), + Ok(events) => events, + }; + + if events.len() == 1 { + let sent = sink.send(events[0].clone()).await; + + if let Err(e) = sent { + tracing::error!("Failed to send event to sink: {:?}", e); + + return Err(String::from("Failed to send event to sink")); + } + } else { + let sent = sink.send_batch(&events).await; + + if let Err(e) = sent { + tracing::error!("Failed to send batch events to sink: {:?}", e); + + return Err(String::from("Failed to send batch events to sink")); + } + } + Ok(()) } #[cfg(test)] mod tests { + use crate::sink; use std::collections::HashMap; + use std::sync::Arc; use serde_json::json; use super::process_events; use crate::event::Event; + use crate::router::State; + + #[tokio::test] + async fn all_events_have_same_token() { + let state = State { + sink: Arc::new(sink::PrintSink {}), + }; - #[test] - fn all_events_have_same_token() { let events = vec![ Event { token: Some(String::from("hello")), @@ -94,11 +150,16 @@ mod tests { }, ]; - assert_eq!(process_events(&events).is_ok(), true); + let processed = process_events(state.sink, &events).await; + assert_eq!(processed.is_ok(), true); } - #[test] - fn all_events_have_different_token() { + #[tokio::test] + async fn all_events_have_different_token() { + let state = State { + sink: Arc::new(sink::PrintSink {}), + }; + let events = vec![ Event { token: Some(String::from("hello")), @@ -112,6 +173,7 @@ mod tests { }, ]; - assert_eq!(process_events(&events).is_err(), true); + let processed = process_events(state.sink, &events).await; + assert_eq!(processed.is_err(), true); } } diff --git a/src/event.rs b/src/event.rs index 04e6c8ee571b2..df6d200db90af 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::io::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -41,26 +42,35 @@ impl Event { /// could be more than one. Hence this function has to return a Vec. /// TODO: Use an axum extractor for this pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result> { + tracing::debug!(len = bytes.len(), "decoding new event"); + match query.compression { Some(Compression::GzipJs) => { - let d = GzDecoder::new(bytes.reader()); - Ok(serde_json::from_reader(d)?) + let mut d = GzDecoder::new(bytes.reader()); + let mut s = String::new(); + d.read_to_string(&mut s)?; + + tracing::debug!(json = s, "decoded event data"); + + let event = serde_json::from_str(s.as_str())?; + + Ok(event) } None => Ok(serde_json::from_reader(bytes.reader())?), } } } -#[derive(Default, Debug, Deserialize, Serialize)] +#[derive(Clone, Default, Debug, Deserialize, Serialize)] pub struct ProcessedEvent { - uuid: Uuid, - distinct_id: String, - ip: String, - site_url: String, - data: String, - now: String, - sent_at: String, - token: String, + pub uuid: Uuid, + pub distinct_id: String, + pub ip: String, + pub site_url: String, + pub data: String, + pub now: String, + pub sent_at: String, + pub token: String, } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index b07a9eb167f1d..d8cf49f4b620a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ -mod api; -mod capture; pub mod event; pub mod router; + +mod api; +mod capture; +mod sink; mod token; diff --git a/src/main.rs b/src/main.rs index 3fcd2574b7aef..85412e19adc35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod api; mod capture; mod event; mod router; +mod sink; mod token; #[tokio::main] diff --git a/src/router.rs b/src/router.rs index 52478b634d59e..9af45c76454c3 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,8 +1,20 @@ -use crate::capture; +use std::sync::Arc; + use axum::{routing::post, Router}; use tower_http::trace::TraceLayer; +use crate::{capture, sink}; + +#[derive(Clone)] +pub struct State { + pub sink: Arc, +} + pub fn router() -> Router { + let state = State { + sink: Arc::new(sink::PrintSink {}), + }; + Router::new() // TODO: use NormalizePathLayer::trim_trailing_slash .route("/capture", post(capture::event)) @@ -14,4 +26,5 @@ pub fn router() -> Router { .route("/engage", post(capture::event)) .route("/engage/", post(capture::event)) .layer(TraceLayer::new_for_http()) + .with_state(state) } diff --git a/src/sink.rs b/src/sink.rs new file mode 100644 index 0000000000000..44592dcf7b409 --- /dev/null +++ b/src/sink.rs @@ -0,0 +1,31 @@ +use anyhow::Result; +use async_trait::async_trait; + +use crate::event::ProcessedEvent; + +#[async_trait] +pub trait EventSink { + async fn send(&self, event: ProcessedEvent) -> Result<()>; + async fn send_batch(&self, events: &[ProcessedEvent]) -> Result<()>; +} + +pub struct PrintSink {} + +#[async_trait] +impl EventSink for PrintSink { + async fn send(&self, event: ProcessedEvent) -> Result<()> { + tracing::info!("single event: {:?}", event); + + Ok(()) + } + async fn send_batch(&self, events: &[ProcessedEvent]) -> Result<()> { + let span = tracing::span!(tracing::Level::INFO, "batch of events"); + let _enter = span.enter(); + + for event in events { + tracing::info!("event: {:?}", event); + } + + Ok(()) + } +} From 63a960949f2d08e80e2c07358ff3881aea1f4c14 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 23 May 2023 13:53:46 +0100 Subject: [PATCH 014/249] Handle x-www-form-urlencoded requests to /e/ (#12) --- Cargo.lock | 1 + Cargo.toml | 1 + src/capture.rs | 22 +++++++++++++++++-- src/event.rs | 27 +++++++++++++++-------- tests/django_compat.rs | 45 ++++++++++++++++++++++----------------- tests/requests_dump.jsonl | 31 +++++++++------------------ 6 files changed, 75 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b9ede977c52f..2d2ffd6a7267f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,6 +146,7 @@ dependencies = [ "mockall", "serde", "serde_json", + "serde_urlencoded", "time", "tokio", "tower-http", diff --git a/Cargo.toml b/Cargo.toml index dfd2756ce7317..23cfe2d24d257 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ flate2 = "1.0" base64 = "0.21.1" uuid = { version = "1.3.3", features = ["serde", "v4"] } async-trait = "0.1.68" +serde_urlencoded = "0.7.1" [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/src/capture.rs b/src/capture.rs index dc7a7fd960c8b..e8edaa005b4c9 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::ops::Deref; use std::sync::Arc; use anyhow::Result; @@ -7,6 +8,8 @@ use bytes::Bytes; use axum::{http::StatusCode, Json}; // TODO: stream this instead use axum::extract::{Query, State}; +use axum::http::HeaderMap; +use base64::Engine; use uuid::Uuid; use crate::api::CaptureResponseCode; @@ -14,18 +17,31 @@ use crate::event::ProcessedEvent; use crate::{ api::CaptureResponse, - event::{Event, EventQuery}, + event::{Event, EventFormData, EventQuery}, router, sink, token, }; pub async fn event( state: State, meta: Query, + headers: HeaderMap, body: Bytes, ) -> Result, (StatusCode, String)> { tracing::debug!(len = body.len(), "new event request"); - let events = Event::from_bytes(&meta, body); + let events = match headers + .get("content-type") + .map_or("", |v| v.to_str().unwrap_or("")) + { + "application/x-www-form-urlencoded" => { + let input: EventFormData = serde_urlencoded::from_bytes(body.deref()).unwrap(); + let payload = base64::engine::general_purpose::STANDARD + .decode(input.data) + .unwrap(); + Event::from_bytes(&meta, payload.into()) + } + _ => Event::from_bytes(&meta, body), + }; let events = match events { Ok(events) => events, @@ -38,6 +54,8 @@ pub async fn event( } }; + println!("Got events {:?}", &events); + if events.is_empty() { return Err((StatusCode::BAD_REQUEST, String::from("No events in batch"))); } diff --git a/src/event.rs b/src/event.rs index df6d200db90af..c5f09db61cd4f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -4,7 +4,7 @@ use std::io::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::Value; -use anyhow::Result; +use anyhow::{anyhow, Result}; use bytes::{Buf, Bytes}; use flate2::read::GzDecoder; use uuid::Uuid; @@ -28,6 +28,11 @@ pub struct EventQuery { sent_at: Option, } +#[derive(Debug, Deserialize)] +pub struct EventFormData { + pub data: String, +} + #[derive(Default, Debug, Deserialize, Serialize)] pub struct Event { #[serde(alias = "$token", alias = "api_key")] @@ -44,20 +49,24 @@ impl Event { pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result> { tracing::debug!(len = bytes.len(), "decoding new event"); - match query.compression { + let payload = match query.compression { Some(Compression::GzipJs) => { let mut d = GzDecoder::new(bytes.reader()); let mut s = String::new(); d.read_to_string(&mut s)?; - - tracing::debug!(json = s, "decoded event data"); - - let event = serde_json::from_str(s.as_str())?; - - Ok(event) + s } - None => Ok(serde_json::from_reader(bytes.reader())?), + None => String::from_utf8(bytes.into())?, + }; + + tracing::debug!(json = payload, "decoded event data"); + if let Ok(events) = serde_json::from_str::>(&payload) { + return Ok(events); + } + if let Ok(events) = serde_json::from_str::(&payload) { + return Ok(vec![events]); } + Err(anyhow!("unknown input shape")) } } diff --git a/tests/django_compat.rs b/tests/django_compat.rs index f76fea0ba8a24..24d526375f7bf 100644 --- a/tests/django_compat.rs +++ b/tests/django_compat.rs @@ -1,29 +1,19 @@ use axum::http::StatusCode; use axum_test_helper::TestClient; +use base64::engine::general_purpose; +use base64::Engine; use capture::event::ProcessedEvent; use capture::router::router; use serde::Deserialize; -use serde_json::Value; use std::fs::File; use std::io::{BufRead, BufReader}; -use time::OffsetDateTime; - -/* - "path": request.get_full_path(), - "method": request.method, - "content-encoding": request.META.get("content-encoding", ""), - "ip": request.META.get("HTTP_X_FORWARDED_FOR", request.META.get("REMOTE_ADDR")), - "now": now.isoformat(), - "body": base64.b64encode(request.body).decode(encoding="ascii"), - "output": [], -*/ #[derive(Debug, Deserialize)] struct RequestDump { path: String, method: String, - #[serde(alias = "content-encoding")] content_encoding: String, + content_type: String, ip: String, now: String, body: String, @@ -32,25 +22,40 @@ struct RequestDump { static REQUESTS_DUMP_FILE_NAME: &str = "tests/requests_dump.jsonl"; -#[ignore] #[tokio::test] async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let file = File::open(REQUESTS_DUMP_FILE_NAME)?; let reader = BufReader::new(file); + for line in reader.lines() { - let request: RequestDump = serde_json::from_str(&line?)?; + let case: RequestDump = serde_json::from_str(&line?)?; - if request.path.starts_with("/s") { - println!("Skipping {} dump", &request.path); + if !case.path.starts_with("/e/") { + println!("Skipping {} test case", &case.path); continue; } - println!("{:?}", &request); - // TODO: massage data + let raw_body = general_purpose::STANDARD.decode(&case.body)?; + assert_eq!( + case.method, "POST", + "update code to handle method {}", + case.method + ); let app = router(); let client = TestClient::new(app); - let res = client.post("/e/").send().await; + let mut req = client.post(&case.path).body(raw_body); + if !case.content_encoding.is_empty() { + req = req.header("Content-encoding", case.content_encoding); + } + if !case.content_type.is_empty() { + req = req.header("Content-type", case.content_type); + } + if !case.ip.is_empty() { + req = req.header("X-Forwarded-For", case.ip); + } + let res = req.send().await; + assert_eq!(res.status(), StatusCode::OK, "{}", res.text().await); } Ok(()) diff --git a/tests/requests_dump.jsonl b/tests/requests_dump.jsonl index ee8ba3d5e8e50..b62f1c61665a7 100644 --- a/tests/requests_dump.jsonl +++ b/tests/requests_dump.jsonl @@ -1,21 +1,10 @@ -{"path":"/s/?compression=gzip-js&ip=1&_=1684752074919&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:01.993704+00:00","body":"H4sIAAAAAAAAA+2Xb0vcMBjAv0oJvphgvfxrmvbV2M0xcOgNdI6pHLkmtdXa1iT1dhO/+9KxTdlgR+WmHJdXpcmTp3maX35NT++AulW1BSnYMrVoTdFYsANa3bRK21IZkN499EylsKJvsYtWgZTtgF8NbdVdlLXLovVczUZZU5umUq9Rn0ssqkbIPqpyz6pckNK60a7LapG5PKegqadfCz390R68Kqxt09GoajJRFY2xKceEjowVtsxGWdHVV+He2/H45PjwZPfSpBGKY5Yisu0yHupRS3ebWishF/0IlRWivlBPSMoQTAnbBuePSjgFZ+CNkMH7o6NJ0I/tTBrA4AyA8/t7V095rVzrdQtSxDiNIwxjlGDuuraMMqZ0dZYuDUCcU+L6mIQMh5AhJBlHEaUSZiFFWOKMyBAxOoM8fAhGiEjmqtyal7Vs5n/mwjhiMIQEilxAnktCYvqPbMTFR/0qNFeqX7q2yKYqLmllxgflx/xgX3youuryZvHpYjKBE6bjb8f8czt+142/VPudGylLY8s6s78nQiidzRLCGclDiKKcKUEUnomM84eJ4BiqGPYT+RmeZAoC946aPDfKoYgT7l7Zi5A5F7p+DOatqLonwYN5iqL/QGRC/yZyr77pVKdkkIuychet3L2xQe42k1ZWL4KyDhiEcAmnied0XTjdJIOSBDFP5iAyk8TtZS/QlQqU8iUGdZz6L/26cLphAvVgDgXTn0BXLVBMlwoUQ8/pQE5jL9BnEKgHczCY7szuBbpSgSbL/uEdp8hzuiacbphAPZhDwYy8QF/gBIo9p2vC6YYJ1IM5FEzqBbpigSK8XKDEczqY0/Pv4oS7EYgfAAA=","output":[{"uuid":"0188430d-40cb-0000-82bb-5cab205fc9d8","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40ca-0000-c581-b136488335da\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+2XXS8DURCG35+y6RUJarfVD1fiI3HHBXGBSFVRSqu2aMRfxzNn28YNkdhoxWRydqczc+Zjz7tvtm+vh3pWQamG6qmFtqpIFS1wLehMDTyNkdXieupooAu1dTuyFtRHHtl7qqKa6uK559rBsqYYf5arR54h1i73sw8ZLe6B1Znka4WMXdZ4b4reIPe4v0PuVudET7rEd/JhT6Q5bCnSI7aIWM0m+zvYu/SWYq8pUUllvPdhwpSJmqH/S+a71bUWtaVNbSAH2tcO1yVdEb2qFaaqIhX0mCzzkz53qF+kbplY669PXzbtcFKlFSo08F2g/06vFdYyegnNej3+4kTsyR6x1oMt0rb2kF20ce1ByBuRMQqRlu8FGZ9UWzdMlkXfUCPLa13UmKFKRwl7q1jqaLXRzlnD4CMR/ZD/cwQ+hHMa/OI52vMyzK38GczVyfFdzG2R9y48T1uGvnOi2uF07JdNlvmtJ/Nm77vZ7XyG6IYJw85ykJ/jsz6j+HSO/A8cWQJ/ZnWOdI7MmyPL9P9zlswQWnOWdJacMkv6l6SzZP4smVA1H5Y0zVnSWXKaLDm7GHSW/MssWc/lH3eG0NhZ0llyyiwZO0s6S870t2TiLOksOWWWTJwlnSVzZ8k4YCwfliyx81jvS12b9+QeAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752071928, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752071929, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073916, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073918, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073918, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073920, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073920, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073921, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073921, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073922, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752073922, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752073923, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2988}","now":"2023-05-22T10:42:01.993704+00:00","sent_at":"2023-05-22T10:41:14.919000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684752098970&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:01.995810+00:00","body":"H4sIAAAAAAAAA+2Z207bMBiAXyWyuBgSoT7FcXI1rWOaxASdBGMaoMqNHRIISbAdug7x7nOmbaBNWteqCJX6Kor9+48PXz458ekdULeqtiAFW6YWrSkaC3ZAq5tWaVsqA9K7h5qxFFb0JXbWKpCyHfCroK26i7J2WbSeqskga2rTVOo16nOJWdUI2UdV7lmVC1JaN9pVWS0yl+cUNPX4a6HHP8qDV4W1bToYVE0mqqIxNuWY0IGxwpbZICu6+ircezscnhwfnuxemjRCccxSRLZdxkM9aOluU2sl5KxvobJC1BdqiaQMwZSwbXD+aAin4Ay8ETJ4f3Q0Cvq2nUkDGJwBcH5/78ZTXitXet2CFDFO4wjDJEri2FVtGWVM6cZZujQAcU4JjBGTkOEQMoQk4yiiVMIspAhLnBEZIkYnkIcPwQgRydwot6ZlLZvpn7kwjhgMIYEiF5DnkpCY/iMbcfFRvwrNleqXri2ysYpLWpnhQfkxP9gXH6quuryZfboYjeCI6fjbMf/cDt91wy/VfudaytLYss7s744QSieThHBG8hCiKGdKEIUnIuP8oSM4hiqGfUd+hieZgsDNUZPnRjkUcZKg+51nIXMqdP0YzFtRdUvBg3mKoicgMqF/E7lX33SqUzLIRVm5i1bu3tggdy+TVlbPgrIOGIRwDqfcc7oop/CZON0kg8YJJZ7MhchEEEfeoCs2KOVzFOpApR7UdQF1wxTqyVyYTDdlXqErVWgybxfqQHU+8KCuB6gbplBP5tqQ+XIV+j+7UOZBXRRU94XpFfr0CvVkLkwm9gpdsUIxna9Q/89+bUDdMIV6Mhcm058mrVqhCM9XqD9OWgLU8++WAqIwkh8AAA==","output":[{"uuid":"0188430d-40cd-0000-7103-c248d33d28b5","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40cc-0000-fcd3-63fc3328fd1f\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+1ZXU/CUAw9P2XhSROVr30wn4xK4hs+aHxAQhAQ0MEmDJAY/7p6egeEF40JC8PYNHcrbe9pu9udLOHzo4435BBjgQhdaqew4OKI1xw6aNHTWlolLkKAKXoYYLS05jCmzLn3AXm0EdIz4TWg5QxF+hOsiDgLWkPeOxuIEjfjCtZ4XYMYcq32xtRbxF7VV+dd8jTxij59zY09Fg5oiykRY/MUydnm/oD2kLXFtFdQQhk2vRPTYcyO2qb+Pvsb4RnHqOISF5Q73KLG6wmeGH0Kh115FJd6kSiH6zprzJ9nXpuxUt+YdUm3i3WWrsnQoq9HfTe1ulwF6mVqUmvjhxORJ3vPdW5sFq5wQ7mmtso9NbgWES0TKXjvlNVJDTBkZ0n0kDkSXKmiwh48VlTiXp933zzFZOe+zeCcEWOD//0Ezsw5TXd4jiViyMw5f2bmfGL8duaqxH0xz1OWTN8jowbmdOSXdJb4pSbxJu+72OV8FtRlJmR2Cka2n8/Kns6ncuR/4EjPvD9l5UjlyNQ50mb927NkMqG2sqSyZMYsaStLKkumzpJ+Kt+SyYQ6ypLKkhmzpKMsqSy519+SrrKksmTGLOkqSypLps6SJWZNiyU9ZUllyYxZUv+7UZZMnyWLZsbSYUn596aBL0Ml6tzkHgAA\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752095977, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752095978, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097943, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097944, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097944, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097945, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097945, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097946, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097946, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097947, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752097947, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752097948, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2991}","now":"2023-05-22T10:42:01.995810+00:00","sent_at":"2023-05-22T10:41:38.970000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684752114005&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:02.050911+00:00","body":"H4sIAAAAAAAAA+2Y3WrbMBSAX8WIXqxQN/qzLPtqLOsYdLQZtOtYW4JiybVb13YluVlW8u6Tx7aUDZYlZIwQXRlLR8c60ucPocsnoB5VbUEK9kwtWlM0FhyAVjet0rZUBqRPi56xFFb0LXbWKpCyA/Cjoa26m7J2WbSeqskga2rTVOol6nOJWdUI2UdV7luVC1JaN9p1WS0yl+cSNPX4c6HH39qDF4W1bToYVE0mqqIxNuWY0IGxwpbZICu6+i48ej0cXpyfXhzemjRCccxSRPZdxlM9aOlhU2sl5KwfobJC1DdqjaQMwZSwfXD9rIRLcAVeCRm8PTsbBf3YzqQBDK4AuJ7PXT3lvXKt9y1IEeM0jjBCCELouvaMMqZ0dZYuDUCcUwJjxCRkOIQMIck4iiiVMAspwhJnRIaI0Qnk4SIYISKZq3JvWtaymf6aC+OIwRASKHIBeS4JiekfshEXH/W70NypfuvaIhuruKSVGZ6U7/OTY/Gu6qrbh9mHm9EIjpiOv5zzj+3wTTf8VB13bqQsjS3rzP6cCKF0MkkIZyQPIYpypgRReCIyzhcTwTFUMewn8j08yRQEbo2aPDfKoUggxPOD/0LmVOj6OZiPourWggfzFEX/gMiE/k7kUf3QqU7JIBdl5R5auXdjg9z9TFpZPQvKOmCOwyWcIs/ptnC6SwbFCfdkrkYmgtgtmTfoRg2KErzEoY5UJwRP6naQumMO9WSuTKY7uHuHbtShybJjqAOVeFC3BdQdU6gnc1UyUeIVumGFUr5codSDui2g7phCPZlbQ6ZXqAd1K0DdMYVGnsxVyeReoZu+DP2Lu1AP6hqgXn8FVjpm4JMfAAA=","output":[{"uuid":"0188430d-40cc-0004-71c9-b5d5762c946c","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40cc-0003-1759-ec61e7e52da5\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+1Yy07CUBA9n9Kw0kTFlvJcGZXEHS40LpCQCghoeQgFJMZfV8/cSwkbiYmVR5xMbjudmTsP7ukJ6edHFW9IIcIcQ7SoleAghyNeU2gioCdYWCVuiBATtNFFf2FNYUSZce8D0mhgQM+Y15CWM7j021xD5pnTOuC9uZJR4qZc4TJfy2QccMV7I+oBc8f9VXmXOnW8okNffWWPgwPaIsqQsWmK1Gxwf0j7gL1FtBfgIQOf3rGZMOJEDdN/h/P18YxjlHGJC8odblHh9QRPjC4hy6nylBx1l1kOl31WWD/Nuj5jpb8R+5Jp58sqLVMhoK9NfTO95rhOqWeoSa+1NSciv+w917mxObjCDeWaWlx7YvI6zOiYSMn3TolPqoseJ7PRPdaweaWLAmfIsyOPT67pScTu3DUMzhgxMvm/R+DUnNNkg+foMYdgLrs3mCsyx08xV2beF/N7yhL0PTKqa05HnmQy65eexGvfd7HL+cypCyYEOxZbv8enu6P4VI78Dxzp8f0p7CwGlSP3mSNd1vUS4EmLUU95UnlyyzzpKU8qTybOk8VE/k1ahGaUJZUlt8ySGWVJZcnEWdJn/0mxpK8sqSy5ZZb0lSWVJZUllSWVJddgMKssqSz5B18mk/suKQit4QuIGGIM5h4AAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752111000, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752111001, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112981, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112982, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112982, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112983, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112983, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112984, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112984, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112984, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752112985, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752112985, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 3002}","now":"2023-05-22T10:42:02.050911+00:00","sent_at":"2023-05-22T10:41:54.005000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684752101977&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:02.048295+00:00","body":"H4sIAAAAAAAAA+2Ub0vcMBjAv0oJvphgvfxrmvbV2M0xcOgNdI6pHLkmtdWa1CT1dhO/+1LZpmzgcXKbDPuq9MnzPM2T/Po7vgHqWmkPcrDhtGhdZTzYAq01rbK+Vg7kN/crUym86CN+0SqQsy3wM9A23VmtQxdr52o2Kox2plGvUd9LLBojZJ/VhG81IUlZa2xY8lYUoc8xMHr6tbLTu3j0qvK+zUejxhSiqYzzOceEjpwXvi5GRdXpi3jn7Xh8dLh/tH3u8gSlKcsR2Qwd9+2opdtGWyXkoq9QRSX0mXpCU4ZgTtgmOH0wwjE4AW+EjN4fHEyivrZzeQSjEwBOb2/DPPWlCtHLFuSIcZomGGY8S5OwtOGUc3WYsw5tAOKcEpgiJiHDMWQIScZRQqmERUwRlrggMkaMziCP75MRIpKFKTfmtZZm/nsvjBMGY0igKAXkpSQkpY90IyE/6W/BXKj+6tqqmKq0po0b79Ufy71d8aHpmvOrxaezyQROmE2/HfLP7fhdN/7S7HahUtbO17rwvzZCKJ3NMsIZKWOIkpIpQRSeiYLz+43gFKoU9hv5kZ4VCoJwRqYsnQooEgjh7dazkDkXVj8E81o03ZPgwTxHyV8gMqN/ErmjrzrVKRmVom7Cw6rw7nxUhp/JKm8XUa0jFg51Cads4HQlTnGWZc/E6csxKIIwY0EHA5krkIlC6WDQNRuU8kcVegcqGkBdFVQ6KPRfKBQPZK5KJhkUumaFYrpcoQOo/w2og0IHMh8nMxzZoNC1KhTh5QoNPhhAXRXU0+/5Yx7EDBUAAA==","output":[{"uuid":"0188430d-40cc-0002-a8da-245e5d68b7b7","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-40cc-0001-801c-85fd950326c8\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPpGa2QC/+2XQU8CQQyF30/ZcNJEhV1wEU9GJfGGB40HJAYRFUVBWFRi/Ovq11lBLhoTCCKZNLPbaTuv7U55Ce9vVb0oo0RDddVE21agWGs8M7pQHU/902pxXbU10JVauv+0ZtRDnjh7rqwa6uDp82xj2VGIP8XqgjPE2uF9MYFocY+s9hiv6RA7rNHZBL0O9qi+Km/Lc6ZnXeM7mzgTaAVbgnSJzSKWs8H5NvYOtSXYtxQprwLevuswoaOGq/+a/u51q3WVta895ETHqvDc0A3R29qkqyISo4egrI7rrJA/S94CsVZfj7qs2+E4S9NlqOO7Qp9PrTErh55Hs1prP9yIfdlT1q6zBTrQEXKINso9cLgBiIGLNLxXZHRTLd3RWRp9R44U16rYoociFUWcLbEruV16ctFm8ImInsP/fgIf3T0N5niPERg2c5v/ZuZKYPx25srgPrjvacum75Kolrsd21lnqd9qMm/6eze73c8Q3WbCZifnZPr5jBd0Pj1HLjtHhm6CS26WPUd6jpw1RxaofzqW/JrQ0LOkZ8k/ZsnIs6RnyZmzZETWWbFk5FnSs6RnSc+SS8eSofvPPBuWzHOypg9uMIiYmBQAAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752098975, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752098976, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752100960, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752100961, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752100962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752100962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752100962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752100963, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 3000}","now":"2023-05-22T10:42:02.048295+00:00","sent_at":"2023-05-22T10:41:41.977000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684752123024&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:03.027729+00:00","body":"H4sIAAAAAAAAA+2UXU/VMBjHv8rScCEJ4/RtXbcr4xFjgoFjAmIEctKzdqxQ2tF2HI+E725nVIgmEoiGC8/Vsuflv+e//voc3wB1rWwENdgIVvShcxFsgd67XvmoVQD1zV1mLkUUYySuegVqtgV+BHoznGmbVLxfqsWkcTY4o16iUUusjBNyrDLpWyYVKe+dT6noRZN0joGz88+dn3+LZy+6GPt6MjGuEaZzIdYcEzoJUUTdTJpusBf5zuvp9Ohw/2j7PNQFKktWI7KZFPf9pKfbznol5GrsUE0n7Jl6gihDsCZsE5zes3AMTsArIbO3BwezbOwdQp3B7ASA09vb5EdfqhS97EGNGKdlgRGGEJOU2ggqBJ186iQDEOeUwBIxCRnOIUNIMo4KSiVscoqwxA2ROWJ0AXl+V4wQkSy53FhqK93yVy2MCwZzSKBoBeStJKSkf1Ajqb4YT8FdqPHo+q6Zq1JTE6Z7+n27tyvemcGcX60+nM1mcMZ8+eWQf+ynb4bpJ7M7pE6pQ9S2iT8HIZQuFhXhjLQ5REXLlCAKL0TD+d0guISqhOMg38urRkGQ/pFr26ASiriqytutZyFzKby9D+a1MMOT4MG8RsU/ILKivxO5Y68GNSiZtUKb9PAqvYeYtekyeRX9KtM2YxDCBzgt1pw+llP2TJz+TxsUVbxak/koMlGysN6gf3mDVg+tUFRVaA3q40E9/QqvsD61hgoAAA==","output":[{"uuid":"0188430d-4494-0001-7d1f-eae0ac044b9f","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4494-0000-2fde-272e9466a2a0\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPtGa2QC/+2Uy04CUQyG/0eZsNJE5X5dGZXEHS40LtCQERDQ4SIMIDG8uvr1DBA2GhMJwUianun08redds7He1VviinUTAM1kUrylNMRZ0wN+Vj8hdb8Bgo0Vksd9RbamIbQlNgHxVVXH8uIM0BzqiT2CGsAzgxtn2djDdH8JnCwwms6xD68jA2RfbCX9VV5Wp6aXtXGVluL8XSALoQG+MYhy1knPkDfp7YQfUEppZXBOnIdhnRUd/W36a+nZx2rrAudQ7e6UYXzRE94l5SlqzyUQ06Ccriqs0L+OHkz+Fp9Q+qybmerLE2XwcfWQt5OrTk4gZxGslrvv5mIfdk7+MzpPF3qGrpCWuYeO1wPRM95Gt4cWk6qoy6dRd5dckS4VkWBHvJUlOItRXzC9RVF7toOTvEYOvyvN3Di5jTe4hxTYNjOZf/MzhXB+OnOlcF9cd/T2LbvEa+Om469WWeR3Woya/S/m97mM0O2nbDdSTj6/X5md3Q/93fkf7gjk/w/BXh/R+7vyE3fkcWN3JK2oUXOOfGfu7RZCEwKAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752120023, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752120025, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752121989, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752121991, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2997}","now":"2023-05-22T10:42:03.027729+00:00","sent_at":"2023-05-22T10:42:03.024000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684751932581&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.993934+00:00","body":"H4sIAAAAAAAAA+2UX0/VMBjGv8rScCEJ47Rr13W7Mh4xJhg4JiBGICc9a8cKpR1tx/FI+O52RoVookIkhLCrZe+fZ++7/vocXgF5KU0AFVjzhne+tQFsgM7ZTrqgpAfV1U1mLnjgQySsOgkqugF+BDrdnygTVZxbysWktsZbLV+iQYuvtOViqNLxWzoWSeesi6ngeB11DoE188+tm3+LJy/aELpqMtG25rq1PlQsw2TiAw+qntRtb87SrdfT6cH+7sHmqa9yVBS0Qng9Ku66SUc2rXGSi9XQIeuWmxN5D1GKYIXpOji+tcIhOAKvuEje7u3NkqG391UCkyMAjq+v4z7qXMboeQcqRBkpclRmZc5QTK156b2Ke6ooAxBjBMMCUQFplkKKkKAM5YQIWKcEZSKrsUgRJQvI0ptihLCgccu1pTLCLn/VyrKcwhRiyBsOWSMwLsgf1HCsz4dTsGdyOLqureeyUET76Y563+xs83e616cXqw8nsxmcUVd82Wcfu+mbfvpJb/exUygflKnDz0EwIYtFiRnFTQpR3lDJscwWvGbsZpCsgLKAwyDfy8taQhD/kW0aLyOKWVmW1xuPQuaSO3MbzEuu+3vBk7EK5Q9AZEl+J3LLXPSylyJpuNLx4WR89yFp4mVyMrhVokxSUgjhX0DNRlDvCip7JFCfmYWOZD4ZMkcLHUF9EqA+MwvFI5l3JbMYLfQ/W+g/OOjI6T04Pf4KKs0GxMkPAAA=","output":[{"uuid":"0188430d-4c42-0001-5c53-b092a3eae230","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c42-0000-208e-16cab7bc0a1c\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPxGa2QC/+2US08CUQyFz0+ZsNJERV7DY2VUEne40LhAQhAQUF7CABLjX1e/3gHCRmIiIRgnzZ0pbe9pOy3n86OsN8UUaK6hmmgFefJ1xDOmhmp4agurxQ3V1UQtddRfWGMaITPuPiiuugZ4xjy7WM6UwB9iDcGZYx3wbqwhWtyU013hNR3igLO8G6DXwF7WV+Zteap6VRtfde2OpwNsATIkNo5Yzjr3u9gH1BZgzymplNJ4x67DgI7qrv42/fX1rGMVdakL5E63KvE80RPRBWXoKov46AlQDld1lsgfJ2+aWKtvRF3W7XyVpeky1PC10HdTq885RU+hWa2VDROxL3vPOXc2T1e6Qa7RlrknDtcD0XORhveOLCfVUY/OwugeOUJcqyJHD1lXUZ6O8mg59PDmvu3gjIiRw/9+A6duTpMdzjEJhu1c5s/sXB6Mn+5cEdwX9z3t2PY9EtVx07Ff1lnot5rMG/7fzW7zmaPbTnhk9clu8vsNTe7phkYs+X9YMhmxZMSSEUtGLBmx5IYdTEUsGbHk1llyexxp+1nRF6KoW4VyDwAA\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684751929581, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751929582, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751929582, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751929582, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751929583, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751929583, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2999}","now":"2023-05-22T10:42:04.993934+00:00","sent_at":"2023-05-22T10:38:52.581000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684751935588&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.995438+00:00","body":"H4sIAAAAAAAAA+2WYU/VMBSG/8rS8EESxm3Xruv2yXjFmGDgmoAYgdz0rh0rlHa0Hdcr4b/bGRWiiQSCGnSflp2evjunffquh1dAXkoTQAXWvOGdb20AG6BztpMuKOlBdXUzMhc88CESVp0EFd0A3wKd7k+UiSrOLeViUlvjrZbP0aDFV9pyMWTp+C0dk6Rz1sWh4HgddQ6BNfOPrZt/iSfP2hC6ajLRtua6tT5ULMNk4gMPqp7UbW/O0q2X0+nB/u7B5qmvclQUtEJ4PSruuklHNq1xkovVMEPWLTcn8gGiFMEK03VwfKuFQ3AEXnCRvN7bmyXD3N5XCUyOADi+vo79qHMZo+cdqBBlpMhRibOc0Ti05qX3KvapogxAjBEMC0QFpFkKKUKCMpQTImCdEpSJrMYiRZQsIEtvkhHCgsYu15bKCLv8USvLcgpTiCFvOGSNwLggv1DDMT8fdsGeyWHruraey0IR7ac76m2zs83f6F6fXqzencxmcEZd8Wmfve+mr/rpB73dx5lC+aBMHb4XgglZLErMKG5SiPKGSo5ltuA1YzeFZAWUBRwK+Zpe1hKCuEa2abyMKGII4fXGXyFzyZ25DeYl1/2D4MlYhfLfQGRJfiZyy1z0spciabjS8eFkfPchaeJhcjK4VaJMUtK4qneAWoygPhVQ/zMLHcl8MmT+uxaKstFCHxvUrCzL0UL/hIWykcz7khkP82ihj2qhd19Cy/jfGjm9L6fHnwGF59KmyQ8AAA==","output":[{"uuid":"0188430d-4c44-0001-b56a-cc6e82a03b21","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c44-0000-1533-2d9329721e64\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPxGa2QC/+2WS08CUQyFz0+ZsNJERV7DY2VUEne40LhAQxAQUF7CABLDX1e/3gHCRmMiIaiT5t4pbe/p6dxOw/tbWa+KKdBMAzXQCvLk64A9prqqeKoLq8UN1NFYTbXVW1hjGiJTzt4rrpr6eEbsHSwnSuAPsQbgzLD2edbXEC1uwuqs8BoOsc9ang3Qq2Av+ZV5Wp6KXtTCV1k742kPW4AMiI0jlrPG+Q72PtwC7DkllVIa78hVGFBRzfFvUV9PTzpUUec6Q250rRL7kR6JLihDVVnER0+Asr/iWSJ/nLxpYo3fEF5W7WyVpeEyVPE10bfD1Wcdo6fQjOvdFzdib/aWdepsni50hVyiLXOPHa4HouciDW+OLG+qrS6VhdFdcoS4xiJHDVnHKA+XJFoOa3hy13pwSsTQ4X/egRN3T+Mt3mMSDOu5zK/puTwY3+25IrjP7n3asu57IKrtbsd+WWWh3ziZN/zezW73M0O3nvDI6pPd5Ocdmt3RDo2m5P+ZktloSkZTcuNTMgH7aEpGU/KvTMlcNCWjKbnxKbmpf5J5MOac/wAjkc3Fcg8AAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684751932586, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751932587, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751932587, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751932587, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684751932588, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684751932590, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 3000}","now":"2023-05-22T10:42:04.995438+00:00","sent_at":"2023-05-22T10:38:55.588000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684752077923&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.997482+00:00","body":"H4sIAAAAAAAAA+2XW0vcQBiG/0oYvKhg3Dllcrgq3VoKFt2C1lKVZTYzMdFxJs5M3G7F/95JabvSQmVlW6nmKuQ7vPm+zJMXcnwD5LXUHhRgw2neutp4sAVaa1ppfSMdKG6WmangnvcRv2glKNgW+BFoVXfW6KBi7VzORqXRzij5EvVafKEMF32VCs9SoUhaa2xIecvLoHMMjJ5+ru30Wzx6UXvfFqORMiVXtXG+yDChI+e5b8pRWXf6It55PR4fHe4fbZ+7IkFpygpENoPivh21dNtoK7lY9B2yrLk+kw8QZQgWhG2C0zsrHIMT8IqL6O3BwSTqeztXRDA6AeD09jbs01zKEL1sQYFYRtMEw5TmOAupDSeda8KeTZABKMsogSliAjIcQ4aQYBlKKBWwjCnCApdExIjRGcziZTFCRLCw5ca80cLMf9XCOGEwhgTyisOsEoSk9A9qJNQn/SmYC9kfXVuXU5k2VLnxXvO+2tvl71Snzq8WH84mEzhhNv1ymH1sx2+68Se124VO0Tjf6NL/HIRQOpvlJGOkiiFKKiY5kXjGyyxbDoJTKFPYD/K9PC8lBOEdmapyMqCI8zy53XoUMufc6rtgXnPVPQgenBUo+QtE5vR3Inf0VSc7KaKKNypcrAz3zkdV+Jis9HYRNTpiEMKB03VzSh+J0+fkoCxHZCBzJTIRRHBw0DU7aH6fhQZQgx8MoK4EKswHC/0XFjqQ+d+Q+XQtFNPBQp8QqM/MQsNP6UDmamSmg4Wu2UIRvt9C2QDq6qCefgX0iVnZDBUAAA==","output":[{"uuid":"0188430d-4c46-0001-9d63-c33d1ad5a8bc","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c46-0000-7b0c-6e3bf34bf14a\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAPxGa2QC/+2USU8CQRCF30+ZcNJEZRGG5WRcEm940HhAYhBRUDZhECfGv65+1QPEi8bDxLVT6Z6iqvpVVXfxXp4belRGkWKN1UGrKVCoDfaMLtXC01pYLW6svma6Vk/DhTWjCTLn7IWyamuEZ8rex7KjPP4EawxOjHXE9/INosXds/orvI5DHLGWZyP0FtjL+hp8Lc+5HtTFd/7mTKA1bBEyJjaLWM425/vYR9QWYa+ooG0V8U5dhxEdtV39Xfob6labOtC+9pBTnajOvqUbomsq0VUZCdHzoKyv6qyTP0veIrFW34S6rNt4laXjMrTwXaN/Ta0hK4e+jWa1Nj94EbvZM9auswU61DFyhLbMPXO4AYiBizS8J2T5Uj0N6CyJHpAjwbUqKvRQpqICZ8voVbTK4uRPm8E5EROH//4E3rt3mn3hO9p92cyVfs3MVcH47MwdgHvn7tOWTd8VUT33OvbLOkv8VpN5k/+72e19YnSbCZudnJO/O5+eI/8DR4bMoN2e50jPkWlzZDUVlkwmtOhZ0rPkN7Nk0bOkZ8nUWbJAVs+SniX/CkuWPEt6lkydJfNuxtJhyZCTTb0CdWfxy5gUAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752074928, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752074928, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752076913, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752076914, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752076914, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752076914, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752076915, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752076916, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2995}","now":"2023-05-22T10:42:04.997482+00:00","sent_at":"2023-05-22T10:41:17.923000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684752104985&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:42:04.999014+00:00","body":"H4sIAAAAAAAAA+2ZbUvcMBzAv0oJvphgvTw1Tftq7OYYOPQGOsdUjlyT2mpta5J6u4nffenYdrKBxx0nclxelSb//JuHX3+E5PwBqHtVW5CCHVOL1hSNBXug1U2rtC2VAenDvGYshRV9iZ21CqRsD/wpaKvuqqxdFq2najLImto0lXqL+lxiVjVC9lGV+1blgpTWjXZVVovM5TkHTT3+Xujxr/LgTWFtmw4GVZOJqmiMTTkmdGCssGU2yIquvgkP3g+HZ6fHZ/vXJo1QHLMUkV2X8VgPWrrf1FoJOetbqKwQ9ZVaISlDMCVsF1w+GcI5uADvhAw+npyMgr5tZ9IABhcAXD4+uvGUt8qV3rYgRYzTOMIIooTHrmrHKGNKN87SpQGIc0pgjJiEDIeQISQZRxGlEmYhRVjijMgQMTqBPJwHI0Qkc6PcmZa1bKb/5sI4YjCEBIpcQJ5LQmL6TDbi4qN+FZob1S9dW2RjFZe0MsOj8nN+dCg+VV11fTf7cjUawRHT8Y9T/rUdfuiG36rDzrWUpbFlndm/HSGUTiYJ4YzkIURRzpQgCk9Exvm8IziGKoZ9R36HJ5mCwM1Rk+dGORRxktDHvVchcyp0/RTMe1F1K8GDeYqiFyAyof8TeVDfdapTMshFWbmHVu7d2CB3P5NWVs+Csg4YhHABp4nndFM43SaDkoQhT+ZSZCKI3ZR5g67VoJQvUKgDFXtQNwXULVMo8WQuSSZy+yGv0LUqNFm0C3Wgui2VB3UzQN0yhUaezGXJdEcfXqFrVSjCixXKPKibAuqWKdSTuTSZbsq8QteqUEwXK9Sf2S8NqtseeYW+vEK5J3NZMv1t0mso1F8nrQDq5U9Nk6Owkh8AAA==","output":[{"uuid":"0188430d-4c49-0001-a3b5-5cbaee834ee0","distinct_id":"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"0188430d-4c49-0000-e27d-3ad69e097d43\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAP1Ga2QC/+2ZTU8iQRCG358y4bQmrsgAA3gyfiTe9LBmD0oIIgsoCuKgks3+9dWneoB4WWPiRDBb6fRQVFXXB108IeH575l+q6BUM43VRdpRpESbPAu6VBtLe641v7GGmqqngW7n2oImrEfOXqiojkZY7nkO0eyqhD2LNSbODO2I18tXEc3vgT1cxuuGiCP24myK3Cb2or4zXi1PS0/qY2u9OhPpG7qUNca3yLKcHc4P0Y+oLUVfV6yyKljvQ4cpHXVC/X36u9W1vutQB9pn/dSpjnlu6QrvHVXpqsZKkEtE2VjWeUz+Inkr+Fp9E+qybmfLLN2QoY2th/w5tSbsbeQyktXafONG7JM9Z+8FXaQj/WCdIC1yT0PciIhR8LR4f1iLmxrohs4y7xtyZHGtijo91KgoDvWU1EBTm59ctxl8xGMS4v97Ah/CPU0/8R5jYtjMVb/MzDWI8d6ZOyTuXfg8bdv0/cJrEG7H3llnmd1qMmv2fTe93c8M2WbCZmc7rI/PZ2NN59MZ+T8wssz8WWXOSGdk3oysUP/HKZlNaOyUdEqumJJlp6RTMndKNnL5LZlNaMUp6ZRcMSWrTkmnZO6ULFF9XpRMnJJOyRVTMnFKOiVzp2RM1rwoWXNKOiVXTMm6U9IpudaUtH9vmnoBGOaMiuQeAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684752101987, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752101989, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103961, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103962, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103963, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103964, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103965, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103966, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103966, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103967, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}, {\"timestamp\": 1684752103968, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684752103969, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"warn\"}}}]}, \"$session_id\": \"188430716d062-0611d681544d0c-412d2c3d-164b08-188430716d113d6\", \"$window_id\": \"188430716d22560-030afa08fd3374-412d2c3d-164b08-188430716d32255\", \"token\": \"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu\", \"distinct_id\": \"188344bb93863f-015f6ea3e2bac88-412d2c3d-270e70-188344bb939ce0\"}, \"offset\": 2994}","now":"2023-05-22T10:42:04.999014+00:00","sent_at":"2023-05-22T10:41:44.985000+00:00","token":"phc_e7i4lsCNiQfNKaLluljqyVgPP0P6r7zU8XpCFuCZlKu"}]} -{"path":"/e/?ip=1&_=1684752184509&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.525363+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ3WVdkbGRtbGxkeUlzSW5CeWIzQmxjblJwWlhNaU9uc2lKRzl6SWpvaVRXRmpJRTlUSUZnaUxDSWtiM05mZG1WeWMybHZiaUk2SWpFd0xqRTFMakFpTENJa1luSnZkM05sY2lJNklrWnBjbVZtYjNnaUxDSWtaR1YyYVdObFgzUjVjR1VpT2lKRVpYTnJkRzl3SWl3aUpHTjFjbkpsYm5SZmRYSnNJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0x5SXNJaVJvYjNOMElqb2liRzlqWVd4b2IzTjBPamd3TURBaUxDSWtjR0YwYUc1aGJXVWlPaUl2SWl3aUpHSnliM2R6WlhKZmRtVnljMmx2YmlJNk1URXpMQ0lrWW5KdmQzTmxjbDlzWVc1bmRXRm5aU0k2SW1WdUxWVlRJaXdpSkhOamNtVmxibDlvWldsbmFIUWlPamszTkN3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRVd01Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPRFF6TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFME16TXNJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpVM0xqSWlMQ0lrYVc1elpYSjBYMmxrSWpvaWMzRXhhWEppT1dRME0ySmhZbXBxTWlJc0lpUjBhVzFsSWpveE5qZzBOelV5TVRnMExqVXdPU3dpWkdsemRHbHVZM1JmYVdRaU9pSXhPRGcwTXpCbE16UmlOVE5qWXkwd1pETmpObU0zTURrME5ERTFaRGd0TkRFeVpESmpNMlF0TVRZMFlqQTRMVEU0T0RRek1HVXpOR0kyWkRNNElpd2lKR1JsZG1salpWOXBaQ0k2SWpFNE9EUXpNR1V6TkdJMU0yTmpMVEJrTTJNMll6Y3dPVFEwTVRWa09DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqWmtNemdpTENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW5SdmEyVnVJam9pY0doalgycElZMFIxTjIwelduWnVTVzV3YTJKNGJVcHlTMFZpZVVwMWEzbEJXa042ZVV0bFREQnpWSGhDTTJzaUxDSWtjMlZ6YzJsdmJsOXBaQ0k2SWpFNE9EUXpNR1V6TkdJM04yWXdMVEF6TXpCa05XUXlPRE00WmpObE9DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqZ3lPRE16SWl3aUpIZHBibVJ2ZDE5cFpDSTZJakU0T0RRek1HVXpOR0k1TXpBMU5pMHdORGRoT1RCaE1HSXdNVE5pTkRndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdKaE1qVmxOaUlzSWlSd1lXZGxkbWxsZDE5cFpDSTZJakU0T0RRek1HVXpOR0ppTVRZM1lpMHdOekJrTVRrMVpqVTVNak0yTmpndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdKak1qazRNQ0o5TENKMGFXMWxjM1JoYlhBaU9pSXlNREl6TFRBMUxUSXlWREV3T2pRek9qQTBMalV3T1ZvaWZRJTNEJTNE","output":[{"uuid":"0188430e-34ce-0000-0e70-879b9262bb17","distinct_id":"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"sq1irb9d43babjj2\", \"$time\": 1684752184.509, \"distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"timestamp\": \"2023-05-22T10:43:04.509Z\"}","now":"2023-05-22T10:43:04.525363+00:00","sent_at":"2023-05-22T10:43:04.509000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?ip=1&_=1684752184509&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.515071+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ2Y0hSZmFXNGlMQ0p3Y205d1pYSjBhV1Z6SWpwN0lpUnZjeUk2SWsxaFl5QlBVeUJZSWl3aUpHOXpYM1psY25OcGIyNGlPaUl4TUM0eE5TNHdJaXdpSkdKeWIzZHpaWElpT2lKR2FYSmxabTk0SWl3aUpHUmxkbWxqWlY5MGVYQmxJam9pUkdWemEzUnZjQ0lzSWlSamRYSnlaVzUwWDNWeWJDSTZJbWgwZEhBNkx5OXNiMk5oYkdodmMzUTZPREF3TUM4aUxDSWthRzl6ZENJNklteHZZMkZzYUc5emREbzRNREF3SWl3aUpIQmhkR2h1WVcxbElqb2lMeUlzSWlSaWNtOTNjMlZ5WDNabGNuTnBiMjRpT2pFeE15d2lKR0p5YjNkelpYSmZiR0Z1WjNWaFoyVWlPaUpsYmkxVlV5SXNJaVJ6WTNKbFpXNWZhR1ZwWjJoMElqbzVOelFzSWlSelkzSmxaVzVmZDJsa2RHZ2lPakUxTURBc0lpUjJhV1YzY0c5eWRGOW9aV2xuYUhRaU9qZzBNeXdpSkhacFpYZHdiM0owWDNkcFpIUm9Jam94TkRNekxDSWtiR2xpSWpvaWQyVmlJaXdpSkd4cFlsOTJaWEp6YVc5dUlqb2lNUzQxTnk0eUlpd2lKR2x1YzJWeWRGOXBaQ0k2SWpsbmVYTnpkM1JtZEdFNE1IVjNkbmdpTENJa2RHbHRaU0k2TVRZNE5EYzFNakU0TkM0MU1Ea3NJbVJwYzNScGJtTjBYMmxrSWpvaU1UZzRORE13WlRNMFlqVXpZMk10TUdRell6WmpOekE1TkRReE5XUTRMVFF4TW1ReVl6TmtMVEUyTkdJd09DMHhPRGcwTXpCbE16UmlObVF6T0NJc0lpUmtaWFpwWTJWZmFXUWlPaUl4T0RnME16QmxNelJpTlROall5MHdaRE5qTm1NM01EazBOREUxWkRndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdJMlpETTRJaXdpSkhKbFptVnljbVZ5SWpvaWFIUjBjRG92TDJ4dlkyRnNhRzl6ZERvNE1EQXdMM05wWjI1MWNDSXNJaVJ5WldabGNuSnBibWRmWkc5dFlXbHVJam9pYkc5allXeG9iM04wT2pnd01EQWlMQ0owYjJ0bGJpSTZJbkJvWTE5cVNHTkVkVGR0TTFwMmJrbHVjR3RpZUcxS2NrdEZZbmxLZFd0NVFWcERlbmxMWlV3d2MxUjRRak5ySWl3aUpITmxjM05wYjI1ZmFXUWlPaUl4T0RnME16QmxNelJpTnpkbU1DMHdNek13WkRWa01qZ3pPR1l6WlRndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUXpNR1V6TkdJNE1qZ3pNeUlzSWlSM2FXNWtiM2RmYVdRaU9pSXhPRGcwTXpCbE16UmlPVE13TlRZdE1EUTNZVGt3WVRCaU1ERXpZalE0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME16QmxNelJpWVRJMVpUWWlMQ0lrY0dGblpYWnBaWGRmYVdRaU9pSXhPRGcwTXpCbE16UmlZakUyTjJJdE1EY3daREU1TldZMU9USXpOalk0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME16QmxNelJpWXpJNU9EQWlmU3dpZEdsdFpYTjBZVzF3SWpvaU1qQXlNeTB3TlMweU1sUXhNRG8wTXpvd05DNDFNRGxhSW4wJTNE","output":[{"uuid":"0188430e-34c8-0000-5a19-0714f9887930","distinct_id":"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"9gysswtfta80uwvx\", \"$time\": 1684752184.509, \"distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"timestamp\": \"2023-05-22T10:43:04.509Z\"}","now":"2023-05-22T10:43:04.515071+00:00","sent_at":"2023-05-22T10:43:04.509000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?ip=1&_=1684752184514&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.529988+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJwWkdWdWRHbG1lU0lzSW5CeWIzQmxjblJwWlhNaU9uc2lKRzl6SWpvaVRXRmpJRTlUSUZnaUxDSWtiM05mZG1WeWMybHZiaUk2SWpFd0xqRTFMakFpTENJa1luSnZkM05sY2lJNklrWnBjbVZtYjNnaUxDSWtaR1YyYVdObFgzUjVjR1VpT2lKRVpYTnJkRzl3SWl3aUpHTjFjbkpsYm5SZmRYSnNJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0x5SXNJaVJvYjNOMElqb2liRzlqWVd4b2IzTjBPamd3TURBaUxDSWtjR0YwYUc1aGJXVWlPaUl2SWl3aUpHSnliM2R6WlhKZmRtVnljMmx2YmlJNk1URXpMQ0lrWW5KdmQzTmxjbDlzWVc1bmRXRm5aU0k2SW1WdUxWVlRJaXdpSkhOamNtVmxibDlvWldsbmFIUWlPamszTkN3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRVd01Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPRFF6TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFME16TXNJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpVM0xqSWlMQ0lrYVc1elpYSjBYMmxrSWpvaWVXVnViamx0YVhRNE4yNXNaR1J4YUNJc0lpUjBhVzFsSWpveE5qZzBOelV5TVRnMExqVXhOQ3dpWkdsemRHbHVZM1JmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadElpd2lKR1JsZG1salpWOXBaQ0k2SWpFNE9EUXpNR1V6TkdJMU0yTmpMVEJrTTJNMll6Y3dPVFEwTVRWa09DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqWmtNemdpTENJa2RYTmxjbDlwWkNJNkltZFJSRWhzY0hoeFEwaG5WV3RvTlhNelRFOVdWVkUxUVdOT2RERnJiVlpzTUhoMlQyRnFNbk5tTm0waUxDSWtjbVZtWlhKeVpYSWlPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPamd3TURBdmMybG5iblZ3SWl3aUpISmxabVZ5Y21sdVoxOWtiMjFoYVc0aU9pSnNiMk5oYkdodmMzUTZPREF3TUNJc0lpUmhibTl1WDJScGMzUnBibU4wWDJsa0lqb2lNVGc0TkRNd1pUTTBZalV6WTJNdE1HUXpZelpqTnpBNU5EUXhOV1E0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME16QmxNelJpTm1Rek9DSXNJblJ2YTJWdUlqb2ljR2hqWDJwSVkwUjFOMjB6V25adVNXNXdhMko0YlVweVMwVmllVXAxYTNsQldrTjZlVXRsVERCelZIaENNMnNpTENJa2MyVnpjMmx2Ymw5cFpDSTZJakU0T0RRek1HVXpOR0kzTjJZd0xUQXpNekJrTldReU9ETTRaak5sT0MwME1USmtNbU16WkMweE5qUmlNRGd0TVRnNE5ETXdaVE0wWWpneU9ETXpJaXdpSkhkcGJtUnZkMTlwWkNJNklqRTRPRFF6TUdVek5HSTVNekExTmkwd05EZGhPVEJoTUdJd01UTmlORGd0TkRFeVpESmpNMlF0TVRZMFlqQTRMVEU0T0RRek1HVXpOR0poTWpWbE5pSXNJaVJ3WVdkbGRtbGxkMTlwWkNJNklqRTRPRFF6TUdVek5HSmlNVFkzWWkwd056QmtNVGsxWmpVNU1qTTJOamd0TkRFeVpESmpNMlF0TVRZMFlqQTRMVEU0T0RRek1HVXpOR0pqTWprNE1DSjlMQ0lrYzJWMElqcDdmU3dpSkhObGRGOXZibU5sSWpwN2ZTd2lkR2x0WlhOMFlXMXdJam9pTWpBeU15MHdOUzB5TWxReE1EbzBNem93TkM0MU1UUmFJbjAlM0Q=","output":[{"uuid":"0188430e-34d2-0000-99e4-4cb75151aa19","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$identify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yenn9mit87nlddqh\", \"$time\": 1684752184.514, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$anon_distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"$set\": {}, \"$set_once\": {}, \"timestamp\": \"2023-05-22T10:43:04.514Z\"}","now":"2023-05-22T10:43:04.529988+00:00","sent_at":"2023-05-22T10:43:04.514000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/engage/?ip=1&_=1684752184515&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:04.531659+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW1WdFlXbHNJam9pZUdGMmFXVnlRSEJ2YzNSb2IyY3VZMjl0SW4wc0lpUjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkdScGMzUnBibU4wWDJsa0lqb2laMUZFU0d4d2VIRkRTR2RWYTJnMWN6Tk1UMVpWVVRWQlkwNTBNV3R0Vm13d2VIWlBZV295YzJZMmJTSXNJaVJrWlhacFkyVmZhV1FpT2lJeE9EZzBNekJsTXpSaU5UTmpZeTB3WkROak5tTTNNRGswTkRFMVpEZ3ROREV5WkRKak0yUXRNVFkwWWpBNExURTRPRFF6TUdVek5HSTJaRE00SWl3aUpIVnpaWEpmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadEluMCUzRA==","output":[{"uuid":"0188430e-34d4-0000-387d-e634d8ec56a6","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T10:43:04.531659+00:00","sent_at":"2023-05-22T10:43:04.515000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684752187560&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:07.564872+00:00","body":"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","output":[{"uuid":"0188430e-40ad-0000-e548-fc5de1421f3b","distinct_id":"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"7d1mrlsrey6apuzb\", \"$time\": 1684752184.51, \"distinct_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"session-reset-on-load\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"offset\": 3044}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0001-b527-acee69cb829b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"56mza3obi7hco2vh\", \"$time\": 1684752184.515, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\"}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": false, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"offset\": 3039}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0002-e60d-ac4f19b25506","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"nqru46xqy512vurl\", \"$time\": 1684752184.515, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34bb167b-070d195f5923668-412d2c3d-164b08-188430e34bc2980\"}, \"offset\": 3039}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0003-e8e3-5965b1f00c95","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"621xmy2vdcpezwlh\", \"$time\": 1684752184.55, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 3004}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0004-b960-e71c6531c083","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"lihlwwsc66al5e1i\", \"$time\": 1684752184.555, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"posthog-3000\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2999}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0005-4907-567be483079e","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"nt2osnfl5abzmq8y\", \"$time\": 1684752184.555, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"enable-prompts\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2999}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0006-2957-27a39ce38568","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"cvfdres92ldvucw0\", \"$time\": 1684752184.559, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"hackathon-apm\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2995}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0007-9951-26b9cabcd2ab","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yqzwbgtdjdnmdwbf\", \"$time\": 1684752184.56, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"early-access-feature\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2994}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0008-80c4-46772bf66fc5","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"sv6bufbuqcbvtvdu\", \"$time\": 1684752184.56, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"hogql\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2994}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-0009-5175-65c70ceabd9d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"eyoa5pfu7ddy9m5m\", \"$time\": 1684752184.56, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"feedback-scene\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2994}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000a-5dc4-9d624d8dc3fa","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"no1by5vd7x64u3sx\", \"$time\": 1684752184.586, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"onboarding-v2-demo\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2968}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000b-0f86-e81af913f30b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"jvedtjd1wp8xwwkw\", \"$time\": 1684752184.599, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"notebooks\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2955}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000c-fb88-5d95e7940f12","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"ingestion landing seen\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ib9n3revlo62eca3\", \"$time\": 1684752184.603, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2951}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-40ad-000d-fec3-693237c3318e","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"9vunv0ozv84m22me\", \"$time\": 1684752184.621, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"instance\", \"$group_key\": \"http://localhost:8000\", \"$group_set\": {\"site_url\": \"http://localhost:8000\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2933}","now":"2023-05-22T10:43:07.564872+00:00","sent_at":"2023-05-22T10:43:07.560000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684752190565&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:10.567715+00:00","body":"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","output":[{"uuid":"0188430e-4c68-0000-2df4-40793d05388a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"vegy51ygdnvw1ny0\", \"$time\": 1684752188.139, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"p\", \"classes\": [\"mb-2\"], \"attr__class\": \"mb-2\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"I can add a code snippet to my product.\"}, {\"tag_name\": \"div\", \"classes\": [\"mt-4\", \"mb-0\"], \"attr__class\": \"mt-4 mb-0\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--has-side-icon\", \"mb-4\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--has-side-icon mb-4\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"mb-6\"], \"attr__class\": \"flex flex-col mb-6\", \"nth_child\": 4, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e34e4eef-039e8e6dd4d53c-412d2c3d-164b08-188430e34e520f8\"}, \"offset\": 2421}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-4c68-0001-eb2a-54453fb06eec","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/platform\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/platform\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"3hnz1hglhce8vl5k\", \"$time\": 1684752188.145, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e42ef110e-042aef35510b338-412d2c3d-164b08-188430e42f01bb3\"}, \"offset\": 2416}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-4c68-0002-6ea4-3b0d05101aee","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/platform\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/platform\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"4r1etipqepb7m8xn\", \"$time\": 1684752188.809, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"backend\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"mb-6\"], \"attr__class\": \"flex flex-col mb-6\", \"nth_child\": 4, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e42ef110e-042aef35510b338-412d2c3d-164b08-188430e42f01bb3\"}, \"offset\": 1752}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-4c68-0003-3fe5-c2dfb803996d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"txh2rjlwpqzfn25q\", \"$time\": 1684752188.815, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e458d299-0a26be2f319ee58-412d2c3d-164b08-188430e458e1e11\"}, \"offset\": 1746}","now":"2023-05-22T10:43:10.567715+00:00","sent_at":"2023-05-22T10:43:10.565000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684752193570&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:13.576236+00:00","body":"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","output":[{"uuid":"0188430e-582a-0000-2b14-ad7e278afb7b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"2mvrdgcqmvepz5u8\", \"$time\": 1684752191.57, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"PHP\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"attr__data-attr\": \"select-framework-PHP\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"div\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"FrameworkPanel\"], \"attr__class\": \"FrameworkPanel\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e458d299-0a26be2f319ee58-412d2c3d-164b08-188430e458e1e11\"}, \"offset\": 1997}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-582a-0001-7181-20c3ce2580fe","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend/php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend/php\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pin8vjsmr8mepxwj\", \"$time\": 1684752191.58, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e505ac40-0ebb044514cabf8-412d2c3d-164b08-188430e505b26e1\"}, \"offset\": 1987}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-582a-0002-bea7-d4b125bb0a9a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/backend/php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/backend/php\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"mtqwb993ug1ltj2o\", \"$time\": 1684752192.856, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"Continue\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"panel-footer\"], \"attr__class\": \"panel-footer\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 11, \"nth_of_type\": 5}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"InstructionsPanel\", \"mb-8\"], \"attr__class\": \"InstructionsPanel mb-8\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e505ac40-0ebb044514cabf8-412d2c3d-164b08-188430e505b26e1\"}, \"offset\": 710}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-582a-0003-cd42-5039c46a0c3b","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/verify?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/verify\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"i2zgz88lw17xt1x0\", \"$time\": 1684752192.862, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e555c203-01d0ef0ffe354a-412d2c3d-164b08-188430e555d12ed\"}, \"offset\": 705}","now":"2023-05-22T10:43:13.576236+00:00","sent_at":"2023-05-22T10:43:13.570000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/engage/?ip=1&_=1684752196089&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:16.094175+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW1WdFlXbHNJam9pZUdGMmFXVnlRSEJ2YzNSb2IyY3VZMjl0SW4wc0lpUjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkdScGMzUnBibU4wWDJsa0lqb2laMUZFU0d4d2VIRkRTR2RWYTJnMWN6Tk1UMVpWVVRWQlkwNTBNV3R0Vm13d2VIWlBZV295YzJZMmJTSXNJaVJrWlhacFkyVmZhV1FpT2lJeE9EZzBNekJsTXpSaU5UTmpZeTB3WkROak5tTTNNRGswTkRFMVpEZ3ROREV5WkRKak0yUXRNVFkwWWpBNExURTRPRFF6TUdVek5HSTJaRE00SWl3aUpIVnpaWEpmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadEluMCUzRA==","output":[{"uuid":"0188430e-61fe-0000-6f49-83e60118a598","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T10:43:16.094175+00:00","sent_at":"2023-05-22T10:43:16.089000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/engage/?ip=1&_=1684752196112&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:16.114065+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qZ3dNREF2YzJsbmJuVndJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lKc2IyTmhiR2h2YzNRNk9EQXdNQ0lzSW1WdFlXbHNJam9pZUdGMmFXVnlRSEJ2YzNSb2IyY3VZMjl0SW4wc0lpUjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkdScGMzUnBibU4wWDJsa0lqb2laMUZFU0d4d2VIRkRTR2RWYTJnMWN6Tk1UMVpWVVRWQlkwNTBNV3R0Vm13d2VIWlBZV295YzJZMmJTSXNJaVJrWlhacFkyVmZhV1FpT2lJeE9EZzBNekJsTXpSaU5UTmpZeTB3WkROak5tTTNNRGswTkRFMVpEZ3ROREV5WkRKak0yUXRNVFkwWWpBNExURTRPRFF6TUdVek5HSTJaRE00SWl3aUpIVnpaWEpmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadEluMCUzRA==","output":[{"uuid":"0188430e-6212-0000-ba3f-5cbd5ec58bd2","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T10:43:16.114065+00:00","sent_at":"2023-05-22T10:43:16.112000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684752196576&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:16.582542+00:00","body":"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","output":[{"uuid":"0188430e-63e7-0000-35f7-77eaf3880b39","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/verify?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/verify\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pqx96n7zrbq30v6t\", \"$time\": 1684752194.578, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"or continue without verifying\"}, {\"tag_name\": \"button\", \"$el_text\": \"or continue without verifying\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-primary\", \"LemonButton--full-width\", \"LemonButton--centered\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-primary LemonButton--full-width LemonButton--centered\", \"nth_child\": 5, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"ingestion-listening-for-events\"], \"attr__class\": \"ingestion-listening-for-events\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"text-center\"], \"attr__class\": \"text-center\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e555c203-01d0ef0ffe354a-412d2c3d-164b08-188430e555d12ed\"}, \"offset\": 1993}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0001-3713-fae840c9d31f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ca1xryhb43tz0aqr\", \"$time\": 1684752194.59, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 1982}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0002-9a6f-fdc96fab0d9d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"ingestion continue without verifying\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"02dxxmdqmq1icsqc\", \"$time\": 1684752194.591, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 1980}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0003-c4bd-0cee63372ea6","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"o1vi2lr0d7efkvrd\", \"$time\": 1684752195.807, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"Complete\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"panel-footer\"], \"attr__class\": \"panel-footer\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-screen\", \"flex-col\"], \"attr__class\": \"flex h-screen flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar__layout SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 765}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0004-272b-0bdf0f14875d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"ingestion recordings turned off\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"wxw6rpbifx56jkkv\", \"$time\": 1684752195.811, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"session_recording_opt_in\": false, \"capture_console_log_opt_in\": false, \"capture_performance_opt_in\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 760}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0005-82ae-014d11c490fb","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"completed_snippet_onboarding team setting updated\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=backend&framework=php\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ghxomrt7jyayufsg\", \"$time\": 1684752195.936, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"setting\": \"completed_snippet_onboarding\", \"value\": true, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 636}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0006-a1b6-c15b0e084f14","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"activation sidebar shown\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"9ciqdwvb0gm7stqn\", \"$time\": 1684752195.955, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"active_tasks_count\": 5, \"completed_tasks_count\": 2, \"completion_percent_count\": 29, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 617}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e7-0007-2bd1-96f9150acdd3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"89hf5t22vlawns70\", \"$time\": 1684752195.989, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"cloud-announcement\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 582}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0000-38d3-622f0b7db0b3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"g5s1ttbkbuqqjp1f\", \"$time\": 1684752196.006, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$feature_flag\": \"require-email-verification\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e5c1b2884-08baad4ddb54278-412d2c3d-164b08-188430e5c1d1dde\"}, \"offset\": 566}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0001-26c9-ed81e4d2f77a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"314lgzt3xfrozlwu\", \"$time\": 1684752196.054, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 518}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0002-e82f-aba2ca288e21","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"client_request_failure\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ps0zh0lhjw7ntghy\", \"$time\": 1684752196.07, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"pathname\": \"/api/billing-v2/\", \"method\": \"GET\", \"duration\": 65, \"status\": 404, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 502}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0003-e240-08053caca9aa","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"7muhndo38d1ixe0p\", \"$time\": 1684752196.089, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 483}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0004-ced5-5ad043dcd37a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"qiacra8lvetpncbm\", \"$time\": 1684752196.09, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 482}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0005-b2eb-104477bc52f8","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"xnwub6hl3xv00xkq\", \"$time\": 1684752196.112, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 460}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"0188430e-63e8-0006-74b6-493aaf93af42","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yxtxo6923bqvqrk0\", \"$time\": 1684752196.113, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 459}","now":"2023-05-22T10:43:16.582542+00:00","sent_at":"2023-05-22T10:43:16.576000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684752239786&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:43:59.790498+00:00","body":"H4sIAAAAAAAAA61We2/bNhD/KoaRPyeHessBhqFpNqRttqDIUgwNCoEiKYkxJWoS5UeLfvfd0YptOY+2QwEDhu75ux/vjrz7MhVLUZvp2fSE9kYz2pi+FdNfpk2rG9EaKbrp2ZfpiYa/6Z+UTa5vJv+AGgTpUrSd1DUoXDJzwxlBedbqVSdaEP4hW5HrNQq5WEomUrNpBCguRLcwukEF69sW0qd9q0BRGtOcnZ4qgKFK3ZmzhBByagF2v+k607Tlsi5SpqtGCSP4r6btEewJWkOAsScqGmrKmlaYdgh0AHJfgev6B2JF66KnBTqJ2rm9QZeOtULUaSlkUUKqeRzshSvJTQlBQkJAuJRi1ejW7GyTAIPvxA/WgY9iJTNIsxIZJoGPQ1ZnYTzzUC5rgGVSyUEaqzLM1T13g1AnUq1QbyRW6EZJEIee50ezOIHYXHZG1mzwK95fXKpm/e/ry+J2UYadf3X94fZ9+Ir9ZdxF9UGR9fKa3ntdHlUHR2Zd3QRKIMIPstBnzCHcZxGLyTwI3JAnTuB63GM+d9woyEji7M0j7icYrEdW/wcK2aVcVDqFZrwXDKjMqeoEBCxa3Te2M3eqKRnyOr4bCQfOnzihSyMn51EQhUESzQn2im4LWsvP1GxJ3nl5czLfegVcuM58HsaxR10ShAEiqTtDa4Yt8WSXTr8CqoMJSoF8minBUygdjjTtJAfnB/yUGbkUaS6oNc4VLaCau0+gOpSlDd0oTTlWCgkayFbqYtQigW/nrhVUVQgOTAR3mJJsUWrgHXSiolJZHHiidAlfCG2HplOULV7Qn8AcC5hTHOqnR7STRd3bgd6a4pByDWkR4+OhtIP4sA0sVLsLxME8opUSlR3YM9hSBsgY5rhbFmDOFO06XE530ytokfoNAz/gjxrTpqnVgu1e9aAZpm/qCmywrexhUEfCXCrcSbWukcOtDGf4HFYatM2ETLwAfjvdulI1phwYWq1Ws5U/g2479SxFFvUQWrPedgeYW453GtpK6pSSc4HMDdutNmXKSqlgfoAX/NL5wN6Wp9SINcKHJhwz1VAs/Jiq894YXaepfJ6ykcnLCI5yZtbTnvIe13MYQHHw5TgGbxzabo7lMHum75ymldUT2lo7ANPgNXakKWnnjKscmm6H8tnaJ08CG0vHsMa6PaixfAfph1jlcjkm8RUuELvDbmCxnFNbhO5EOlT26Fi/5fDz4eyOpDEO3mLN2sFpaTK4J74P38Af+k/Qe7L1/clQvwPLOCXc/KOUwbdS7qPsalN0o3vzOPVgMDk2fBGA/ySAHyZpi8Re063WRznhPfHy3GuOgzlMWQmrG1e/3aujON6jOMCB0Qu78JqSpfeX7KKPK//jsn5TN4tsXb1t3/2ebd72i82rj68/b96JK9L9vT737a0BFOOVcfxQieOcOMT3CQ+5l/hJ7ouXHioJ2PgYbiVrrlfH0eY+CSOHBDGdE0oy4vpZ8FI46oUC+xSenwW8o8Q4YOTywIs5xMuCuSB+zHiUPP+OAnN4vWQhPjF0nncCVqoPV8rXT/8B8mE6/L4LAAA=","output":[{"uuid":"0188430f-0caf-0000-dc00-030ac4424349","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"7lh5fljd145o8ilw\", \"$time\": 1684752236.783, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"svg\", \"classes\": [\"LemonIcon\"], \"attr__class\": \"LemonIcon\", \"attr__width\": \"1em\", \"attr__height\": \"1em\", \"attr__fill\": \"none\", \"attr__viewBox\": \"0 0 24 24\", \"attr__xmlns\": \"http://www.w3.org/2000/svg\", \"attr__focusable\": \"false\", \"attr__aria-hidden\": \"true\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__icon\"], \"attr__class\": \"LemonButton__icon\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-primary\", \"LemonButton--no-content\", \"LemonButton--has-icon\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-primary LemonButton--no-content LemonButton--has-icon\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ActivationSideBar__close_button\"], \"attr__class\": \"ActivationSideBar__close_button\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ActivationSideBar__content\", \"pt-2\", \"px-4\", \"pb-16\"], \"attr__class\": \"ActivationSideBar__content pt-2 px-4 pb-16\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ActivationSideBar\"], \"attr__class\": \"ActivationSideBar\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\"], \"attr__class\": \"SideBar SideBar__layout\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 3000}","now":"2023-05-22T10:43:59.790498+00:00","sent_at":"2023-05-22T10:43:59.786000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684752248800&ver=1.57.2","method":"POST","content-encoding":"","ip":"127.0.0.1","now":"2023-05-22T10:44:08.814768+00:00","body":"H4sIAAAAAAAAA51W607cOBR+lVHETwLOPUFarUrZFduyiyqWalVURY7tJGYSO7WdubTi3fc4E4bJMEDVX0zO7fvO8blw98NhCyaMc+Yc4d5IgjvTK+YcO52SHVOGM+2c/XCOJPxx/sZkdn0z+w/UIMgXTGkuBSg8dOJFJ8jKCyWXmikQ/skVK+XKCilbcMJys+4YKC6YnhvZWQXplQL4vFcNKGpjurPT0wZoNLXU5ixFCJ0OBPXvUhQSK8pFlRPZdg0zjP5mVG/JHllrCDD1tIoOm1rg1sKOgXZIPmXgecGOuMGi6nFlnZhwb2+siyaKMZHXjFc1QGVJ+CRccmpqCBIhBMIFZ8tOKrO1TUMbfCt+tA4DK254ATBLVlgQ+Nit6kmUnPhWzgXQMjmnIFUJWSA5/1bW82CFqt7qDbcZenEaJpHvh8kJQsmxQ7k2XJDRr/p0cdl0q2/vL6vbeR3p4Or68+2n6B35x3jz9nODVotrfO/rMm53nmxw9VJIAbEgLKKAEBfRgMQkQVkYehFN3dDzqU8C6npxWKDUfTKPaZDaYL2t6i+w4DqnrJU5NOM9I1DKEjeaQcBKyb4bOnOrctCI6wZezFx4f+RGHo7dksZhHIVpnCHbK1JVWPDv2GyKvPXyM5RtvELKPDfLoiTxsYfCKLRMhDZYENsSB7vUeQBWOxOUQ/Fx0TCaQ+rwpLnmFJwf+WNi+ILlJcODcdngCrK5+wqqXVne4XUjMbWZAkAHaLWsJi0SBsPcKYab1pIDE0Zd0nAyryXUHXSsxbwZeNgXxQv4stS2bHSDyfwV/RHMMYM5tUN9eEQ1r0Q/DPTG1A4plQBrOT4fymEQH7fBQHXYBWxnHq1Vw9phYM9gSxkoxjjHusMC7EmDtbbb6c65gh4R570xUuSwHISxGw1qiY1RILCG4HfQ6tgRps5JzRvoTkC1X7IcuW1Y5IatbH9dwUPMWqnYbFwkD8cTXsUQesjvZZ+XeINi58t1O8VbrNb7YmhC0+sdbbt27YS1K9c231PSY3G3nF4sxewQ7FQ4BZ1ZyNkj4KR+0X799ipE+WJagLEoz55qW6yNmGKDXftzq3LN0KGvokdvodv+dHHXuS+2zDOLCSCs7wlg8BbgDeyAc6xAOP7K4dSsZX8AeTSY7RtOCMAJ+gkCrzX4QYcNk82xkfKNpPdDFJLavhy7sIYRtitgOISTOP6zOFADI+fMroyuJvn9Jbnokzb4shB/iW5erNoP6uMfxfpDP1+/+/L++/oju0L639V5MGwPKLFdHfsHK0lK5KIgQDSifhqkZcBeO1gp2AQ23JILKpf70bIARbGLwgRnCKMCeUERvhYO+xGLN/+GVHBP2TRg7NHQTyjEK8KMoSAhNE5fvqdgDlesiOypkWWpGSwYL8nQw9f/AYmT89LGCQAA","output":[{"uuid":"0188430f-2fef-0000-25cb-b19f141897ba","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"r7cv0okqfhk3x0gu\", \"$time\": 1684752247.007, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"http://localhost:8000/signup\", \"$referring_domain\": \"localhost:8000\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Load more events\"}, {\"tag_name\": \"button\", \"$el_text\": \"Load more events\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"my-8\", \"mx-auto\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary my-8 mx-auto\", \"nth_child\": 5, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"events\"], \"attr__class\": \"events\", \"attr__data-attr\": \"events-table\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\"], \"attr__class\": \"SideBar SideBar__layout\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"188430e34b77f0-0330d5d2838f3e8-412d2c3d-164b08-188430e34b82833\", \"$window_id\": \"188430e34b93056-047a90a0b013b48-412d2c3d-164b08-188430e34ba25e6\", \"$pageview_id\": \"188430e61d427d-04b49e037cd68d8-412d2c3d-164b08-188430e61d548b5\"}, \"offset\": 1790}","now":"2023-05-22T10:44:08.814768+00:00","sent_at":"2023-05-22T10:44:08.800000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?ip=1&_=1684771477160&ver=1.57.2","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-05-22T16:04:37.164197+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ2Y0hSZmFXNGlMQ0p3Y205d1pYSjBhV1Z6SWpwN0lpUnZjeUk2SWsxaFl5QlBVeUJZSWl3aUpHOXpYM1psY25OcGIyNGlPaUl4TUM0eE5TNHdJaXdpSkdKeWIzZHpaWElpT2lKR2FYSmxabTk0SWl3aUpHUmxkbWxqWlY5MGVYQmxJam9pUkdWemEzUnZjQ0lzSWlSamRYSnlaVzUwWDNWeWJDSTZJbWgwZEhBNkx5OXNiMk5oYkdodmMzUTZPREF3TUM4aUxDSWthRzl6ZENJNklteHZZMkZzYUc5emREbzRNREF3SWl3aUpIQmhkR2h1WVcxbElqb2lMeUlzSWlSaWNtOTNjMlZ5WDNabGNuTnBiMjRpT2pFeE15d2lKR0p5YjNkelpYSmZiR0Z1WjNWaFoyVWlPaUpsYmkxVlV5SXNJaVJ6WTNKbFpXNWZhR1ZwWjJoMElqbzVOelFzSWlSelkzSmxaVzVmZDJsa2RHZ2lPakUxTURBc0lpUjJhV1YzY0c5eWRGOW9aV2xuYUhRaU9qZzBNeXdpSkhacFpYZHdiM0owWDNkcFpIUm9Jam94TkRNekxDSWtiR2xpSWpvaWQyVmlJaXdpSkd4cFlsOTJaWEp6YVc5dUlqb2lNUzQxTnk0eUlpd2lKR2x1YzJWeWRGOXBaQ0k2SW5CM01XeHBaSEZ6TTNGMWJXaDJjekVpTENJa2RHbHRaU0k2TVRZNE5EYzNNVFEzTnk0eE5pd2laR2x6ZEdsdVkzUmZhV1FpT2lKblVVUkliSEI0Y1VOSVoxVnJhRFZ6TTB4UFZsVlJOVUZqVG5ReGEyMVdiREI0ZGs5aGFqSnpaalp0SWl3aUpHUmxkbWxqWlY5cFpDSTZJakU0T0RRek1HVXpOR0kxTTJOakxUQmtNMk0yWXpjd09UUTBNVFZrT0MwME1USmtNbU16WkMweE5qUmlNRGd0TVRnNE5ETXdaVE0wWWpaa016Z2lMQ0lrZFhObGNsOXBaQ0k2SW1kUlJFaHNjSGh4UTBoblZXdG9OWE16VEU5V1ZWRTFRV05PZERGcmJWWnNNSGgyVDJGcU1uTm1ObTBpTENKcGMxOWtaVzF2WDNCeWIycGxZM1FpT21aaGJITmxMQ0lrWjNKdmRYQnpJanA3SW5CeWIycGxZM1FpT2lJd01UZzRORE13WlMwek1UWmxMVEF3TURBdE5URmhOaTFtWkRZME5qVTBPRFk1TUdVaUxDSnZjbWRoYm1sNllYUnBiMjRpT2lJd01UZzRORE13WlMweU9UQTVMVEF3TURBdE5HUmxNUzA1T1RVM056SmhNVEEwTlRRaUxDSnBibk4wWVc1alpTSTZJbWgwZEhBNkx5OXNiMk5oYkdodmMzUTZPREF3TUNKOUxDSWtZWFYwYjJOaGNIUjFjbVZmWkdsellXSnNaV1JmYzJWeWRtVnlYM05wWkdVaU9tWmhiSE5sTENJa1lXTjBhWFpsWDJabFlYUjFjbVZmWm14aFozTWlPbHRkTENJa1ptVmhkSFZ5WlY5bWJHRm5YM0JoZVd4dllXUnpJanA3ZlN3aWNHOXpkR2h2WjE5MlpYSnphVzl1SWpvaU1TNDBNeTR3SWl3aWNtVmhiRzBpT2lKb2IzTjBaV1F0WTJ4cFkydG9iM1Z6WlNJc0ltVnRZV2xzWDNObGNuWnBZMlZmWVhaaGFXeGhZbXhsSWpwbVlXeHpaU3dpYzJ4aFkydGZjMlZ5ZG1salpWOWhkbUZwYkdGaWJHVWlPbVpoYkhObExDSWtjbVZtWlhKeVpYSWlPaUlrWkdseVpXTjBJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lJa1pHbHlaV04wSWl3aWRHOXJaVzRpT2lKd2FHTmZha2hqUkhVM2JUTmFkbTVKYm5CclluaHRTbkpMUldKNVNuVnJlVUZhUTNwNVMyVk1NSE5VZUVJemF5SXNJaVJ6WlhOemFXOXVYMmxrSWpvaU1UZzRORFF6TWpaaU4yUXhZakV4TFRCbE4ySTJNVGcwWVRJek9EVXdPQzAwTVRKa01tTXpaQzB4TmpSaU1EZ3RNVGc0TkRRek1qWmlOMlV4WldRd0lpd2lKSGRwYm1SdmQxOXBaQ0k2SWpFNE9EUTBNelE1Tm1FMFlpMHdaamMzTkdGbE9UUTRPVEk1TmpndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUTBNelE1Tm1FMU1qQXpNU0lzSWlSd1lXZGxkbWxsZDE5cFpDSTZJakU0T0RRME16UTVObUUyTVROallpMHdOak0wTmpjME9HRTNPR05pWXkwME1USmtNbU16WkMweE5qUmlNRGd0TVRnNE5EUXpORGsyWVRjeU1UZ3pJbjBzSW5ScGJXVnpkR0Z0Y0NJNklqSXdNak10TURVdE1qSlVNVFk2TURRNk16Y3VNVFl3V2lKOQ==","output":[{"uuid":"01884434-96bc-0000-a64d-d01794a3cbbd","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pw1lidqs3qumhvs1\", \"$time\": 1684771477.16, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"timestamp\": \"2023-05-22T16:04:37.160Z\"}","now":"2023-05-22T16:04:37.164197+00:00","sent_at":"2023-05-22T16:04:37.160000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?ip=1&_=1684771477161&ver=1.57.2","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-05-22T16:04:37.165076+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ3WVdkbGRtbGxkeUlzSW5CeWIzQmxjblJwWlhNaU9uc2lKRzl6SWpvaVRXRmpJRTlUSUZnaUxDSWtiM05mZG1WeWMybHZiaUk2SWpFd0xqRTFMakFpTENJa1luSnZkM05sY2lJNklrWnBjbVZtYjNnaUxDSWtaR1YyYVdObFgzUjVjR1VpT2lKRVpYTnJkRzl3SWl3aUpHTjFjbkpsYm5SZmRYSnNJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0x5SXNJaVJvYjNOMElqb2liRzlqWVd4b2IzTjBPamd3TURBaUxDSWtjR0YwYUc1aGJXVWlPaUl2SWl3aUpHSnliM2R6WlhKZmRtVnljMmx2YmlJNk1URXpMQ0lrWW5KdmQzTmxjbDlzWVc1bmRXRm5aU0k2SW1WdUxWVlRJaXdpSkhOamNtVmxibDlvWldsbmFIUWlPamszTkN3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRVd01Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPRFF6TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFME16TXNJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpVM0xqSWlMQ0lrYVc1elpYSjBYMmxrSWpvaWJuUjVkWE51Tm5Zek1uWmxhbWN3Y3lJc0lpUjBhVzFsSWpveE5qZzBOemN4TkRjM0xqRTJMQ0prYVhOMGFXNWpkRjlwWkNJNkltZFJSRWhzY0hoeFEwaG5WV3RvTlhNelRFOVdWVkUxUVdOT2RERnJiVlpzTUhoMlQyRnFNbk5tTm0waUxDSWtaR1YyYVdObFgybGtJam9pTVRnNE5ETXdaVE0wWWpVelkyTXRNR1F6WXpaak56QTVORFF4TldRNExUUXhNbVF5WXpOa0xURTJOR0l3T0MweE9EZzBNekJsTXpSaU5tUXpPQ0lzSWlSMWMyVnlYMmxrSWpvaVoxRkVTR3h3ZUhGRFNHZFZhMmcxY3pOTVQxWlZVVFZCWTA1ME1XdHRWbXd3ZUhaUFlXb3ljMlkyYlNJc0ltbHpYMlJsYlc5ZmNISnZhbVZqZENJNlptRnNjMlVzSWlSbmNtOTFjSE1pT25zaWNISnZhbVZqZENJNklqQXhPRGcwTXpCbExUTXhObVV0TURBd01DMDFNV0UyTFdaa05qUTJOVFE0Tmprd1pTSXNJbTl5WjJGdWFYcGhkR2x2YmlJNklqQXhPRGcwTXpCbExUSTVNRGt0TURBd01DMDBaR1V4TFRrNU5UYzNNbUV4TURRMU5DSXNJbWx1YzNSaGJtTmxJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0luMHNJaVJoZFhSdlkyRndkSFZ5WlY5a2FYTmhZbXhsWkY5elpYSjJaWEpmYzJsa1pTSTZabUZzYzJVc0lpUmhZM1JwZG1WZlptVmhkSFZ5WlY5bWJHRm5jeUk2VzEwc0lpUm1aV0YwZFhKbFgyWnNZV2RmY0dGNWJHOWhaSE1pT250OUxDSndiM04wYUc5blgzWmxjbk5wYjI0aU9pSXhMalF6TGpBaUxDSnlaV0ZzYlNJNkltaHZjM1JsWkMxamJHbGphMmh2ZFhObElpd2laVzFoYVd4ZmMyVnlkbWxqWlY5aGRtRnBiR0ZpYkdVaU9tWmhiSE5sTENKemJHRmphMTl6WlhKMmFXTmxYMkYyWVdsc1lXSnNaU0k2Wm1Gc2MyVXNJaVJ5WldabGNuSmxjaUk2SWlSa2FYSmxZM1FpTENJa2NtVm1aWEp5YVc1blgyUnZiV0ZwYmlJNklpUmthWEpsWTNRaUxDSjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkhObGMzTnBiMjVmYVdRaU9pSXhPRGcwTkRNeU5tSTNaREZpTVRFdE1HVTNZall4T0RSaE1qTTROVEE0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME5ETXlObUkzWlRGbFpEQWlMQ0lrZDJsdVpHOTNYMmxrSWpvaU1UZzRORFF6TkRrMllUUmlMVEJtTnpjMFlXVTVORGc1TWprMk9DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORFF6TkRrMllUVXlNRE14SWl3aUpIQmhaMlYyYVdWM1gybGtJam9pTVRnNE5EUXpORGsyWVRZeE0yTmlMVEEyTXpRMk56UTRZVGM0WTJKakxUUXhNbVF5WXpOa0xURTJOR0l3T0MweE9EZzBORE0wT1RaaE56SXhPRE1pZlN3aWRHbHRaWE4wWVcxd0lqb2lNakF5TXkwd05TMHlNbFF4Tmpvd05Eb3pOeTR4TmpGYUluMCUzRA==","output":[{"uuid":"01884434-96ba-0000-1404-a179647bd08a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ntyusn6v32vejg0s\", \"$time\": 1684771477.16, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"timestamp\": \"2023-05-22T16:04:37.161Z\"}","now":"2023-05-22T16:04:37.165076+00:00","sent_at":"2023-05-22T16:04:37.161000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/engage/?ip=1&_=1684771477165&ver=1.57.2","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-05-22T16:04:37.167862+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSWtaR2x5WldOMElpd2lKSEpsWm1WeWNtbHVaMTlrYjIxaGFXNGlPaUlrWkdseVpXTjBJaXdpWlcxaGFXd2lPaUo0WVhacFpYSkFjRzl6ZEdodlp5NWpiMjBpZlN3aUpIUnZhMlZ1SWpvaWNHaGpYMnBJWTBSMU4yMHpXblp1U1c1d2EySjRiVXB5UzBWaWVVcDFhM2xCV2tONmVVdGxUREJ6VkhoQ00yc2lMQ0lrWkdsemRHbHVZM1JmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadElpd2lKR1JsZG1salpWOXBaQ0k2SWpFNE9EUXpNR1V6TkdJMU0yTmpMVEJrTTJNMll6Y3dPVFEwTVRWa09DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqWmtNemdpTENJa2RYTmxjbDlwWkNJNkltZFJSRWhzY0hoeFEwaG5WV3RvTlhNelRFOVdWVkUxUVdOT2RERnJiVlpzTUhoMlQyRnFNbk5tTm0waWZRJTNEJTNE","output":[{"uuid":"01884434-96bb-0000-669a-d0fa1bef0768","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T16:04:37.167862+00:00","sent_at":"2023-05-22T16:04:37.165000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684771480232&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:40.235238+00:00","body":"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","output":[{"uuid":"01884434-a2ab-0000-af34-b060321fb9d0","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pewv8vh77ktqcv8n\", \"$time\": 1684771477.161, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"session-reset-on-load\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 3067}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0001-adb0-d108b4f12665","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"977yp0jwt21hnwcg\", \"$time\": 1684771477.166, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 3062}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0002-1714-12b5f9fc4941","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"p6w8hxbuqycejwr9\", \"$time\": 1684771477.166, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 3062}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0003-6004-77b16abfc46e","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"b3aji1adgd006z7q\", \"$time\": 1684771477.232, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"posthog-3000\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2996}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0004-6b5c-d8e7d6e97bd8","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"3ud3ntuo2qae4tw7\", \"$time\": 1684771477.233, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"enable-prompts\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2995}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0005-3e04-fa6fc146673a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"v5q0dt8g357ju35s\", \"$time\": 1684771477.24, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"notebooks\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2987}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0006-5a0e-177fb26860c5","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"evyz0oq5z2yo9zl8\", \"$time\": 1684771477.243, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"cloud-announcement\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2985}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0007-dc5e-bb723cae292a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"fwcsezx3qw2jryru\", \"$time\": 1684771477.252, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"hogql\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2976}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0008-5f7e-6efafcb7b2be","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"gzltwvh6qtff36oz\", \"$time\": 1684771477.266, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"hackathon-apm\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2962}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0009-c180-af233b44b000","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"gjxvn1u0l3l5fxqc\", \"$time\": 1684771477.267, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"early-access-feature\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2961}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000a-bd12-c96d4565d278","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"lt2fhodtixw6kfuv\", \"$time\": 1684771477.267, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"feedback-scene\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2961}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000b-02bb-7a705b509049","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"f255icw4jshgl94d\", \"$time\": 1684771477.282, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"require-email-verification\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2946}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000c-e137-db1cc0161182","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"6rvyf37nr7704osj\", \"$time\": 1684771477.349, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"session-recording-infinite-list\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2879}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000d-7bab-cce00f8e396c","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yu008bx11z13mm65\", \"$time\": 1684771477.349, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"session-recording-summary-listing\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2879}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000e-4ed9-e08a5a7ecb60","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"68vvaoju15ug02zn\", \"$time\": 1684771477.349, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"recordings-list-v2-enabled\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2879}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000f-15e6-c0c8a84bd8d3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ejk2l0xdtzty23na\", \"$time\": 1684771477.382, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2846}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0010-a367-b217cfce9758","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"q4csan5ar3uyxu1c\", \"$time\": 1684771477.402, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"instance\", \"$group_key\": \"http://localhost:8000\", \"$group_set\": {\"site_url\": \"http://localhost:8000\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2826}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0011-ab12-d83075b735b3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"client_request_failure\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"fux7i2k80t2uyqah\", \"$time\": 1684771477.622, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"pathname\": \"/api/billing-v2/\", \"method\": \"GET\", \"duration\": 341, \"status\": 404, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2606}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0012-7b5a-642b99f7c343","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"recording list fetched\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"jdnaxi7p1xu8abp6\", \"$time\": 1684771477.793, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"load_time\": 311, \"listing_version\": \"1\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2435}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0013-9ed2-da153edee668","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"lvp6x0x9grc01m4s\", \"$time\": 1684771478.077, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"data-exploration-insights\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2151}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0014-b658-f962d9c6de38","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"dashboard loading time\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ywaj59v3bl8q9scj\", \"$time\": 1684771478.16, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"loadingMilliseconds\": 816, \"dashboardId\": 1, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2068}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0015-fdde-825234a6f71f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"viewed dashboard\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"wb213rksdxcsobh3\", \"$time\": 1684771478.906, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"created_at\": \"2023-05-22T10:43:03.675923Z\", \"is_shared\": false, \"pinned\": true, \"creation_mode\": \"default\", \"sample_items_count\": 6, \"item_count\": 6, \"created_by_system\": true, \"dashboard_id\": 1, \"lastRefreshed\": \"2023-05-22T16:02:16.882Z\", \"refreshAge\": 142, \"trends_count\": 3, \"retention_count\": 1, \"lifecycle_count\": 1, \"funnels_count\": 1, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 1322}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684771480251&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:40.254254+00:00","body":"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","output":[{"uuid":"01884434-a2db-0000-c91c-f13ff43dd8c8","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-a2da-0000-ee43-723c53b20b8f\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"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\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": true, \"events_summary\": [{\"timestamp\": 1684771476282, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476788, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476788, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476795, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476795, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476800, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476800, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476896, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477161, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477166, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477172, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477175, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477175, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477208, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477225, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477229, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477229, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477231, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477238, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477238, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477239, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477247, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477247, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477248, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477252, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477252, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477262, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477281, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477286, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477300, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477327, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477328, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477343, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477370, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477402, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477402, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477410, \"type\": 4, \"data\": {\"href\": \"http://localhost:8000/home\", \"width\": 1433, \"height\": 843}}, {\"timestamp\": 1684771477428, \"type\": 2, \"data\": {}}, {\"timestamp\": 1684771477481, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477482, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477483, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477488, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477508, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477543, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477565, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477569, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477576, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477600, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477603, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477624, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771477631, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477664, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477701, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477773, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477796, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478421, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478426, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478429, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478432, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478433, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478447, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478449, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478472, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478476, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478478, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478480, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478481, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478491, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478494, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478501, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478552, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478779, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771479028, \"type\": 3, \"data\": {\"source\": 4, \"width\": 1433, \"height\": 540}}, {\"timestamp\": 1684771479110, \"type\": 3, \"data\": {\"source\": 2, \"type\": 6}}, {\"timestamp\": 1684771479754, \"type\": 3, \"data\": {\"source\": 1}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"offset\": 2839}","now":"2023-05-22T16:04:40.254254+00:00","sent_at":"2023-05-22T16:04:40.251000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684771483262&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:43.268842+00:00","body":"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","output":[{"uuid":"01884434-ae85-0000-0790-3f208b1ec925","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"b4chaybacz87b91v\", \"$time\": 1684771482.742, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"a\", \"$el_text\": \"Replay\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__data-attr\": \"menu-item-replay\", \"attr__href\": \"/replay/recent\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 6, \"nth_of_type\": 4}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\"], \"attr__class\": \"SideBar SideBar__layout\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 4, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 518}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0001-5a90-cd53a406401d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ue3df3rfc6j1h584\", \"$time\": 1684771482.901, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad091b0-03eb47f8265dc08-412d2c3d-164b08-1884434ad0a2092\"}, \"offset\": 360}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0002-988a-cc85b5fb42f1","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"recording viewed summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"xdfl85kbbeoftbor\", \"$time\": 1684771482.915, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 5563, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad091b0-03eb47f8265dc08-412d2c3d-164b08-1884434ad0a2092\"}, \"offset\": 346}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0003-e5d8-07b69a479391","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"y6wlixr7tcrdj71j\", \"$time\": 1684771482.963, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 298}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0004-c4f5-64ddb410d79f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"client_request_failure\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"c2nat8nn0n5nlgp1\", \"$time\": 1684771482.995, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"pathname\": \"/api/projects/1/session_recording_playlists\", \"method\": \"GET\", \"duration\": 113, \"status\": 404, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 266}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0005-2691-56f4c4e7d4bb","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"toast error\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"278rnjz16fs6rg1t\", \"$time\": 1684771482.999, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"message\": \"Load playlists failed: Endpoint not found.\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 262}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0006-1698-9970569297af","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"recording list fetched\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"i55f3ag6w7zbby1a\", \"$time\": 1684771483.127, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"load_time\": 130, \"listing_version\": \"1\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 134}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684771483260&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:43.265573+00:00","body":"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","output":[{"uuid":"01884434-ae8b-0001-daec-3c60a40202fa","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-ae8b-0000-78d7-f52731c49734\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"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\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684771480258, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771480759, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771481261, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771481896, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771482397, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771482683, \"type\": 3, \"data\": {\"source\": 2, \"type\": 5}}, {\"timestamp\": 1684771482683, \"type\": 3, \"data\": {\"source\": 2, \"type\": 1}}, {\"timestamp\": 1684771482684, \"type\": 3, \"data\": {\"source\": 2, \"type\": 5}}, {\"timestamp\": 1684771482739, \"type\": 3, \"data\": {\"source\": 2, \"type\": 0}}, {\"timestamp\": 1684771482743, \"type\": 3, \"data\": {\"source\": 2, \"type\": 2}}, {\"timestamp\": 1684771482757, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771482767, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771482881, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771482901, \"type\": 5, \"data\": {\"tag\": \"$pageview\", \"payload\": {\"href\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\"}}}, {\"timestamp\": 1684771482953, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482953, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482954, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482955, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482956, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482956, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482956, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482957, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482957, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482957, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482958, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482958, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482959, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482960, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482961, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482963, \"type\": 5, \"data\": {\"tag\": \"$pageview\", \"payload\": {\"href\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\"}}}, {\"timestamp\": 1684771482968, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482974, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482983, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482990, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482991, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482993, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771482995, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771483000, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771483002, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483013, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483131, \"type\": 3, \"data\": {\"source\": 0}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"offset\": 2993}","now":"2023-05-22T16:04:43.265573+00:00","sent_at":"2023-05-22T16:04:43.260000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684771486268&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:46.270510+00:00","body":"H4sIAAAAAAAAA+1VXW+bMBT9K5OVR5BsbMDkrWsnpR9aVKntQ6sqMuAkFDAMOyks4r/vOmRq0ynZHqY+5Qn73nPONZwj87RBci2VQWM00krUelkZ5KC6qWrZmExqNN68dWapMMJWTFdLNKYO+l3Q1apJoIQdZGRrgPb07CBhTJPFK2Nl7L6RZbXebjaoFg3MvUzRmATMc1A2rGhveWm6ZfSglpVSG1HWtstZGBLGKaU+tEZaap1VamapiHDOGPWCOExJTIiLZRgHhDPhUe5j7jLipV5CUxeGxLB/w0siUwwvPXrNVFq9vpdjUSBY7OJ5GDIhI8YjLwoOaVmw72FKQMtUuVQgUy+T2cskuViFJX1cq0tV53FbXjXX3+LuapV3Z4/nP7treYP1XfuV5sBMM20ylZjhGIvbi0lRtz/OJ4v7fOlrejN9uL/1z5LvhuTlQ4Hb9VS8eHoelAg+STWfawlmehHFvfN53m4G+8IIaO8bQDZdAdwNEiorhQG73LoQnQueGqijZqVUphao7+15dyoQh32VpBAaFuiuEvB15t1sZuzqy4e9a5aylK5bZIvln01XNk3VwKS9JP5D3EJCT3E7GjffZ/89bgS4lc5sZIaItWgcAK6DeSE8h6wEg2fT3Ulwf9DFyD+5eNxFanP+CS5y/y8uusyHC36L9ThcBhbMQWn3j4DKPhpv0wdohnfSdsQOHX5AU8jqoZAw5vNTSI6FhHDM++dfwg/U6TcIAAA=","output":[{"uuid":"01884434-ba3e-0001-712d-9832c78876cc","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-ba3e-0000-51fd-ebb6a55b6144\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAJ6Sa2QC/8VU22qDQBA9nxJ8rhA1RtM/6FNf+hZKsbk0QqKipkSCv972zLpuYy6thEAYVndn5syZmb18f02xh4USFTIsOHvEAB4e+LUwR0RLpLXiVyDFFjlmxneofUtqdvwWWj/Fq7ZIjJKYGO/Eit+pT07thrE/O1ZhzIgXa0LkEzNqbA7GGMHV6PhI76E+YJ9TDhlrk3FMTuGTGjdk+o0RMkpAcfgPGU/E18h79mt/VG2AiWG/jGsykZzXJo+9QiSqB5HqRcqVzarWXFecNX0pDUJ2aUsfwST44LpW0vbkOC+3V14zxVcYnYUXZhIp9hhLZvJGKY1u8I/d5lip07Tg3Gb0mLmueiFtYnJKytFU9/f5vO0Zk5l3kzPmaN+MloIZtLvbPUc7vRqb2JXWuMzFO3O3Avp263pmH5eMu6CmPd/1lfVPeMPuUX9I3tvVb7Mav/NW7EzU0NyJ6kDnXHjF3F5cQ45TLtH7Z7j8C1xBDy55AUdX7u5IdSVU6B+N+AMddAYAAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684771483335, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483713, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483953, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771484458, \"type\": 3, \"data\": {\"source\": 1}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"offset\": 2930}","now":"2023-05-22T16:04:46.270510+00:00","sent_at":"2023-05-22T16:04:46.268000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/s/?compression=gzip-js&ip=1&_=1684771486823&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:46.831635+00:00","body":"H4sIAAAAAAAAA+2US0/jMBSF/4vVZTLyK85jx8BIHUCDkIBFEaqc2DQmTeKJ3TaZqv99bngIWHRTqbvuknPvOb7Kd53HLdJr3XiUoYlrpHVl61GAbNda3XmjHcq2n5W5kl6Oih+sRhkL0Ifg2lVXgESD95oIkFEoI7HYgWRq7bysLQgi4XFMeCJihqE0cdo50zbzsRuRJOGcUZHHiuSEhFjHuSAJl5QlEU5CTqiiBVMhETyH989+TbTCMPlkYxrVbr7G8VRInof4OY651ClPUpqKfVljc0QxI5Dl20o3EGPLYv4yLS5Wcc1m6+Z3Y6u8ry+7q1/5cLmqhrPZ+b/hSl9jd9f/ZBU4lXHeNIV/G2NxezFd2v7v+XRxX5WRY9c3D/e30Vnxx5Oqfljifn0jX6h7FjX6/rUQxZSFOAopvSMiwzzj4kfM6Az6jk1uPzd64nYYN3YEbhgO1r0H2+NTgKT3nclXfoyBu/12BVOwfS2A2Q9L8G6RbEwtPXAM7VIOIYzvQUdWrpxWaLfbQWan63b9GjgeoNTr0/7tSE/bcdh2pEfYDh6gjVG+BEScQb3UZlFCesLZXoLR6b98GMEIA8Gn/+KghNVTBwAA","output":[{"uuid":"01884434-bc70-0001-6e8d-1718c6247aff","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-bc70-0000-a5ba-6c77cd70a82b\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAJ6Sa2QC/71Syw6CMBCcTzGcNUFBRH/FcMBHhETE0PoghF9Xh6VUjfFCjGlStrOzs9Mt99sSFRxolDhiy2iBATwMuTvYIGYmNmjDU8hxQoG15U4M910hMGhKjRYZY0a0tuwUGdlK9DNWdqwAIXxyZ4x9xgEjD66p/JfXvj4nP/Hp2u5bXLkrgy8RmUyjoVmTYsVaLQ47TvUx97nt/r2uddJ43lsflVQcZAaxzCLnacRb7XkuGbVz0bbCYS6msiLSOKhlda4Lohk1zm9+n3facL3ifV9h/pNX8A33ItPUSGxXnzqdUkJ+ih2/2uRDyffzPpU/PcIDAvlBHZgDAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684771486730, \"type\": 3, \"data\": {\"source\": 2, \"type\": 6}}, {\"timestamp\": 1684771486732, \"type\": 3, \"data\": {\"source\": 2, \"type\": 6}}, {\"timestamp\": 1684771486739, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771486750, \"type\": 3, \"data\": {\"source\": 4, \"width\": 1433, \"height\": 843}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"timestamp\": \"2023-05-22T16:04:46.732Z\"}","now":"2023-05-22T16:04:46.831635+00:00","sent_at":"2023-05-22T16:04:46.823000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1684771486824&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:46.833134+00:00","body":"H4sIAAAAAAAAA51U2W7cNhT9lUCw3yKbpKhtgKLw0sJN0hpB4qBwEAgUeaWRhyOqFKXxOMi/91IzmQWd9CEvWs65Ow/v568BjNC6YBacdaIGDWKE4HXQWdOBdQ30wexrcGbwFfwp5Kv7D6/+RhqBYgTbN6ZFgpILGl8Qj5fWrHqwCP7eWKjMswcVjI2Ewq07QOIW+oUznSfkYC0mLwarkZg7180uL7WRQs9N72YZIeTSQqfFGl8SLX+tGu0w7y/n6fU5Yz30voQCSWNV09aFGqxwCCF5Hl1trHze7T9jO1OPsBt8LmC9Yw/dJ3IUetg6J2QD+cEIZ+zOqXb+M73d0PvJbQ3i6/N4y02jPoEL6ZOeIJRwUFTWLHfJQkbVJp0foB8TTu54ZJ7ohJu3YunnfTzBg0PanyCl0QGsRVsPqAX0hTZ8+OBdemkB2mIOTT3HjHnK9+CqUW6OQWJCEBwbWHXGup1txn3wHfzdmkce1k2JaVZQ+iT4c6iqizi9YB5vWizLFY1CVKRYXTTq1Yq6l7KtPO8a3yhNMp6mlGfJRcbY60A1vWtaufWr39/e6e75n5u7+mExj/vo3f2nh/fxlfzL0cXykybP4714Yn2VLA8kO7nSDFsgEPEyjqQMiYpkIlOSc05jlYWcMsVkpEKa8JJk4d48UVHmgw1+qj9RRdMXCpamQEk9gcRRVkL3gAFra4Zuupk7KiDbvGFEEwhRBiSMqUjCSiU8iXEqOfEX29hatM3LpPJDL5aTfOPFFdAwz+M0ZYISHnNfSds70UoviZO3NPiGVYnBIdi5wUKBwxelBlVg63ikRd8odP5ev9f7iMIGMRlXWtTYzecvSB1iRSfW2gjlO8UEHWabm/pIIjya9o4FoZe+ODQBFUrdyMXc4NyRg6Vo9FSHP1Ex4p8vbVdNr4Vc/A9/JvFqGg2Fxtz7TQPtiQadHbzHf/fS5mt/547dgnHSOS5MwIXot+eZwvWJ57pDp+VmsBXf9551ZgEe6eayeLqTt0O6jB7H9o+2W5TPyzf27W/l+s2wWF893rys38I70n98vo4W05XeFrnTOI9YUqaKlpSGBNIyoRkXLMpiFPUpkW/sgYKaVs6qaZVZHYbjeSJ4GZIqTbmAnGc5y5MfxfLGMSMR3ayvGi8gHEcTKqYRjVCmSQm0opTJjPHqR/HQnFFIEq9NvyFQwcsOozHCopDEIWMfaTIjfManhfEYfPvyL6II7K8OBwAA","output":[{"uuid":"01884434-bc71-0000-a710-92ebd1e26e1f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageleave\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"a7ang3vlww1tzbnf\", \"$time\": 1684771486.822, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"timestamp\": \"2023-05-22T16:04:46.822Z\"}","now":"2023-05-22T16:04:46.833134+00:00","sent_at":"2023-05-22T16:04:46.824000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} From 663f87a2135ba682c5edf2ba08279d2beabd161c Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 23 May 2023 14:34:48 +0100 Subject: [PATCH 015/249] Add timesource trait (#13) --- Cargo.lock | 11 +++++++++++ Cargo.toml | 2 +- src/capture.rs | 2 ++ src/lib.rs | 1 + src/main.rs | 6 +++++- src/router.rs | 6 ++++-- src/time.rs | 16 ++++++++++++++++ tests/django_compat.rs | 19 ++++++++++++++++++- 8 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 src/time.rs diff --git a/Cargo.lock b/Cargo.lock index 2d2ffd6a7267f..f6922b7ae4e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1139,8 +1139,10 @@ version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ + "itoa", "serde", "time-core", + "time-macros", ] [[package]] @@ -1149,6 +1151,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 23cfe2d24d257..da6382923702d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" governor = "0.5.1" tower_governor = "0.0.4" -time = "0.3.20" +time = { version = "0.3.20", features = ["formatting"]} tower-http = { version = "0.4.0", features = ["trace"] } bytes = "1" anyhow = "1.0" diff --git a/src/capture.rs b/src/capture.rs index e8edaa005b4c9..9bba1e76fce5b 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -153,6 +153,7 @@ mod tests { async fn all_events_have_same_token() { let state = State { sink: Arc::new(sink::PrintSink {}), + timesource: Arc::new(crate::time::SystemTime {}), }; let events = vec![ @@ -176,6 +177,7 @@ mod tests { async fn all_events_have_different_token() { let state = State { sink: Arc::new(sink::PrintSink {}), + timesource: Arc::new(crate::time::SystemTime {}), }; let events = vec![ diff --git a/src/lib.rs b/src/lib.rs index d8cf49f4b620a..6b9e84dead103 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ pub mod event; pub mod router; +pub mod time; mod api; mod capture; diff --git a/src/main.rs b/src/main.rs index 85412e19adc35..9bce0ab50559b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,13 @@ use std::net::SocketAddr; +use crate::time::SystemTime; + mod api; mod capture; mod event; mod router; mod sink; +mod time; mod token; #[tokio::main] @@ -12,7 +15,8 @@ async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - let app = router::router(); + let st = SystemTime {}; + let app = router::router(st); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/src/router.rs b/src/router.rs index 9af45c76454c3..0c7e7978694ab 100644 --- a/src/router.rs +++ b/src/router.rs @@ -3,16 +3,18 @@ use std::sync::Arc; use axum::{routing::post, Router}; use tower_http::trace::TraceLayer; -use crate::{capture, sink}; +use crate::{capture, sink, time::TimeSource}; #[derive(Clone)] pub struct State { pub sink: Arc, + pub timesource: Arc, } -pub fn router() -> Router { +pub fn router(timesource: TZ) -> Router { let state = State { sink: Arc::new(sink::PrintSink {}), + timesource: Arc::new(timesource), }; Router::new() diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000000000..e510f5f692fcc --- /dev/null +++ b/src/time.rs @@ -0,0 +1,16 @@ +pub trait TimeSource { + // Return an ISO timestamp + fn current_time(&self) -> String; +} + +#[derive(Clone)] +pub struct SystemTime {} + +impl TimeSource for SystemTime { + fn current_time(&self) -> String { + let time = time::OffsetDateTime::now_utc(); + + time.format(&time::format_description::well_known::Iso8601::DEFAULT) + .expect("failed to iso8601 format timestamp") + } +} diff --git a/tests/django_compat.rs b/tests/django_compat.rs index 24d526375f7bf..3cfee2263dffb 100644 --- a/tests/django_compat.rs +++ b/tests/django_compat.rs @@ -4,9 +4,11 @@ use base64::engine::general_purpose; use base64::Engine; use capture::event::ProcessedEvent; use capture::router::router; +use capture::time::TimeSource; use serde::Deserialize; use std::fs::File; use std::io::{BufRead, BufReader}; +use time::OffsetDateTime; #[derive(Debug, Deserialize)] struct RequestDump { @@ -22,6 +24,17 @@ struct RequestDump { static REQUESTS_DUMP_FILE_NAME: &str = "tests/requests_dump.jsonl"; +#[derive(Clone)] +pub struct FixedTime { + pub time: time::OffsetDateTime, +} + +impl TimeSource for FixedTime { + fn current_time(&self) -> String { + self.time.to_string() + } +} + #[tokio::test] async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let file = File::open(REQUESTS_DUMP_FILE_NAME)?; @@ -42,7 +55,11 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { case.method ); - let app = router(); + let timesource = FixedTime { + time: OffsetDateTime::now_utc(), + }; + let app = router(timesource); + let client = TestClient::new(app); let mut req = client.post(&case.path).body(raw_body); if !case.content_encoding.is_empty() { From 9da6bfe9af8c63906a5683ed774c2d3f3a138878 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 23 May 2023 14:53:56 +0100 Subject: [PATCH 016/249] Add MemorySink and check we emit the right amount of messages (#14) * add memorySink * don't bother with time parsing --- src/api.rs | 4 ++-- src/lib.rs | 7 +++---- src/main.rs | 3 +-- src/router.rs | 10 +++++++-- tests/django_compat.rs | 47 +++++++++++++++++++++++++++++++++++------- 5 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/api.rs b/src/api.rs index b3a18e696c105..09e79ae7b961b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -12,12 +12,12 @@ pub struct CaptureRequest { pub properties: HashMap, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum CaptureResponseCode { Ok = 1, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct CaptureResponse { pub status: CaptureResponseCode, } diff --git a/src/lib.rs b/src/lib.rs index 6b9e84dead103..641385f93d553 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,7 @@ +pub mod api; +mod capture; pub mod event; pub mod router; +pub mod sink; pub mod time; - -mod api; -mod capture; -mod sink; mod token; diff --git a/src/main.rs b/src/main.rs index 9bce0ab50559b..c550c218fec93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,7 @@ async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - let st = SystemTime {}; - let app = router::router(st); + let app = router::router(SystemTime {}, sink::PrintSink {}); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/src/router.rs b/src/router.rs index 0c7e7978694ab..047ceec932607 100644 --- a/src/router.rs +++ b/src/router.rs @@ -11,9 +11,15 @@ pub struct State { pub timesource: Arc, } -pub fn router(timesource: TZ) -> Router { +pub fn router< + TZ: TimeSource + Send + Sync + 'static, + S: sink::EventSink + Send + Sync + 'static, +>( + timesource: TZ, + sink: S, +) -> Router { let state = State { - sink: Arc::new(sink::PrintSink {}), + sink: Arc::new(sink), timesource: Arc::new(timesource), }; diff --git a/tests/django_compat.rs b/tests/django_compat.rs index 3cfee2263dffb..e305412a0d43f 100644 --- a/tests/django_compat.rs +++ b/tests/django_compat.rs @@ -1,14 +1,17 @@ +use async_trait::async_trait; use axum::http::StatusCode; use axum_test_helper::TestClient; use base64::engine::general_purpose; use base64::Engine; +use capture::api::{CaptureResponse, CaptureResponseCode}; use capture::event::ProcessedEvent; use capture::router::router; +use capture::sink::EventSink; use capture::time::TimeSource; use serde::Deserialize; use std::fs::File; use std::io::{BufRead, BufReader}; -use time::OffsetDateTime; +use std::sync::{Arc, Mutex}; #[derive(Debug, Deserialize)] struct RequestDump { @@ -26,7 +29,7 @@ static REQUESTS_DUMP_FILE_NAME: &str = "tests/requests_dump.jsonl"; #[derive(Clone)] pub struct FixedTime { - pub time: time::OffsetDateTime, + pub time: String, } impl TimeSource for FixedTime { @@ -35,6 +38,30 @@ impl TimeSource for FixedTime { } } +#[derive(Clone, Default)] +struct MemorySink { + events: Arc>>, +} + +impl MemorySink { + fn len(&self) -> usize { + self.events.lock().unwrap().len() + } +} + +#[async_trait] +impl EventSink for MemorySink { + async fn send(&self, event: ProcessedEvent) -> anyhow::Result<()> { + self.events.lock().unwrap().push(event); + Ok(()) + } + + async fn send_batch(&self, events: &[ProcessedEvent]) -> anyhow::Result<()> { + self.events.lock().unwrap().extend_from_slice(&events); + Ok(()) + } +} + #[tokio::test] async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let file = File::open(REQUESTS_DUMP_FILE_NAME)?; @@ -42,7 +69,6 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { for line in reader.lines() { let case: RequestDump = serde_json::from_str(&line?)?; - if !case.path.starts_with("/e/") { println!("Skipping {} test case", &case.path); continue; @@ -55,10 +81,9 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { case.method ); - let timesource = FixedTime { - time: OffsetDateTime::now_utc(), - }; - let app = router(timesource); + let sink = MemorySink::default(); + let timesource = FixedTime { time: case.now }; + let app = router(timesource, sink.clone()); let client = TestClient::new(app); let mut req = client.post(&case.path).body(raw_body); @@ -72,8 +97,14 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { req = req.header("X-Forwarded-For", case.ip); } let res = req.send().await; - assert_eq!(res.status(), StatusCode::OK, "{}", res.text().await); + assert_eq!( + Some(CaptureResponse { + status: CaptureResponseCode::Ok + }), + res.json().await + ); + assert_eq!(sink.len(), case.output.len()) } Ok(()) } From de20a3c07ae11163230fcef134d33b9e191fef00 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 25 May 2023 16:18:15 +0100 Subject: [PATCH 017/249] Add liveness/readiness (#17) --- src/router.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/router.rs b/src/router.rs index 047ceec932607..199cd2095e0a0 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use axum::{routing::post, Router}; +use axum::{ + routing::{get, post}, + Router, +}; use tower_http::trace::TraceLayer; use crate::{capture, sink, time::TimeSource}; @@ -11,6 +14,10 @@ pub struct State { pub timesource: Arc, } +async fn index() -> &'static str { + "capture" +} + pub fn router< TZ: TimeSource + Send + Sync + 'static, S: sink::EventSink + Send + Sync + 'static, @@ -25,6 +32,7 @@ pub fn router< Router::new() // TODO: use NormalizePathLayer::trim_trailing_slash + .route("/", get(index)) .route("/capture", post(capture::event)) .route("/capture/", post(capture::event)) .route("/batch", post(capture::event)) From 351be7f7ee6236facb73e008a1dbd47a18f94d1c Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 26 May 2023 11:04:38 +0100 Subject: [PATCH 018/249] Write Kafka sink, using librdkafka (#16) * Add rdkafka dep (requires cmake install) * Send batch no reference * I think this batching thing works * fmt --- Cargo.lock | 127 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/capture.rs | 2 +- src/event.rs | 6 ++ src/main.rs | 16 +++++- src/sink.rs | 81 +++++++++++++++++++++++++- tests/django_compat.rs | 2 +- 7 files changed, 228 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6922b7ae4e7d..d6275655449b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,7 @@ dependencies = [ "flate2", "governor", "mockall", + "rdkafka", "serde", "serde_json", "serde_urlencoded", @@ -156,12 +157,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "crc32fast" version = "1.3.2" @@ -554,6 +570,18 @@ version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +[[package]] +name = "libz-sys" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -711,6 +739,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -784,6 +833,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -820,6 +875,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.56" @@ -893,6 +958,36 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rdkafka" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8acd8f5c5482fdf89e8878227bafa442d8c4409f6287391c85549ca83626c27" +dependencies = [ + "futures", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio", +] + +[[package]] +name = "rdkafka-sys" +version = "3.0.0+1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca35e95c88e08cdc643b25744e38ccee7c93c7e90d1ac6850fe74cbaa40803c3" +dependencies = [ + "cmake", + "libc", + "libz-sys", + "num_enum", + "pkg-config", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1219,6 +1314,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml_datetime" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" + +[[package]] +name = "toml_edit" +version = "0.19.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1408,6 +1520,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -1613,6 +1731,15 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "winnow" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index da6382923702d..340110740e3fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ base64 = "0.21.1" uuid = { version = "1.3.3", features = ["serde", "v4"] } async-trait = "0.1.68" serde_urlencoded = "0.7.1" +rdkafka = { version = "0.25", features = ["cmake-build"] } [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/src/capture.rs b/src/capture.rs index 9bba1e76fce5b..541ef315cd895 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -125,7 +125,7 @@ pub async fn process_events( return Err(String::from("Failed to send event to sink")); } } else { - let sent = sink.send_batch(&events).await; + let sent = sink.send_batch(events).await; if let Err(e) = sent { tracing::error!("Failed to send batch events to sink: {:?}", e); diff --git a/src/event.rs b/src/event.rs index c5f09db61cd4f..8cac0ecf8a1ab 100644 --- a/src/event.rs +++ b/src/event.rs @@ -82,6 +82,12 @@ pub struct ProcessedEvent { pub token: String, } +impl ProcessedEvent { + pub fn key(&self) -> String { + format!("{}:{}", self.token, self.distinct_id) + } +} + #[cfg(test)] mod tests { use super::Compression; diff --git a/src/main.rs b/src/main.rs index c550c218fec93..77d1e8d1a016f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use std::env; use std::net::SocketAddr; use crate::time::SystemTime; @@ -12,11 +13,22 @@ mod token; #[tokio::main] async fn main() { + let use_print_sink = env::var("PRINT_SINK").is_ok(); + + let app = if use_print_sink { + router::router(SystemTime {}, sink::PrintSink {}) + } else { + let brokers = env::var("KAFKA_BROKERS").expect("Expected KAFKA_BROKERS"); + let topic = env::var("KAFKA_TOPIC").expect("Expected KAFKA_TOPIC"); + + let sink = sink::KafkaSink::new(topic, brokers).unwrap(); + + router::router(SystemTime {}, sink) + }; + // initialize tracing tracing_subscriber::fmt::init(); - let app = router::router(SystemTime {}, sink::PrintSink {}); - // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); diff --git a/src/sink.rs b/src/sink.rs index 44592dcf7b409..20ab3fc70a429 100644 --- a/src/sink.rs +++ b/src/sink.rs @@ -1,12 +1,16 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use async_trait::async_trait; +use tokio::task::JoinSet; + +use rdkafka::config::ClientConfig; +use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; use crate::event::ProcessedEvent; #[async_trait] pub trait EventSink { async fn send(&self, event: ProcessedEvent) -> Result<()>; - async fn send_batch(&self, events: &[ProcessedEvent]) -> Result<()>; + async fn send_batch(&self, events: Vec) -> Result<()>; } pub struct PrintSink {} @@ -18,7 +22,7 @@ impl EventSink for PrintSink { Ok(()) } - async fn send_batch(&self, events: &[ProcessedEvent]) -> Result<()> { + async fn send_batch(&self, events: Vec) -> Result<()> { let span = tracing::span!(tracing::Level::INFO, "batch of events"); let _enter = span.enter(); @@ -29,3 +33,74 @@ impl EventSink for PrintSink { Ok(()) } } + +#[derive(Clone)] +pub struct KafkaSink { + producer: FutureProducer, + topic: String, +} + +impl KafkaSink { + pub fn new(topic: String, brokers: String) -> Result { + let producer: FutureProducer = ClientConfig::new() + .set("bootstrap.servers", &brokers) + .create()?; + + Ok(KafkaSink { producer, topic }) + } +} + +impl KafkaSink { + async fn kafka_send( + producer: FutureProducer, + topic: String, + event: ProcessedEvent, + ) -> Result<()> { + let payload = serde_json::to_string(&event)?; + + let key = event.key(); + + match producer.send_result(FutureRecord { + topic: topic.as_str(), + payload: Some(&payload), + partition: None, + key: Some(&key), + timestamp: None, + headers: None, + }) { + Ok(_) => {} + Err(e) => { + tracing::error!("failed to produce event: {}", e.0); + + // TODO: Improve error handling + return Err(anyhow!("failed to produce event {}", e.0)); + } + } + + Ok(()) + } +} + +#[async_trait] +impl EventSink for KafkaSink { + async fn send(&self, event: ProcessedEvent) -> Result<()> { + Self::kafka_send(self.producer.clone(), self.topic.clone(), event).await + } + + async fn send_batch(&self, events: Vec) -> Result<()> { + let mut set = JoinSet::new(); + + for event in events { + let producer = self.producer.clone(); + let topic = self.topic.clone(); + + set.spawn(Self::kafka_send(producer, topic, event)); + } + + while let Some(res) = set.join_next().await { + println!("{:?}", res); + } + + Ok(()) + } +} diff --git a/tests/django_compat.rs b/tests/django_compat.rs index e305412a0d43f..c8ac9a4ccd252 100644 --- a/tests/django_compat.rs +++ b/tests/django_compat.rs @@ -56,7 +56,7 @@ impl EventSink for MemorySink { Ok(()) } - async fn send_batch(&self, events: &[ProcessedEvent]) -> anyhow::Result<()> { + async fn send_batch(&self, events: Vec) -> anyhow::Result<()> { self.events.lock().unwrap().extend_from_slice(&events); Ok(()) } From 938b3a193d09170d05b0bda159c5a621ac1aecce Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Wed, 6 Sep 2023 11:46:32 +0100 Subject: [PATCH 019/249] Fix docker build (#18) * update and fix dockerfile * Update dependencies --- Cargo.lock | 534 +++++++++++++++++++++++++++++++---------------------- Cargo.toml | 2 +- Dockerfile | 4 +- 3 files changed, 320 insertions(+), 220 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6275655449b9..d8e3ef016a0a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -10,28 +19,28 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -42,13 +51,13 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.15" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b32c5ea3aabaf4deb5f5ced2d688ec0844c881c9e6c696a8b769a05fc691e62" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "bitflags", + "bitflags 1.3.2", "bytes", "futures-util", "http", @@ -107,11 +116,26 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" [[package]] name = "bitflags" @@ -119,11 +143,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytes" @@ -159,9 +189,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -189,26 +222,32 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] [[package]] name = "dashmap" -version = "5.4.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.0", "lock_api", "once_cell", "parking_lot_core", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "difflib" version = "0.4.0" @@ -223,24 +262,30 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", @@ -263,9 +308,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -342,7 +387,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -383,15 +428,21 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + [[package]] name = "governor" version = "0.5.1" @@ -412,9 +463,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -422,7 +473,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -435,14 +486,17 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "http" @@ -468,9 +522,9 @@ dependencies = [ [[package]] name = "http-range-header" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" @@ -480,15 +534,15 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -501,7 +555,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -510,9 +564,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -525,14 +579,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", ] [[package]] name = "ipnet" -version = "2.7.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "itertools" @@ -545,15 +609,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -566,15 +630,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libz-sys" -version = "1.1.9" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" dependencies = [ "cc", "libc", @@ -584,9 +648,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -594,12 +658,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "mach" @@ -612,15 +673,15 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" @@ -649,12 +710,11 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -722,18 +782,18 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", @@ -760,11 +820,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "overload" @@ -784,48 +853,48 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-targets", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.31", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -887,9 +956,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -912,9 +981,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -955,16 +1024,17 @@ version = "10.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "rdkafka" -version = "0.25.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8acd8f5c5482fdf89e8878227bafa442d8c4409f6287391c85549ca83626c27" +checksum = "053adfa02fab06e86c01d586cc68aa47ee0ff4489a59469081dc12cbcde578bf" dependencies = [ - "futures", + "futures-channel", + "futures-util", "libc", "log", "rdkafka-sys", @@ -977,9 +1047,9 @@ dependencies = [ [[package]] name = "rdkafka-sys" -version = "3.0.0+1.6.0" +version = "4.6.0+2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca35e95c88e08cdc643b25744e38ccee7c93c7e90d1ac6850fe74cbaa40803c3" +checksum = "ad63c279fca41a27c231c450a2d2ad18288032e9cbb159ad16c9d96eba35aaaf" dependencies = [ "cmake", "libc", @@ -990,18 +1060,30 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.8.1" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -1010,15 +1092,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64", "bytes", @@ -1051,49 +1133,55 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -1102,10 +1190,11 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.11" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ + "itoa", "serde", ] @@ -1141,18 +1230,18 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" @@ -1164,6 +1253,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "syn" version = "1.0.109" @@ -1177,9 +1276,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -1200,22 +1299,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -1230,10 +1329,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ + "deranged", "itoa", "serde", "time-core", @@ -1242,15 +1342,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] @@ -1272,11 +1372,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -1284,20 +1384,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.31", ] [[package]] @@ -1316,17 +1416,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.9" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d964908cec0d030b812013af25a0e57fddfadb1e066ecc6681d86253129d4f" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ - "indexmap", + "indexmap 2.0.0", "toml_datetime", "winnow", ] @@ -1349,11 +1449,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags", + "bitflags 2.4.0", "bytes", "futures-core", "futures-util", @@ -1413,20 +1513,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.31", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -1445,9 +1545,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -1465,9 +1565,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -1480,9 +1580,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -1495,9 +1595,9 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -1506,9 +1606,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.3.3" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "getrandom", "serde", @@ -1534,11 +1634,10 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -1556,9 +1655,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1566,24 +1665,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.31", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -1593,9 +1692,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1603,28 +1702,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.31", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-streams" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" dependencies = [ "futures-util", "js-sys", @@ -1635,9 +1734,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -1667,18 +1766,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1691,60 +1790,61 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.4.6" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 340110740e3fc..7514974a7e765 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ base64 = "0.21.1" uuid = { version = "1.3.3", features = ["serde", "v4"] } async-trait = "0.1.68" serde_urlencoded = "0.7.1" -rdkafka = { version = "0.25", features = ["cmake-build"] } +rdkafka = { version = "0.34", features = ["cmake-build"] } [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/Dockerfile b/Dockerfile index f9e1ce8920971..d7f528619e1a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM lukemathwalker/cargo-chef:latest-rust-1.68.0 AS chef +FROM lukemathwalker/cargo-chef:latest-rust-1.72.0 AS chef WORKDIR app FROM chef AS planner @@ -8,7 +8,7 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder # Ensure working C compile setup (not installed by default in arm64 images) -RUN apt update && apt install build-essential -y +RUN apt update && apt install build-essential cmake -y COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json From fca44ad6c9e245656e0d7053e6789673879d9038 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 8 Sep 2023 14:19:07 +0100 Subject: [PATCH 020/249] Correct debian version (#19) The other version has glibc errors. I should investigate, but don't have time rn. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d7f528619e1a1..c44e53be54fc8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM lukemathwalker/cargo-chef:latest-rust-1.72.0 AS chef +FROM lukemathwalker/cargo-chef:latest-rust-1.72.0-buster AS chef WORKDIR app FROM chef AS planner From d3f5de66fbf27e68963bd071e0b48fa7cc8912c6 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 8 Sep 2023 16:05:11 +0100 Subject: [PATCH 021/249] Only build ARM images, as we only run ARM (#20) --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f6b8ebb7bb44a..76d3ef09d2d1b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -52,7 +52,7 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - platforms: linux/amd64, linux/arm64 + platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max build-args: RUST_BACKTRACE=1 From a9cc8a2d6724e1be13f0d8f65a172b444d48cf40 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 11 Sep 2023 12:48:57 +0100 Subject: [PATCH 022/249] Allow setting bind address (#21) --- src/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 77d1e8d1a016f..3e6acde32af15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use std::env; -use std::net::SocketAddr; use crate::time::SystemTime; @@ -14,6 +13,7 @@ mod token; #[tokio::main] async fn main() { let use_print_sink = env::var("PRINT_SINK").is_ok(); + let address = env::var("ADDRESS").unwrap_or(String::from("127.0.0.1:3000")); let app = if use_print_sink { router::router(SystemTime {}, sink::PrintSink {}) @@ -31,11 +31,10 @@ async fn main() { // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - tracing::debug!("listening on {}", addr); + tracing::info!("listening on {}", address); - axum::Server::bind(&addr) + axum::Server::bind(&address.parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); From eea90a5c196555504a8a22bf01fb222ebf4c3e83 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 11 Sep 2023 12:51:17 +0100 Subject: [PATCH 023/249] Use a big ARM builder for docker (#22) --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 76d3ef09d2d1b..98160d19edce4 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -11,7 +11,7 @@ permissions: jobs: build: name: build and publish capture image - runs-on: ubuntu-latest + runs-on: buildjet-8vcpu-ubuntu-2204-arm steps: - name: Check Out Repo From 565d096cf7b1629f6be3d0bc05faccae347babc0 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 13 Sep 2023 16:08:51 +0200 Subject: [PATCH 024/249] Capture several fields to the ProcessedEvent output (#15) --- Cargo.lock | 28 ++++++- Cargo.toml | 7 +- src/api.rs | 3 +- src/capture.rs | 167 ++++++++++++++++++++++++----------------- src/event.rs | 70 +++++++++++++---- src/lib.rs | 1 + src/main.rs | 4 +- src/utils.rs | 38 ++++++++++ tests/django_compat.rs | 12 ++- 9 files changed, 239 insertions(+), 91 deletions(-) create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index d8e3ef016a0a8..91d5f1148bb3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,16 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-trait" version = "0.1.73" @@ -81,6 +91,17 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-client-ip" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8e81eacc93f36480825da5f46a33b5fb2246ed024eacc9e8933425b80c5807" +dependencies = [ + "axum", + "forwarded-header-value", + "serde", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -166,14 +187,17 @@ name = "capture" version = "0.1.0" dependencies = [ "anyhow", + "assert-json-diff", "async-trait", "axum", + "axum-client-ip", "axum-test-helper", "base64", "bytes", "flate2", "governor", "mockall", + "rand", "rdkafka", "serde", "serde_json", @@ -247,6 +271,9 @@ name = "deranged" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", +] [[package]] name = "difflib" @@ -1610,7 +1637,6 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ - "getrandom", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 7514974a7e765..ffc4b7cf97321 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] axum = "0.6.15" +axum-client-ip = "0.4.1" tokio = { version = "1.0", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" @@ -14,17 +15,19 @@ serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" governor = "0.5.1" tower_governor = "0.0.4" -time = { version = "0.3.20", features = ["formatting"]} +time = { version = "0.3.20", features = ["formatting", "macros", "serde"] } tower-http = { version = "0.4.0", features = ["trace"] } bytes = "1" anyhow = "1.0" flate2 = "1.0" base64 = "0.21.1" -uuid = { version = "1.3.3", features = ["serde", "v4"] } +uuid = { version = "1.3.3", features = ["serde"] } async-trait = "0.1.68" serde_urlencoded = "0.7.1" +rand = "0.8.5" rdkafka = { version = "0.34", features = ["cmake-build"] } [dev-dependencies] +assert-json-diff = "2.0.2" axum-test-helper = "0.2.0" mockall = "0.11.2" diff --git a/src/api.rs b/src/api.rs index 09e79ae7b961b..51f8321a85836 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,7 +1,6 @@ -use std::collections::HashMap; - use serde::{Deserialize, Serialize}; use serde_json::Value; +use std::collections::HashMap; #[derive(Debug, Deserialize, Serialize)] pub struct CaptureRequest { diff --git a/src/capture.rs b/src/capture.rs index 541ef315cd895..b7653857b817f 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -2,27 +2,29 @@ use std::collections::HashSet; use std::ops::Deref; use std::sync::Arc; -use anyhow::Result; +use anyhow::{anyhow, Result}; use bytes::Bytes; use axum::{http::StatusCode, Json}; // TODO: stream this instead use axum::extract::{Query, State}; use axum::http::HeaderMap; +use axum_client_ip::InsecureClientIp; use base64::Engine; -use uuid::Uuid; - -use crate::api::CaptureResponseCode; -use crate::event::ProcessedEvent; +use time::OffsetDateTime; +use crate::event::ProcessingContext; +use crate::token::validate_token; use crate::{ - api::CaptureResponse, - event::{Event, EventFormData, EventQuery}, - router, sink, token, + api::{CaptureResponse, CaptureResponseCode}, + event::{EventFormData, EventQuery, ProcessedEvent, RawEvent}, + router, sink, + utils::uuid_v7, }; pub async fn event( state: State, + InsecureClientIp(ip): InsecureClientIp, meta: Query, headers: HeaderMap, body: Bytes, @@ -38,9 +40,9 @@ pub async fn event( let payload = base64::engine::general_purpose::STANDARD .decode(input.data) .unwrap(); - Event::from_bytes(&meta, payload.into()) + RawEvent::from_bytes(&meta, payload.into()) } - _ => Event::from_bytes(&meta, body), + _ => RawEvent::from_bytes(&meta, body), }; let events = match events { @@ -59,8 +61,30 @@ pub async fn event( if events.is_empty() { return Err((StatusCode::BAD_REQUEST, String::from("No events in batch"))); } + let token = match extract_and_verify_token(&events) { + Ok(token) => token, + Err(msg) => return Err((StatusCode::UNAUTHORIZED, msg)), + }; + + let sent_at = meta.sent_at.and_then(|value| { + let value_nanos: i128 = i128::from(value) * 1_000_000; // Assuming the value is in milliseconds, latest posthog-js releases + if let Ok(sent_at) = OffsetDateTime::from_unix_timestamp_nanos(value_nanos) { + if sent_at.year() > 2020 { + // Could be lower if the input is in seconds + return Some(sent_at); + } + } + None + }); + let context = ProcessingContext { + lib_version: meta.lib_version.clone(), + sent_at, + token, + now: state.timesource.current_time(), + client_ip: ip.to_string(), + }; - let processed = process_events(state.sink.clone(), &events).await; + let processed = process_events(state.sink.clone(), &events, &context).await; if let Err(msg) = processed { return Err((StatusCode::BAD_REQUEST, msg)); @@ -71,47 +95,61 @@ pub async fn event( })) } -pub fn process_single_event(_event: &Event) -> Result { - // TODO: Put actual data in here and transform it properly +pub fn process_single_event( + event: &RawEvent, + context: &ProcessingContext, +) -> Result { + let distinct_id = match &event.distinct_id { + Some(id) => id, + None => match event.properties.get("distinct_id").map(|v| v.as_str()) { + Some(Some(id)) => id, + _ => return Err(anyhow!("missing distinct_id")), + }, + }; + Ok(ProcessedEvent { - uuid: Uuid::new_v4(), - distinct_id: Uuid::new_v4().simple().to_string(), - ip: String::new(), + uuid: event.uuid.unwrap_or_else(uuid_v7), + distinct_id: distinct_id.to_string(), + ip: context.client_ip.clone(), site_url: String::new(), data: String::from("hallo I am some data 😊"), - now: String::new(), - sent_at: String::new(), - token: String::from("tokentokentoken"), + now: context.now.clone(), + sent_at: context.sent_at, + token: context.token.clone(), }) } +pub fn extract_and_verify_token(events: &[RawEvent]) -> Result { + let distinct_tokens: HashSet> = HashSet::from_iter( + events + .iter() + .map(RawEvent::extract_token) + .filter(Option::is_some), + ); + + return match distinct_tokens.len() { + 0 => Err(String::from("no token found in request")), + 1 => match distinct_tokens.iter().last() { + Some(Some(token)) => { + validate_token(token).map_err(|err| String::from(err.reason()))?; + Ok(token.clone()) + } + _ => Err(String::from("no token found in request")), + }, + _ => Err(String::from("number of distinct tokens in batch > 1")), + }; +} + pub async fn process_events( sink: Arc, - events: &[Event], + events: &[RawEvent], + context: &ProcessingContext, ) -> Result<(), String> { - let mut distinct_tokens = HashSet::new(); - - // 1. Tokens are all valid - for event in events { - let token = event.token.clone().unwrap_or_else(|| { - event - .properties - .get("token") - .map_or(String::new(), |t| String::from(t.as_str().unwrap())) - }); - - if let Err(invalid) = token::validate_token(token.as_str()) { - return Err(invalid.reason().to_string()); - } - - distinct_tokens.insert(token); - } - - if distinct_tokens.len() > 1 { - return Err(String::from("Number of distinct tokens in batch > 1")); - } - - let events: Vec = match events.iter().map(process_single_event).collect() { + let events: Vec = match events + .iter() + .map(|e| process_single_event(e, context)) + .collect() + { Err(_) => return Err(String::from("Failed to process all events")), Ok(events) => events, }; @@ -139,61 +177,54 @@ pub async fn process_events( #[cfg(test)] mod tests { - use crate::sink; - use std::collections::HashMap; - use std::sync::Arc; - + use crate::capture::extract_and_verify_token; + use crate::event::RawEvent; use serde_json::json; - - use super::process_events; - use crate::event::Event; - use crate::router::State; + use std::collections::HashMap; #[tokio::test] async fn all_events_have_same_token() { - let state = State { - sink: Arc::new(sink::PrintSink {}), - timesource: Arc::new(crate::time::SystemTime {}), - }; - let events = vec![ - Event { + RawEvent { token: Some(String::from("hello")), + distinct_id: Some("testing".to_string()), + uuid: None, event: String::new(), properties: HashMap::new(), }, - Event { + RawEvent { token: None, + distinct_id: Some("testing".to_string()), + uuid: None, event: String::new(), properties: HashMap::from([(String::from("token"), json!("hello"))]), }, ]; - let processed = process_events(state.sink, &events).await; - assert_eq!(processed.is_ok(), true); + let processed = extract_and_verify_token(&events); + assert_eq!(processed.is_ok(), true, "{:?}", processed); } #[tokio::test] async fn all_events_have_different_token() { - let state = State { - sink: Arc::new(sink::PrintSink {}), - timesource: Arc::new(crate::time::SystemTime {}), - }; - let events = vec![ - Event { + RawEvent { token: Some(String::from("hello")), + distinct_id: Some("testing".to_string()), + uuid: None, event: String::new(), properties: HashMap::new(), }, - Event { + RawEvent { token: None, + distinct_id: Some("testing".to_string()), + uuid: None, event: String::new(), properties: HashMap::from([(String::from("token"), json!("goodbye"))]), }, ]; - let processed = process_events(state.sink, &events).await; + let processed = extract_and_verify_token(&events); assert_eq!(processed.is_err(), true); } } diff --git a/src/event.rs b/src/event.rs index 8cac0ecf8a1ab..32c1b5216ca68 100644 --- a/src/event.rs +++ b/src/event.rs @@ -7,6 +7,7 @@ use serde_json::Value; use anyhow::{anyhow, Result}; use bytes::{Buf, Bytes}; use flate2::read::GzDecoder; +use time::OffsetDateTime; use uuid::Uuid; #[derive(Deserialize, Default)] @@ -16,16 +17,24 @@ pub enum Compression { GzipJs, } -#[allow(dead_code)] // until they are used #[derive(Deserialize, Default)] pub struct EventQuery { - compression: Option, + pub compression: Option, #[serde(alias = "ver")] - version: Option, + pub lib_version: Option, #[serde(alias = "_")] - sent_at: Option, + pub sent_at: Option, + + #[serde(skip_serializing)] + pub token: Option, // Filled by handler + + #[serde(skip_serializing)] + pub now: Option, // Filled by handler from timesource + + #[serde(skip_serializing)] + pub client_ip: Option, // Filled by handler } #[derive(Debug, Deserialize)] @@ -34,19 +43,20 @@ pub struct EventFormData { } #[derive(Default, Debug, Deserialize, Serialize)] -pub struct Event { +pub struct RawEvent { #[serde(alias = "$token", alias = "api_key")] pub token: Option, - + pub distinct_id: Option, + pub uuid: Option, pub event: String, pub properties: HashMap, } -impl Event { +impl RawEvent { /// We post up _at least one_ event, so when decompressiong and deserializing there /// could be more than one. Hence this function has to return a Vec. /// TODO: Use an axum extractor for this - pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result> { + pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result> { tracing::debug!(len = bytes.len(), "decoding new event"); let payload = match query.compression { @@ -60,17 +70,43 @@ impl Event { }; tracing::debug!(json = payload, "decoded event data"); - if let Ok(events) = serde_json::from_str::>(&payload) { + if let Ok(events) = serde_json::from_str::>(&payload) { return Ok(events); } - if let Ok(events) = serde_json::from_str::(&payload) { + if let Ok(events) = serde_json::from_str::(&payload) { return Ok(vec![events]); } Err(anyhow!("unknown input shape")) } + + pub fn extract_token(&self) -> Option { + match &self.token { + Some(value) => Some(value.clone()), + None => self + .properties + .get("token") + .and_then(Value::as_str) + .map(String::from), + } + } +} + +pub struct ProcessingContext { + pub lib_version: Option, + pub sent_at: Option, + pub token: String, + pub now: String, + pub client_ip: String, } -#[derive(Clone, Default, Debug, Deserialize, Serialize)] +time::serde::format_description!( + django_iso, + OffsetDateTime, + "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6][offset_hour \ + sign:mandatory]:[offset_minute]" +); + +#[derive(Clone, Default, Debug, Serialize)] pub struct ProcessedEvent { pub uuid: Uuid, pub distinct_id: String, @@ -78,7 +114,8 @@ pub struct ProcessedEvent { pub site_url: String, pub data: String, pub now: String, - pub sent_at: String, + #[serde(with = "django_iso::option")] + pub sent_at: Option, pub token: String, } @@ -94,7 +131,7 @@ mod tests { use base64::Engine as _; use bytes::Bytes; - use super::{Event, EventQuery}; + use super::{EventQuery, RawEvent}; #[test] fn decode_bytes() { @@ -104,11 +141,14 @@ mod tests { .unwrap(); let bytes = Bytes::from(decoded_horrible_blob); - let events = Event::from_bytes( + let events = RawEvent::from_bytes( &EventQuery { compression: Some(Compression::GzipJs), - version: None, + lib_version: None, sent_at: None, + token: None, + now: None, + client_ip: None, }, bytes, ); diff --git a/src/lib.rs b/src/lib.rs index 641385f93d553..96e98755dee98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub mod router; pub mod sink; pub mod time; mod token; +mod utils; diff --git a/src/main.rs b/src/main.rs index 3e6acde32af15..b7e549fca2da4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use std::env; +use std::net::SocketAddr; use crate::time::SystemTime; @@ -9,6 +10,7 @@ mod router; mod sink; mod time; mod token; +mod utils; #[tokio::main] async fn main() { @@ -35,7 +37,7 @@ async fn main() { tracing::info!("listening on {}", address); axum::Server::bind(&address.parse().unwrap()) - .serve(app.into_make_service()) + .serve(app.into_make_service_with_connect_info::()) .await .unwrap(); } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000000000..c5a95fbe2481f --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,38 @@ +use rand::RngCore; +use uuid::Uuid; + +pub fn random_bytes() -> [u8; N] { + let mut ret = [0u8; N]; + rand::thread_rng().fill_bytes(&mut ret); + ret +} + +// basically just ripped from the uuid crate. they have it as unstable, but we can use it fine. +const fn encode_unix_timestamp_millis(millis: u64, random_bytes: &[u8; 10]) -> Uuid { + let millis_high = ((millis >> 16) & 0xFFFF_FFFF) as u32; + let millis_low = (millis & 0xFFFF) as u16; + + let random_and_version = + (random_bytes[0] as u16 | ((random_bytes[1] as u16) << 8) & 0x0FFF) | (0x7 << 12); + + let mut d4 = [0; 8]; + + d4[0] = (random_bytes[2] & 0x3F) | 0x80; + d4[1] = random_bytes[3]; + d4[2] = random_bytes[4]; + d4[3] = random_bytes[5]; + d4[4] = random_bytes[6]; + d4[5] = random_bytes[7]; + d4[6] = random_bytes[8]; + d4[7] = random_bytes[9]; + + Uuid::from_fields(millis_high, millis_low, random_and_version, &d4) +} + +pub fn uuid_v7() -> Uuid { + let bytes = random_bytes(); + let now = time::OffsetDateTime::now_utc(); + let now_millis: u64 = now.unix_timestamp() as u64 * 1_000 + now.millisecond() as u64; + + encode_unix_timestamp_millis(now_millis, &bytes) +} diff --git a/tests/django_compat.rs b/tests/django_compat.rs index c8ac9a4ccd252..29e344aa7bc62 100644 --- a/tests/django_compat.rs +++ b/tests/django_compat.rs @@ -1,3 +1,4 @@ +use assert_json_diff::assert_json_eq; use async_trait::async_trait; use axum::http::StatusCode; use axum_test_helper::TestClient; @@ -9,6 +10,7 @@ use capture::router::router; use capture::sink::EventSink; use capture::time::TimeSource; use serde::Deserialize; +use serde_json::{json, Value}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::sync::{Arc, Mutex}; @@ -22,7 +24,7 @@ struct RequestDump { ip: String, now: String, body: String, - output: Vec, + output: Vec, } static REQUESTS_DUMP_FILE_NAME: &str = "tests/requests_dump.jsonl"; @@ -47,6 +49,10 @@ impl MemorySink { fn len(&self) -> usize { self.events.lock().unwrap().len() } + + fn events(&self) -> Vec { + self.events.lock().unwrap().clone() + } } #[async_trait] @@ -63,6 +69,7 @@ impl EventSink for MemorySink { } #[tokio::test] +#[ignore] async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let file = File::open(REQUESTS_DUMP_FILE_NAME)?; let reader = BufReader::new(file); @@ -104,7 +111,8 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { }), res.json().await ); - assert_eq!(sink.len(), case.output.len()) + assert_eq!(sink.len(), case.output.len()); + assert_json_eq!(json!(case.output), json!(sink.events())) } Ok(()) } From 8eb6a2ac8911c2f5f65066436b002e948d41e5fa Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 14 Sep 2023 13:51:41 +0100 Subject: [PATCH 025/249] Initial workspace setup (#25) * Initial workspace setup I'd like to tidy a few things (dependencies etc), but get this setup asap to avoid awkward rebases going forwards. * Format and delete --- Cargo.lock | 11 +++++++ Cargo.toml | 18 ++++------- bin/send_event.sh | 2 -- capture-server/Cargo.toml | 11 +++++++ {src => capture-server/src}/main.rs | 17 +++------- capture/Cargo.toml | 33 ++++++++++++++++++++ {src => capture/src}/api.rs | 0 {src => capture/src}/capture.rs | 6 ++-- {src => capture/src}/event.rs | 0 {src => capture/src}/lib.rs | 6 ++-- {src => capture/src}/router.rs | 0 {src => capture/src}/sink.rs | 0 {src => capture/src}/time.rs | 0 {src => capture/src}/token.rs | 0 {src => capture/src}/utils.rs | 0 {tests => capture/tests}/django_compat.rs | 0 {tests => capture/tests}/requests_dump.jsonl | 0 17 files changed, 71 insertions(+), 33 deletions(-) delete mode 100755 bin/send_event.sh create mode 100644 capture-server/Cargo.toml rename {src => capture-server/src}/main.rs (79%) create mode 100644 capture/Cargo.toml rename {src => capture/src}/api.rs (100%) rename {src => capture/src}/capture.rs (98%) rename {src => capture/src}/event.rs (100%) rename {src => capture/src}/lib.rs (60%) rename {src => capture/src}/router.rs (100%) rename {src => capture/src}/sink.rs (100%) rename {src => capture/src}/time.rs (100%) rename {src => capture/src}/token.rs (100%) rename {src => capture/src}/utils.rs (100%) rename {tests => capture/tests}/django_compat.rs (100%) rename {tests => capture/tests}/requests_dump.jsonl (100%) diff --git a/Cargo.lock b/Cargo.lock index 91d5f1148bb3a..8ae24941c7f91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "capture-server" +version = "0.1.0" +dependencies = [ + "axum", + "capture", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "cc" version = "1.0.83" diff --git a/Cargo.toml b/Cargo.toml index ffc4b7cf97321..0d56eddfd0815 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,10 @@ -[package] -name = "capture" -version = "0.1.0" -edition = "2021" +[workspace] +members = [ + "capture", + "capture-server" +] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] +[workspace.dependencies] axum = "0.6.15" axum-client-ip = "0.4.1" tokio = { version = "1.0", features = ["full"] } @@ -26,8 +25,3 @@ async-trait = "0.1.68" serde_urlencoded = "0.7.1" rand = "0.8.5" rdkafka = { version = "0.34", features = ["cmake-build"] } - -[dev-dependencies] -assert-json-diff = "2.0.2" -axum-test-helper = "0.2.0" -mockall = "0.11.2" diff --git a/bin/send_event.sh b/bin/send_event.sh deleted file mode 100755 index 4885cf12db376..0000000000000 --- a/bin/send_event.sh +++ /dev/null @@ -1,2 +0,0 @@ -# Send an event to a test server -curl http://localhost:3000/capture -X POST -H "Content-Type: application/json" --data '{"token": "ferrisisbae"}' diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml new file mode 100644 index 0000000000000..04c618286c4cb --- /dev/null +++ b/capture-server/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "capture-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +capture = { path = "../capture" } +axum = { workspace = true } +tokio = { workspace = true } +tracing-subscriber = { workspace = true } +tracing = { workspace = true } diff --git a/src/main.rs b/capture-server/src/main.rs similarity index 79% rename from src/main.rs rename to capture-server/src/main.rs index b7e549fca2da4..c2f22bcb1ac37 100644 --- a/src/main.rs +++ b/capture-server/src/main.rs @@ -1,16 +1,7 @@ use std::env; use std::net::SocketAddr; -use crate::time::SystemTime; - -mod api; -mod capture; -mod event; -mod router; -mod sink; -mod time; -mod token; -mod utils; +use capture::{router, sink, time}; #[tokio::main] async fn main() { @@ -18,19 +9,19 @@ async fn main() { let address = env::var("ADDRESS").unwrap_or(String::from("127.0.0.1:3000")); let app = if use_print_sink { - router::router(SystemTime {}, sink::PrintSink {}) + router::router(time::SystemTime {}, sink::PrintSink {}) } else { let brokers = env::var("KAFKA_BROKERS").expect("Expected KAFKA_BROKERS"); let topic = env::var("KAFKA_TOPIC").expect("Expected KAFKA_TOPIC"); let sink = sink::KafkaSink::new(topic, brokers).unwrap(); - router::router(SystemTime {}, sink) + router::router(time::SystemTime {}, sink) }; // initialize tracing - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt::init(); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/capture/Cargo.toml b/capture/Cargo.toml new file mode 100644 index 0000000000000..61ccceaeea46a --- /dev/null +++ b/capture/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "capture" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = { workspace = true } +axum-client-ip = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +governor = { workspace = true } +tower_governor = { workspace = true } +time = { workspace = true } +tower-http = { workspace = true } +bytes = { workspace = true } +anyhow = { workspace = true } +flate2 = { workspace = true } +base64 = { workspace = true } +uuid = { workspace = true } +async-trait = { workspace = true } +serde_urlencoded = { workspace = true } +rand = { workspace = true } +rdkafka = { workspace = true } + +[dev-dependencies] +assert-json-diff = "2.0.2" +axum-test-helper = "0.2.0" +mockall = "0.11.2" diff --git a/src/api.rs b/capture/src/api.rs similarity index 100% rename from src/api.rs rename to capture/src/api.rs diff --git a/src/capture.rs b/capture/src/capture.rs similarity index 98% rename from src/capture.rs rename to capture/src/capture.rs index b7653857b817f..d54cf0847c219 100644 --- a/src/capture.rs +++ b/capture/src/capture.rs @@ -140,10 +140,10 @@ pub fn extract_and_verify_token(events: &[RawEvent]) -> Result { }; } -pub async fn process_events( +pub async fn process_events<'a>( sink: Arc, - events: &[RawEvent], - context: &ProcessingContext, + events: &'a [RawEvent], + context: &'a ProcessingContext, ) -> Result<(), String> { let events: Vec = match events .iter() diff --git a/src/event.rs b/capture/src/event.rs similarity index 100% rename from src/event.rs rename to capture/src/event.rs diff --git a/src/lib.rs b/capture/src/lib.rs similarity index 60% rename from src/lib.rs rename to capture/src/lib.rs index 96e98755dee98..0f0b269bdb3f9 100644 --- a/src/lib.rs +++ b/capture/src/lib.rs @@ -1,8 +1,8 @@ pub mod api; -mod capture; +pub mod capture; pub mod event; pub mod router; pub mod sink; pub mod time; -mod token; -mod utils; +pub mod token; +pub mod utils; diff --git a/src/router.rs b/capture/src/router.rs similarity index 100% rename from src/router.rs rename to capture/src/router.rs diff --git a/src/sink.rs b/capture/src/sink.rs similarity index 100% rename from src/sink.rs rename to capture/src/sink.rs diff --git a/src/time.rs b/capture/src/time.rs similarity index 100% rename from src/time.rs rename to capture/src/time.rs diff --git a/src/token.rs b/capture/src/token.rs similarity index 100% rename from src/token.rs rename to capture/src/token.rs diff --git a/src/utils.rs b/capture/src/utils.rs similarity index 100% rename from src/utils.rs rename to capture/src/utils.rs diff --git a/tests/django_compat.rs b/capture/tests/django_compat.rs similarity index 100% rename from tests/django_compat.rs rename to capture/tests/django_compat.rs diff --git a/tests/requests_dump.jsonl b/capture/tests/requests_dump.jsonl similarity index 100% rename from tests/requests_dump.jsonl rename to capture/tests/requests_dump.jsonl From 3390610719bba9c1e3bc46e9ea3016a0b264424e Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 14 Sep 2023 14:07:41 +0100 Subject: [PATCH 026/249] Add Prometheus metrics exporting (#23) * Add metrics middleware * Organize a bit better * Track a couple more things * Do not install metrics in tests * Add some lifetimes and fixes * Run formatter * Simplify routes * Fixes and feedback * Patch builds --- .github/workflows/rust.yml | 2 +- Cargo.lock | 138 ++++++++++++++++++++++++++++++++- Cargo.toml | 2 + Dockerfile | 2 +- capture-server/src/main.rs | 4 +- capture/Cargo.toml | 2 + capture/src/lib.rs | 1 + capture/src/prometheus.rs | 55 +++++++++++++ capture/src/router.rs | 30 ++++--- capture/src/sink.rs | 11 ++- capture/tests/django_compat.rs | 2 +- 11 files changed, 232 insertions(+), 17 deletions(-) create mode 100644 capture/src/prometheus.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dbb689dc8e72a..478017af701f2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,7 +30,7 @@ jobs: key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} - name: Run cargo build - run: cargo build --all --locked --release && strip target/release/capture + run: cargo build --all --locked --release && strip target/release/capture-server test: runs-on: buildjet-4vcpu-ubuntu-2204 diff --git a/Cargo.lock b/Cargo.lock index 8ae24941c7f91..0f4cd76bceb4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -196,6 +207,8 @@ dependencies = [ "bytes", "flate2", "governor", + "metrics", + "metrics-exporter-prometheus", "mockall", "rand", "rdkafka", @@ -255,6 +268,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -494,7 +520,7 @@ dependencies = [ "no-std-compat", "nonzero_ext", "parking_lot", - "quanta", + "quanta 0.9.3", "rand", "smallvec", ] @@ -524,6 +550,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.0" @@ -709,6 +744,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + [[package]] name = "matchit" version = "0.7.2" @@ -721,6 +765,70 @@ version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metrics" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5" +dependencies = [ + "ahash", + "metrics-macros", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" +dependencies = [ + "base64", + "hyper", + "indexmap 1.9.3", + "ipnet", + "metrics", + "metrics-util", + "quanta 0.11.1", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.31", +] + +[[package]] +name = "metrics-util" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de2ed6e491ed114b40b732e4d1659a9d53992ebd87490c44a6ffe23739d973e" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.13.1", + "metrics", + "num_cpus", + "quanta 0.11.1", + "sketches-ddsketch", +] + [[package]] name = "mime" version = "0.3.17" @@ -946,6 +1054,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "portable-atomic" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1017,6 +1131,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.33" @@ -1266,6 +1396,12 @@ dependencies = [ "libc", ] +[[package]] +name = "sketches-ddsketch" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 0d56eddfd0815..59ffb47dad7a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,5 @@ async-trait = "0.1.68" serde_urlencoded = "0.7.1" rand = "0.8.5" rdkafka = { version = "0.34", features = ["cmake-build"] } +metrics = "0.21.1" +metrics-exporter-prometheus = "0.12.1" diff --git a/Dockerfile b/Dockerfile index c44e53be54fc8..cb158e22abcc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release --bin capture +RUN cargo build --release --bin capture-server FROM debian:bullseye-20230320-slim AS runtime diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index c2f22bcb1ac37..88ffc949eea7a 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -9,14 +9,14 @@ async fn main() { let address = env::var("ADDRESS").unwrap_or(String::from("127.0.0.1:3000")); let app = if use_print_sink { - router::router(time::SystemTime {}, sink::PrintSink {}) + router::router(time::SystemTime {}, sink::PrintSink {}, true) } else { let brokers = env::var("KAFKA_BROKERS").expect("Expected KAFKA_BROKERS"); let topic = env::var("KAFKA_TOPIC").expect("Expected KAFKA_TOPIC"); let sink = sink::KafkaSink::new(topic, brokers).unwrap(); - router::router(time::SystemTime {}, sink) + router::router(time::SystemTime {}, sink, true) }; // initialize tracing diff --git a/capture/Cargo.toml b/capture/Cargo.toml index 61ccceaeea46a..c58bf67ba2340 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -26,6 +26,8 @@ async-trait = { workspace = true } serde_urlencoded = { workspace = true } rand = { workspace = true } rdkafka = { workspace = true } +metrics = { workspace = true } +metrics-exporter-prometheus = { workspace = true } [dev-dependencies] assert-json-diff = "2.0.2" diff --git a/capture/src/lib.rs b/capture/src/lib.rs index 0f0b269bdb3f9..d4ca041e5e671 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -1,6 +1,7 @@ pub mod api; pub mod capture; pub mod event; +pub mod prometheus; pub mod router; pub mod sink; pub mod time; diff --git a/capture/src/prometheus.rs b/capture/src/prometheus.rs new file mode 100644 index 0000000000000..1fcdb7d7ca30b --- /dev/null +++ b/capture/src/prometheus.rs @@ -0,0 +1,55 @@ +// Middleware + prometheus exporter setup + +use std::time::Instant; + +use axum::{extract::MatchedPath, http::Request, middleware::Next, response::IntoResponse}; +use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; + +pub fn setup_metrics_recorder() -> PrometheusHandle { + // Ok I broke it at the end, but the limit on our ingress is 60 and that's a nicer way of reaching it + const EXPONENTIAL_SECONDS: &[f64] = &[ + 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, + ]; + + PrometheusBuilder::new() + .set_buckets_for_metric( + Matcher::Full("http_requests_duration_seconds".to_string()), + EXPONENTIAL_SECONDS, + ) + .unwrap() + .install_recorder() + .unwrap() +} + +/// Middleware to record some common HTTP metrics +/// Generic over B to allow for arbitrary body types (eg Vec, Streams, a deserialized thing, etc) +/// Someday tower-http might provide a metrics middleware: https://github.com/tower-rs/tower-http/issues/57 +pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse { + let start = Instant::now(); + + let path = if let Some(matched_path) = req.extensions().get::() { + matched_path.as_str().to_owned() + } else { + req.uri().path().to_owned() + }; + + let method = req.method().clone(); + + // Run the rest of the request handling first, so we can measure it and get response + // codes. + let response = next.run(req).await; + + let latency = start.elapsed().as_secs_f64(); + let status = response.status().as_u16().to_string(); + + let labels = [ + ("method", method.to_string()), + ("path", path), + ("status", status), + ]; + + metrics::increment_counter!("http_requests_total", &labels); + metrics::histogram!("http_requests_duration_seconds", latency, &labels); + + response +} diff --git a/capture/src/router.rs b/capture/src/router.rs index 199cd2095e0a0..0c40658c04647 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -1,3 +1,4 @@ +use std::future::ready; use std::sync::Arc; use axum::{ @@ -8,6 +9,8 @@ use tower_http::trace::TraceLayer; use crate::{capture, sink, time::TimeSource}; +use crate::prometheus::{setup_metrics_recorder, track_metrics}; + #[derive(Clone)] pub struct State { pub sink: Arc, @@ -24,23 +27,30 @@ pub fn router< >( timesource: TZ, sink: S, + metrics: bool, ) -> Router { let state = State { sink: Arc::new(sink), timesource: Arc::new(timesource), }; - Router::new() + let router = Router::new() // TODO: use NormalizePathLayer::trim_trailing_slash .route("/", get(index)) - .route("/capture", post(capture::event)) - .route("/capture/", post(capture::event)) - .route("/batch", post(capture::event)) - .route("/batch/", post(capture::event)) - .route("/e", post(capture::event)) - .route("/e/", post(capture::event)) - .route("/engage", post(capture::event)) - .route("/engage/", post(capture::event)) + .route("/i/v0/e", post(capture::event)) + .route("/i/v0/e/", post(capture::event)) .layer(TraceLayer::new_for_http()) - .with_state(state) + .layer(axum::middleware::from_fn(track_metrics)) + .with_state(state); + + // Don't install metrics unless asked to + // Installing a global recorder when capture is used as a library (during tests etc) + // does not work well. + if metrics { + let recorder_handle = setup_metrics_recorder(); + + router.route("/metrics", get(move || ready(recorder_handle.render()))) + } else { + router + } } diff --git a/capture/src/sink.rs b/capture/src/sink.rs index 20ab3fc70a429..96a6be01cdb72 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -20,6 +20,8 @@ impl EventSink for PrintSink { async fn send(&self, event: ProcessedEvent) -> Result<()> { tracing::info!("single event: {:?}", event); + metrics::increment_counter!("capture_events_total"); + Ok(()) } async fn send_batch(&self, events: Vec) -> Result<()> { @@ -27,6 +29,7 @@ impl EventSink for PrintSink { let _enter = span.enter(); for event in events { + metrics::increment_counter!("capture_events_total"); tracing::info!("event: {:?}", event); } @@ -68,10 +71,16 @@ impl KafkaSink { timestamp: None, headers: None, }) { - Ok(_) => {} + Ok(_) => { + metrics::increment_counter!("capture_events_total"); + } Err(e) => { tracing::error!("failed to produce event: {}", e.0); + // TODO(maybe someday): Don't drop them but write them somewhere and try again + // later? + metrics::increment_counter!("capture_events_dropped"); + // TODO: Improve error handling return Err(anyhow!("failed to produce event {}", e.0)); } diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index 29e344aa7bc62..219a8c7fe691d 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -90,7 +90,7 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let sink = MemorySink::default(); let timesource = FixedTime { time: case.now }; - let app = router(timesource, sink.clone()); + let app = router(timesource, sink.clone(), false); let client = TestClient::new(app); let mut req = client.post(&case.path).body(raw_body); From 76905ecb266a63849e8bf1fe717b51fcac827778 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 14 Sep 2023 15:27:01 +0200 Subject: [PATCH 027/249] implement error handling with a CaptureError enum (#24) --- Cargo.lock | 1 + Cargo.toml | 1 + capture/Cargo.toml | 1 + capture/src/api.rs | 49 ++++++++++++++++++++++ capture/src/capture.rs | 74 +++++++++------------------------- capture/src/event.rs | 41 +++++++++++++------ capture/src/sink.rs | 51 ++++++++++++----------- capture/tests/django_compat.rs | 8 ++-- 8 files changed, 132 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f4cd76bceb4a..6ef5c6c66b662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,6 +215,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "thiserror", "time", "tokio", "tower-http", diff --git a/Cargo.toml b/Cargo.toml index 59ffb47dad7a6..68569a3ad2f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,4 @@ rand = "0.8.5" rdkafka = { version = "0.34", features = ["cmake-build"] } metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" +thiserror = "1.0.48" diff --git a/capture/Cargo.toml b/capture/Cargo.toml index c58bf67ba2340..60cca70824cff 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -28,6 +28,7 @@ rand = { workspace = true } rdkafka = { workspace = true } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] assert-json-diff = "2.0.2" diff --git a/capture/src/api.rs b/capture/src/api.rs index 51f8321a85836..9a18a89c4c71c 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -1,6 +1,10 @@ +use crate::token::InvalidTokenReason; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use thiserror::Error; #[derive(Debug, Deserialize, Serialize)] pub struct CaptureRequest { @@ -20,3 +24,48 @@ pub enum CaptureResponseCode { pub struct CaptureResponse { pub status: CaptureResponseCode, } + +#[derive(Error, Debug)] +pub enum CaptureError { + #[error("failed to decode request: {0}")] + RequestDecodingError(String), + #[error("failed to decode request: {0}")] + RequestParsingError(#[from] serde_json::Error), + + #[error("request holds no event")] + EmptyBatch, + #[error("event submitted without a distinct_id")] + MissingDistinctId, + + #[error("event submitted without an api_key")] + NoTokenError, + #[error("batch submitted with inconsistent api_key values")] + MultipleTokensError, + #[error("API key is not valid: {0}")] + TokenValidationError(#[from] InvalidTokenReason), + + #[error("transient error, please retry")] + RetryableSinkError, + #[error("maximum event size exceeded")] + EventTooBig, + #[error("invalid event could not be processed")] + NonRetryableSinkError, +} + +impl IntoResponse for CaptureError { + fn into_response(self) -> Response { + match self { + CaptureError::RequestDecodingError(_) + | CaptureError::RequestParsingError(_) + | CaptureError::EmptyBatch + | CaptureError::MissingDistinctId + | CaptureError::EventTooBig + | CaptureError::NonRetryableSinkError => (StatusCode::BAD_REQUEST, self.to_string()), + CaptureError::NoTokenError + | CaptureError::MultipleTokensError + | CaptureError::TokenValidationError(_) => (StatusCode::UNAUTHORIZED, self.to_string()), + CaptureError::RetryableSinkError => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()), + } + .into_response() + } +} diff --git a/capture/src/capture.rs b/capture/src/capture.rs index d54cf0847c219..e60421759a062 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -2,10 +2,9 @@ use std::collections::HashSet; use std::ops::Deref; use std::sync::Arc; -use anyhow::{anyhow, Result}; use bytes::Bytes; -use axum::{http::StatusCode, Json}; +use axum::Json; // TODO: stream this instead use axum::extract::{Query, State}; use axum::http::HeaderMap; @@ -16,7 +15,7 @@ use time::OffsetDateTime; use crate::event::ProcessingContext; use crate::token::validate_token; use crate::{ - api::{CaptureResponse, CaptureResponseCode}, + api::{CaptureError, CaptureResponse, CaptureResponseCode}, event::{EventFormData, EventQuery, ProcessedEvent, RawEvent}, router, sink, utils::uuid_v7, @@ -28,7 +27,7 @@ pub async fn event( meta: Query, headers: HeaderMap, body: Bytes, -) -> Result, (StatusCode, String)> { +) -> Result, CaptureError> { tracing::debug!(len = body.len(), "new event request"); let events = match headers @@ -43,28 +42,14 @@ pub async fn event( RawEvent::from_bytes(&meta, payload.into()) } _ => RawEvent::from_bytes(&meta, body), - }; - - let events = match events { - Ok(events) => events, - Err(e) => { - tracing::error!("failed to decode event: {:?}", e); - return Err(( - StatusCode::BAD_REQUEST, - String::from("Failed to decode event"), - )); - } - }; + }?; println!("Got events {:?}", &events); if events.is_empty() { - return Err((StatusCode::BAD_REQUEST, String::from("No events in batch"))); + return Err(CaptureError::EmptyBatch); } - let token = match extract_and_verify_token(&events) { - Ok(token) => token, - Err(msg) => return Err((StatusCode::UNAUTHORIZED, msg)), - }; + let token = extract_and_verify_token(&events)?; let sent_at = meta.sent_at.and_then(|value| { let value_nanos: i128 = i128::from(value) * 1_000_000; // Assuming the value is in milliseconds, latest posthog-js releases @@ -84,11 +69,7 @@ pub async fn event( client_ip: ip.to_string(), }; - let processed = process_events(state.sink.clone(), &events, &context).await; - - if let Err(msg) = processed { - return Err((StatusCode::BAD_REQUEST, msg)); - } + process_events(state.sink.clone(), &events, &context).await?; Ok(Json(CaptureResponse { status: CaptureResponseCode::Ok, @@ -98,12 +79,12 @@ pub async fn event( pub fn process_single_event( event: &RawEvent, context: &ProcessingContext, -) -> Result { +) -> Result { let distinct_id = match &event.distinct_id { Some(id) => id, None => match event.properties.get("distinct_id").map(|v| v.as_str()) { Some(Some(id)) => id, - _ => return Err(anyhow!("missing distinct_id")), + _ => return Err(CaptureError::MissingDistinctId), }, }; @@ -119,7 +100,7 @@ pub fn process_single_event( }) } -pub fn extract_and_verify_token(events: &[RawEvent]) -> Result { +pub fn extract_and_verify_token(events: &[RawEvent]) -> Result { let distinct_tokens: HashSet> = HashSet::from_iter( events .iter() @@ -128,15 +109,15 @@ pub fn extract_and_verify_token(events: &[RawEvent]) -> Result { ); return match distinct_tokens.len() { - 0 => Err(String::from("no token found in request")), + 0 => Err(CaptureError::NoTokenError), 1 => match distinct_tokens.iter().last() { Some(Some(token)) => { - validate_token(token).map_err(|err| String::from(err.reason()))?; + validate_token(token)?; Ok(token.clone()) } - _ => Err(String::from("no token found in request")), + _ => Err(CaptureError::NoTokenError), }, - _ => Err(String::from("number of distinct tokens in batch > 1")), + _ => Err(CaptureError::MultipleTokensError), }; } @@ -144,34 +125,17 @@ pub async fn process_events<'a>( sink: Arc, events: &'a [RawEvent], context: &'a ProcessingContext, -) -> Result<(), String> { - let events: Vec = match events +) -> Result<(), CaptureError> { + let events: Vec = events .iter() .map(|e| process_single_event(e, context)) - .collect() - { - Err(_) => return Err(String::from("Failed to process all events")), - Ok(events) => events, - }; + .collect::, CaptureError>>()?; if events.len() == 1 { - let sent = sink.send(events[0].clone()).await; - - if let Err(e) = sent { - tracing::error!("Failed to send event to sink: {:?}", e); - - return Err(String::from("Failed to send event to sink")); - } + sink.send(events[0].clone()).await?; } else { - let sent = sink.send_batch(events).await; - - if let Err(e) = sent { - tracing::error!("Failed to send batch events to sink: {:?}", e); - - return Err(String::from("Failed to send batch events to sink")); - } + sink.send_batch(events).await?; } - Ok(()) } diff --git a/capture/src/event.rs b/capture/src/event.rs index 32c1b5216ca68..fd109f2594ae4 100644 --- a/capture/src/event.rs +++ b/capture/src/event.rs @@ -4,7 +4,7 @@ use std::io::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::Value; -use anyhow::{anyhow, Result}; +use crate::api::CaptureError; use bytes::{Buf, Bytes}; use flate2::read::GzDecoder; use time::OffsetDateTime; @@ -52,31 +52,48 @@ pub struct RawEvent { pub properties: HashMap, } +#[derive(Deserialize)] +#[serde(untagged)] +enum RawRequest { + /// Batch of events + Batch(Vec), + /// Single event + One(RawEvent), +} + +impl RawRequest { + pub fn events(self) -> Vec { + match self { + RawRequest::Batch(events) => events, + RawRequest::One(event) => vec![event], + } + } +} + impl RawEvent { /// We post up _at least one_ event, so when decompressiong and deserializing there /// could be more than one. Hence this function has to return a Vec. /// TODO: Use an axum extractor for this - pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result> { + pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result, CaptureError> { tracing::debug!(len = bytes.len(), "decoding new event"); let payload = match query.compression { Some(Compression::GzipJs) => { let mut d = GzDecoder::new(bytes.reader()); let mut s = String::new(); - d.read_to_string(&mut s)?; + d.read_to_string(&mut s).map_err(|e| { + tracing::error!("failed to decode gzip: {}", e); + CaptureError::RequestDecodingError(String::from("invalid gzip data")) + })?; s } - None => String::from_utf8(bytes.into())?, + None => String::from_utf8(bytes.into()).map_err(|e| { + tracing::error!("failed to decode body: {}", e); + CaptureError::RequestDecodingError(String::from("invalid body encoding")) + })?, }; - tracing::debug!(json = payload, "decoded event data"); - if let Ok(events) = serde_json::from_str::>(&payload) { - return Ok(events); - } - if let Ok(events) = serde_json::from_str::(&payload) { - return Ok(vec![events]); - } - Err(anyhow!("unknown input shape")) + Ok(serde_json::from_str::(&payload)?.events()) } pub fn extract_token(&self) -> Option { diff --git a/capture/src/sink.rs b/capture/src/sink.rs index 96a6be01cdb72..e6f4b7bb4e8ed 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -1,30 +1,31 @@ -use anyhow::{anyhow, Result}; use async_trait::async_trait; use tokio::task::JoinSet; +use crate::api::CaptureError; use rdkafka::config::ClientConfig; +use rdkafka::error::RDKafkaErrorCode; use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; use crate::event::ProcessedEvent; #[async_trait] pub trait EventSink { - async fn send(&self, event: ProcessedEvent) -> Result<()>; - async fn send_batch(&self, events: Vec) -> Result<()>; + async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError>; + async fn send_batch(&self, events: Vec) -> Result<(), CaptureError>; } pub struct PrintSink {} #[async_trait] impl EventSink for PrintSink { - async fn send(&self, event: ProcessedEvent) -> Result<()> { + async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { tracing::info!("single event: {:?}", event); metrics::increment_counter!("capture_events_total"); Ok(()) } - async fn send_batch(&self, events: Vec) -> Result<()> { + async fn send_batch(&self, events: Vec) -> Result<(), CaptureError> { let span = tracing::span!(tracing::Level::INFO, "batch of events"); let _enter = span.enter(); @@ -44,7 +45,7 @@ pub struct KafkaSink { } impl KafkaSink { - pub fn new(topic: String, brokers: String) -> Result { + pub fn new(topic: String, brokers: String) -> anyhow::Result { let producer: FutureProducer = ClientConfig::new() .set("bootstrap.servers", &brokers) .create()?; @@ -58,8 +59,11 @@ impl KafkaSink { producer: FutureProducer, topic: String, event: ProcessedEvent, - ) -> Result<()> { - let payload = serde_json::to_string(&event)?; + ) -> Result<(), CaptureError> { + let payload = serde_json::to_string(&event).map_err(|e| { + tracing::error!("failed to serialize event: {}", e); + CaptureError::NonRetryableSinkError + })?; let key = event.key(); @@ -72,31 +76,32 @@ impl KafkaSink { headers: None, }) { Ok(_) => { - metrics::increment_counter!("capture_events_total"); - } - Err(e) => { - tracing::error!("failed to produce event: {}", e.0); - - // TODO(maybe someday): Don't drop them but write them somewhere and try again - // later? - metrics::increment_counter!("capture_events_dropped"); - - // TODO: Improve error handling - return Err(anyhow!("failed to produce event {}", e.0)); + metrics::increment_counter!("capture_events_ingested"); + Ok(()) } + Err((e, _)) => match e.rdkafka_error_code() { + Some(RDKafkaErrorCode::InvalidMessageSize) => { + metrics::increment_counter!("capture_events_dropped_too_big"); + Err(CaptureError::EventTooBig) + } + _ => { + // TODO(maybe someday): Don't drop them but write them somewhere and try again + metrics::increment_counter!("capture_events_dropped"); + tracing::error!("failed to produce event: {}", e); + Err(CaptureError::RetryableSinkError) + } + }, } - - Ok(()) } } #[async_trait] impl EventSink for KafkaSink { - async fn send(&self, event: ProcessedEvent) -> Result<()> { + async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { Self::kafka_send(self.producer.clone(), self.topic.clone(), event).await } - async fn send_batch(&self, events: Vec) -> Result<()> { + async fn send_batch(&self, events: Vec) -> Result<(), CaptureError> { let mut set = JoinSet::new(); for event in events { diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index 219a8c7fe691d..658ae137735b3 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -4,7 +4,7 @@ use axum::http::StatusCode; use axum_test_helper::TestClient; use base64::engine::general_purpose; use base64::Engine; -use capture::api::{CaptureResponse, CaptureResponseCode}; +use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode}; use capture::event::ProcessedEvent; use capture::router::router; use capture::sink::EventSink; @@ -57,12 +57,12 @@ impl MemorySink { #[async_trait] impl EventSink for MemorySink { - async fn send(&self, event: ProcessedEvent) -> anyhow::Result<()> { + async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { self.events.lock().unwrap().push(event); Ok(()) } - async fn send_batch(&self, events: Vec) -> anyhow::Result<()> { + async fn send_batch(&self, events: Vec) -> Result<(), CaptureError> { self.events.lock().unwrap().extend_from_slice(&events); Ok(()) } @@ -93,7 +93,7 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let app = router(timesource, sink.clone(), false); let client = TestClient::new(app); - let mut req = client.post(&case.path).body(raw_body); + let mut req = client.post("/i/v0/e/").body(raw_body); if !case.content_encoding.is_empty() { req = req.header("Content-encoding", case.content_encoding); } From 1474f999f6bb114921bbd5b45656429c75577885 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 14 Sep 2023 14:44:44 +0100 Subject: [PATCH 028/249] Fix docker build (#26) It's taking SO long to build on my laptop I suspect something is up with docker. I'm 99% sure this should fix the image build though, as the binary has moved. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index cb158e22abcc3..c9e3d8c1621d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,5 +22,5 @@ WORKDIR app USER nobody -COPY --from=builder /app/target/release/capture /usr/local/bin -ENTRYPOINT ["/usr/local/bin/capture"] +COPY --from=builder /app/target/release/capture-server /usr/local/bin +ENTRYPOINT ["/usr/local/bin/capture-server"] From af529b171981adbcf5eab53d1f0663b06f68c676 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 14 Sep 2023 15:30:06 +0100 Subject: [PATCH 029/249] Add graceful shutdown handler (#27) I rolled out the new image, saw the old took ages to kill, and realised we hadn't setup a SIGTERM handler. Add it! --- capture-server/src/main.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 88ffc949eea7a..e2232d5023655 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -2,6 +2,22 @@ use std::env; use std::net::SocketAddr; use capture::{router, sink, time}; +use tokio::signal; + +async fn shutdown() { + let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to register SIGTERM handler"); + + let mut interrupt = signal::unix::signal(signal::unix::SignalKind::interrupt()) + .expect("failed to register SIGINT handler"); + + tokio::select! { + _ = term.recv() => {}, + _ = interrupt.recv() => {}, + }; + + tracing::info!("Shutting down gracefully..."); +} #[tokio::main] async fn main() { @@ -29,6 +45,7 @@ async fn main() { axum::Server::bind(&address.parse().unwrap()) .serve(app.into_make_service_with_connect_info::()) + .with_graceful_shutdown(shutdown()) .await .unwrap(); } From 6ed0609d952758ac1037a5906e1c5a05dc3401f7 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 15 Sep 2023 11:28:24 +0200 Subject: [PATCH 030/249] get django_compat test closer to green (#28) --- Cargo.toml | 2 +- capture/src/capture.rs | 4 +++ capture/src/event.rs | 42 +++++++++--------------- capture/src/time.rs | 4 +-- capture/tests/django_compat.rs | 54 +++++++++++++++++++++++++++---- capture/tests/requests_dump.jsonl | 22 +++++++------ 6 files changed, 82 insertions(+), 46 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 68569a3ad2f86..117abaa970f3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" governor = "0.5.1" tower_governor = "0.0.4" -time = { version = "0.3.20", features = ["formatting", "macros", "serde"] } +time = { version = "0.3.20", features = ["formatting", "macros", "parsing", "serde"] } tower-http = { version = "0.4.0", features = ["trace"] } bytes = "1" anyhow = "1.0" diff --git a/capture/src/capture.rs b/capture/src/capture.rs index e60421759a062..d2f106fb9a657 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -69,6 +69,8 @@ pub async fn event( client_ip: ip.to_string(), }; + println!("Got context {:?}", &context); + process_events(state.sink.clone(), &events, &context).await?; Ok(Json(CaptureResponse { @@ -131,6 +133,8 @@ pub async fn process_events<'a>( .map(|e| process_single_event(e, context)) .collect::, CaptureError>>()?; + println!("Processed events: {:?}", events); + if events.len() == 1 { sink.send(events[0].clone()).await?; } else { diff --git a/capture/src/event.rs b/capture/src/event.rs index fd109f2594ae4..5b0c08d1c2798 100644 --- a/capture/src/event.rs +++ b/capture/src/event.rs @@ -13,8 +13,10 @@ use uuid::Uuid; #[derive(Deserialize, Default)] pub enum Compression { #[default] - #[serde(rename = "gzip-js")] - GzipJs, + Unsupported, + + #[serde(rename = "gzip", alias = "gzip-js")] + Gzip, } #[derive(Deserialize, Default)] @@ -26,15 +28,6 @@ pub struct EventQuery { #[serde(alias = "_")] pub sent_at: Option, - - #[serde(skip_serializing)] - pub token: Option, // Filled by handler - - #[serde(skip_serializing)] - pub now: Option, // Filled by handler from timesource - - #[serde(skip_serializing)] - pub client_ip: Option, // Filled by handler } #[derive(Debug, Deserialize)] @@ -78,7 +71,7 @@ impl RawEvent { tracing::debug!(len = bytes.len(), "decoding new event"); let payload = match query.compression { - Some(Compression::GzipJs) => { + Some(Compression::Gzip) => { let mut d = GzDecoder::new(bytes.reader()); let mut s = String::new(); d.read_to_string(&mut s).map_err(|e| { @@ -87,6 +80,12 @@ impl RawEvent { })?; s } + Some(_) => { + return Err(CaptureError::RequestDecodingError(String::from( + "unsupported compression format", + ))) + } + None => String::from_utf8(bytes.into()).map_err(|e| { tracing::error!("failed to decode body: {}", e); CaptureError::RequestDecodingError(String::from("invalid body encoding")) @@ -108,6 +107,7 @@ impl RawEvent { } } +#[derive(Debug)] pub struct ProcessingContext { pub lib_version: Option, pub sent_at: Option, @@ -116,14 +116,7 @@ pub struct ProcessingContext { pub client_ip: String, } -time::serde::format_description!( - django_iso, - OffsetDateTime, - "[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6][offset_hour \ - sign:mandatory]:[offset_minute]" -); - -#[derive(Clone, Default, Debug, Serialize)] +#[derive(Clone, Default, Debug, Serialize, Eq, PartialEq)] pub struct ProcessedEvent { pub uuid: Uuid, pub distinct_id: String, @@ -131,7 +124,7 @@ pub struct ProcessedEvent { pub site_url: String, pub data: String, pub now: String, - #[serde(with = "django_iso::option")] + #[serde(with = "time::serde::rfc3339::option")] pub sent_at: Option, pub token: String, } @@ -160,16 +153,13 @@ mod tests { let bytes = Bytes::from(decoded_horrible_blob); let events = RawEvent::from_bytes( &EventQuery { - compression: Some(Compression::GzipJs), + compression: Some(Compression::Gzip), lib_version: None, sent_at: None, - token: None, - now: None, - client_ip: None, }, bytes, ); - assert_eq!(events.is_ok(), true); + assert!(events.is_ok()); } } diff --git a/capture/src/time.rs b/capture/src/time.rs index e510f5f692fcc..3cfed322d5338 100644 --- a/capture/src/time.rs +++ b/capture/src/time.rs @@ -10,7 +10,7 @@ impl TimeSource for SystemTime { fn current_time(&self) -> String { let time = time::OffsetDateTime::now_utc(); - time.format(&time::format_description::well_known::Iso8601::DEFAULT) - .expect("failed to iso8601 format timestamp") + time.format(&time::format_description::well_known::Rfc3339) + .expect("failed to format timestamp") } } diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index 658ae137735b3..c39f2c7a91dfa 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -1,4 +1,4 @@ -use assert_json_diff::assert_json_eq; +use assert_json_diff::assert_json_matches_no_panic; use async_trait::async_trait; use axum::http::StatusCode; use axum_test_helper::TestClient; @@ -14,6 +14,8 @@ use serde_json::{json, Value}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::sync::{Arc, Mutex}; +use time::format_description::well_known::{Iso8601, Rfc3339}; +use time::OffsetDateTime; #[derive(Debug, Deserialize)] struct RequestDump { @@ -74,8 +76,10 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let file = File::open(REQUESTS_DUMP_FILE_NAME)?; let reader = BufReader::new(file); - for line in reader.lines() { - let case: RequestDump = serde_json::from_str(&line?)?; + let mut mismatches = 0; + + for (line_number, line_contents) in reader.lines().enumerate() { + let case: RequestDump = serde_json::from_str(&line_contents?)?; if !case.path.starts_with("/e/") { println!("Skipping {} test case", &case.path); continue; @@ -93,7 +97,7 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let app = router(timesource, sink.clone(), false); let client = TestClient::new(app); - let mut req = client.post("/i/v0/e/").body(raw_body); + let mut req = client.post(&format!("/i/v0{}", case.path)).body(raw_body); if !case.content_encoding.is_empty() { req = req.header("Content-encoding", case.content_encoding); } @@ -104,15 +108,51 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { req = req.header("X-Forwarded-For", case.ip); } let res = req.send().await; - assert_eq!(res.status(), StatusCode::OK, "{}", res.text().await); + assert_eq!( + res.status(), + StatusCode::OK, + "line {} rejected: {}", + line_number, + res.text().await + ); assert_eq!( Some(CaptureResponse { status: CaptureResponseCode::Ok }), res.json().await ); - assert_eq!(sink.len(), case.output.len()); - assert_json_eq!(json!(case.output), json!(sink.events())) + assert_eq!( + sink.len(), + case.output.len(), + "event count mismatch on line {}", + line_number + ); + + for (event_number, (message, expected)) in + sink.events().iter().zip(case.output.iter()).enumerate() + { + // Normalizing the expected event to align with known django->rust inconsistencies + let mut expected = expected.clone(); + if let Some(value) = expected.get_mut("sent_at") { + // Default ISO format is different between python and rust, both are valid + // Parse and re-print the value before comparison + let sent_at = + OffsetDateTime::parse(value.as_str().expect("empty"), &Iso8601::DEFAULT)?; + *value = Value::String(sent_at.format(&Rfc3339)?) + } + + let match_config = assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict); + if let Err(e) = + assert_json_matches_no_panic(&json!(expected), &json!(message), match_config) + { + println!( + "mismatch at line {}, event {}: {}", + line_number, event_number, e + ); + mismatches += 1; + } + } } + assert_eq!(0, mismatches, "some events didn't match"); Ok(()) } diff --git a/capture/tests/requests_dump.jsonl b/capture/tests/requests_dump.jsonl index b62f1c61665a7..88d30102039ee 100644 --- a/capture/tests/requests_dump.jsonl +++ b/capture/tests/requests_dump.jsonl @@ -1,10 +1,12 @@ -{"path":"/e/?ip=1&_=1684771477160&ver=1.57.2","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-05-22T16:04:37.164197+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ2Y0hSZmFXNGlMQ0p3Y205d1pYSjBhV1Z6SWpwN0lpUnZjeUk2SWsxaFl5QlBVeUJZSWl3aUpHOXpYM1psY25OcGIyNGlPaUl4TUM0eE5TNHdJaXdpSkdKeWIzZHpaWElpT2lKR2FYSmxabTk0SWl3aUpHUmxkbWxqWlY5MGVYQmxJam9pUkdWemEzUnZjQ0lzSWlSamRYSnlaVzUwWDNWeWJDSTZJbWgwZEhBNkx5OXNiMk5oYkdodmMzUTZPREF3TUM4aUxDSWthRzl6ZENJNklteHZZMkZzYUc5emREbzRNREF3SWl3aUpIQmhkR2h1WVcxbElqb2lMeUlzSWlSaWNtOTNjMlZ5WDNabGNuTnBiMjRpT2pFeE15d2lKR0p5YjNkelpYSmZiR0Z1WjNWaFoyVWlPaUpsYmkxVlV5SXNJaVJ6WTNKbFpXNWZhR1ZwWjJoMElqbzVOelFzSWlSelkzSmxaVzVmZDJsa2RHZ2lPakUxTURBc0lpUjJhV1YzY0c5eWRGOW9aV2xuYUhRaU9qZzBNeXdpSkhacFpYZHdiM0owWDNkcFpIUm9Jam94TkRNekxDSWtiR2xpSWpvaWQyVmlJaXdpSkd4cFlsOTJaWEp6YVc5dUlqb2lNUzQxTnk0eUlpd2lKR2x1YzJWeWRGOXBaQ0k2SW5CM01XeHBaSEZ6TTNGMWJXaDJjekVpTENJa2RHbHRaU0k2TVRZNE5EYzNNVFEzTnk0eE5pd2laR2x6ZEdsdVkzUmZhV1FpT2lKblVVUkliSEI0Y1VOSVoxVnJhRFZ6TTB4UFZsVlJOVUZqVG5ReGEyMVdiREI0ZGs5aGFqSnpaalp0SWl3aUpHUmxkbWxqWlY5cFpDSTZJakU0T0RRek1HVXpOR0kxTTJOakxUQmtNMk0yWXpjd09UUTBNVFZrT0MwME1USmtNbU16WkMweE5qUmlNRGd0TVRnNE5ETXdaVE0wWWpaa016Z2lMQ0lrZFhObGNsOXBaQ0k2SW1kUlJFaHNjSGh4UTBoblZXdG9OWE16VEU5V1ZWRTFRV05PZERGcmJWWnNNSGgyVDJGcU1uTm1ObTBpTENKcGMxOWtaVzF2WDNCeWIycGxZM1FpT21aaGJITmxMQ0lrWjNKdmRYQnpJanA3SW5CeWIycGxZM1FpT2lJd01UZzRORE13WlMwek1UWmxMVEF3TURBdE5URmhOaTFtWkRZME5qVTBPRFk1TUdVaUxDSnZjbWRoYm1sNllYUnBiMjRpT2lJd01UZzRORE13WlMweU9UQTVMVEF3TURBdE5HUmxNUzA1T1RVM056SmhNVEEwTlRRaUxDSnBibk4wWVc1alpTSTZJbWgwZEhBNkx5OXNiMk5oYkdodmMzUTZPREF3TUNKOUxDSWtZWFYwYjJOaGNIUjFjbVZmWkdsellXSnNaV1JmYzJWeWRtVnlYM05wWkdVaU9tWmhiSE5sTENJa1lXTjBhWFpsWDJabFlYUjFjbVZmWm14aFozTWlPbHRkTENJa1ptVmhkSFZ5WlY5bWJHRm5YM0JoZVd4dllXUnpJanA3ZlN3aWNHOXpkR2h2WjE5MlpYSnphVzl1SWpvaU1TNDBNeTR3SWl3aWNtVmhiRzBpT2lKb2IzTjBaV1F0WTJ4cFkydG9iM1Z6WlNJc0ltVnRZV2xzWDNObGNuWnBZMlZmWVhaaGFXeGhZbXhsSWpwbVlXeHpaU3dpYzJ4aFkydGZjMlZ5ZG1salpWOWhkbUZwYkdGaWJHVWlPbVpoYkhObExDSWtjbVZtWlhKeVpYSWlPaUlrWkdseVpXTjBJaXdpSkhKbFptVnljbWx1WjE5a2IyMWhhVzRpT2lJa1pHbHlaV04wSWl3aWRHOXJaVzRpT2lKd2FHTmZha2hqUkhVM2JUTmFkbTVKYm5CclluaHRTbkpMUldKNVNuVnJlVUZhUTNwNVMyVk1NSE5VZUVJemF5SXNJaVJ6WlhOemFXOXVYMmxrSWpvaU1UZzRORFF6TWpaaU4yUXhZakV4TFRCbE4ySTJNVGcwWVRJek9EVXdPQzAwTVRKa01tTXpaQzB4TmpSaU1EZ3RNVGc0TkRRek1qWmlOMlV4WldRd0lpd2lKSGRwYm1SdmQxOXBaQ0k2SWpFNE9EUTBNelE1Tm1FMFlpMHdaamMzTkdGbE9UUTRPVEk1TmpndE5ERXlaREpqTTJRdE1UWTBZakE0TFRFNE9EUTBNelE1Tm1FMU1qQXpNU0lzSWlSd1lXZGxkbWxsZDE5cFpDSTZJakU0T0RRME16UTVObUUyTVROallpMHdOak0wTmpjME9HRTNPR05pWXkwME1USmtNbU16WkMweE5qUmlNRGd0TVRnNE5EUXpORGsyWVRjeU1UZ3pJbjBzSW5ScGJXVnpkR0Z0Y0NJNklqSXdNak10TURVdE1qSlVNVFk2TURRNk16Y3VNVFl3V2lKOQ==","output":[{"uuid":"01884434-96bc-0000-a64d-d01794a3cbbd","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pw1lidqs3qumhvs1\", \"$time\": 1684771477.16, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"timestamp\": \"2023-05-22T16:04:37.160Z\"}","now":"2023-05-22T16:04:37.164197+00:00","sent_at":"2023-05-22T16:04:37.160000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?ip=1&_=1684771477161&ver=1.57.2","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-05-22T16:04:37.165076+00:00","body":"ZGF0YT1leUpsZG1WdWRDSTZJaVJ3WVdkbGRtbGxkeUlzSW5CeWIzQmxjblJwWlhNaU9uc2lKRzl6SWpvaVRXRmpJRTlUSUZnaUxDSWtiM05mZG1WeWMybHZiaUk2SWpFd0xqRTFMakFpTENJa1luSnZkM05sY2lJNklrWnBjbVZtYjNnaUxDSWtaR1YyYVdObFgzUjVjR1VpT2lKRVpYTnJkRzl3SWl3aUpHTjFjbkpsYm5SZmRYSnNJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0x5SXNJaVJvYjNOMElqb2liRzlqWVd4b2IzTjBPamd3TURBaUxDSWtjR0YwYUc1aGJXVWlPaUl2SWl3aUpHSnliM2R6WlhKZmRtVnljMmx2YmlJNk1URXpMQ0lrWW5KdmQzTmxjbDlzWVc1bmRXRm5aU0k2SW1WdUxWVlRJaXdpSkhOamNtVmxibDlvWldsbmFIUWlPamszTkN3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRVd01Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPRFF6TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFME16TXNJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpVM0xqSWlMQ0lrYVc1elpYSjBYMmxrSWpvaWJuUjVkWE51Tm5Zek1uWmxhbWN3Y3lJc0lpUjBhVzFsSWpveE5qZzBOemN4TkRjM0xqRTJMQ0prYVhOMGFXNWpkRjlwWkNJNkltZFJSRWhzY0hoeFEwaG5WV3RvTlhNelRFOVdWVkUxUVdOT2RERnJiVlpzTUhoMlQyRnFNbk5tTm0waUxDSWtaR1YyYVdObFgybGtJam9pTVRnNE5ETXdaVE0wWWpVelkyTXRNR1F6WXpaak56QTVORFF4TldRNExUUXhNbVF5WXpOa0xURTJOR0l3T0MweE9EZzBNekJsTXpSaU5tUXpPQ0lzSWlSMWMyVnlYMmxrSWpvaVoxRkVTR3h3ZUhGRFNHZFZhMmcxY3pOTVQxWlZVVFZCWTA1ME1XdHRWbXd3ZUhaUFlXb3ljMlkyYlNJc0ltbHpYMlJsYlc5ZmNISnZhbVZqZENJNlptRnNjMlVzSWlSbmNtOTFjSE1pT25zaWNISnZhbVZqZENJNklqQXhPRGcwTXpCbExUTXhObVV0TURBd01DMDFNV0UyTFdaa05qUTJOVFE0Tmprd1pTSXNJbTl5WjJGdWFYcGhkR2x2YmlJNklqQXhPRGcwTXpCbExUSTVNRGt0TURBd01DMDBaR1V4TFRrNU5UYzNNbUV4TURRMU5DSXNJbWx1YzNSaGJtTmxJam9pYUhSMGNEb3ZMMnh2WTJGc2FHOXpkRG80TURBd0luMHNJaVJoZFhSdlkyRndkSFZ5WlY5a2FYTmhZbXhsWkY5elpYSjJaWEpmYzJsa1pTSTZabUZzYzJVc0lpUmhZM1JwZG1WZlptVmhkSFZ5WlY5bWJHRm5jeUk2VzEwc0lpUm1aV0YwZFhKbFgyWnNZV2RmY0dGNWJHOWhaSE1pT250OUxDSndiM04wYUc5blgzWmxjbk5wYjI0aU9pSXhMalF6TGpBaUxDSnlaV0ZzYlNJNkltaHZjM1JsWkMxamJHbGphMmh2ZFhObElpd2laVzFoYVd4ZmMyVnlkbWxqWlY5aGRtRnBiR0ZpYkdVaU9tWmhiSE5sTENKemJHRmphMTl6WlhKMmFXTmxYMkYyWVdsc1lXSnNaU0k2Wm1Gc2MyVXNJaVJ5WldabGNuSmxjaUk2SWlSa2FYSmxZM1FpTENJa2NtVm1aWEp5YVc1blgyUnZiV0ZwYmlJNklpUmthWEpsWTNRaUxDSjBiMnRsYmlJNkluQm9ZMTlxU0dORWRUZHRNMXAyYmtsdWNHdGllRzFLY2t0RllubEtkV3Q1UVZwRGVubExaVXd3YzFSNFFqTnJJaXdpSkhObGMzTnBiMjVmYVdRaU9pSXhPRGcwTkRNeU5tSTNaREZpTVRFdE1HVTNZall4T0RSaE1qTTROVEE0TFRReE1tUXlZek5rTFRFMk5HSXdPQzB4T0RnME5ETXlObUkzWlRGbFpEQWlMQ0lrZDJsdVpHOTNYMmxrSWpvaU1UZzRORFF6TkRrMllUUmlMVEJtTnpjMFlXVTVORGc1TWprMk9DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORFF6TkRrMllUVXlNRE14SWl3aUpIQmhaMlYyYVdWM1gybGtJam9pTVRnNE5EUXpORGsyWVRZeE0yTmlMVEEyTXpRMk56UTRZVGM0WTJKakxUUXhNbVF5WXpOa0xURTJOR0l3T0MweE9EZzBORE0wT1RaaE56SXhPRE1pZlN3aWRHbHRaWE4wWVcxd0lqb2lNakF5TXkwd05TMHlNbFF4Tmpvd05Eb3pOeTR4TmpGYUluMCUzRA==","output":[{"uuid":"01884434-96ba-0000-1404-a179647bd08a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ntyusn6v32vejg0s\", \"$time\": 1684771477.16, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"timestamp\": \"2023-05-22T16:04:37.161Z\"}","now":"2023-05-22T16:04:37.165076+00:00","sent_at":"2023-05-22T16:04:37.161000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/engage/?ip=1&_=1684771477165&ver=1.57.2","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-05-22T16:04:37.167862+00:00","body":"ZGF0YT1leUlrYzJWMElqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR0p5YjNkelpYSmZkbVZ5YzJsdmJpSTZNVEV6TENJa2NtVm1aWEp5WlhJaU9pSWtaR2x5WldOMElpd2lKSEpsWm1WeWNtbHVaMTlrYjIxaGFXNGlPaUlrWkdseVpXTjBJaXdpWlcxaGFXd2lPaUo0WVhacFpYSkFjRzl6ZEdodlp5NWpiMjBpZlN3aUpIUnZhMlZ1SWpvaWNHaGpYMnBJWTBSMU4yMHpXblp1U1c1d2EySjRiVXB5UzBWaWVVcDFhM2xCV2tONmVVdGxUREJ6VkhoQ00yc2lMQ0lrWkdsemRHbHVZM1JmYVdRaU9pSm5VVVJJYkhCNGNVTklaMVZyYURWek0weFBWbFZSTlVGalRuUXhhMjFXYkRCNGRrOWhhakp6WmpadElpd2lKR1JsZG1salpWOXBaQ0k2SWpFNE9EUXpNR1V6TkdJMU0yTmpMVEJrTTJNMll6Y3dPVFEwTVRWa09DMDBNVEprTW1NelpDMHhOalJpTURndE1UZzRORE13WlRNMFlqWmtNemdpTENJa2RYTmxjbDlwWkNJNkltZFJSRWhzY0hoeFEwaG5WV3RvTlhNelRFOVdWVkUxUVdOT2RERnJiVlpzTUhoMlQyRnFNbk5tTm0waWZRJTNEJTNE","output":[{"uuid":"01884434-96bb-0000-669a-d0fa1bef0768","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"$set\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$browser_version\": 113, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"email\": \"xavier@posthog.com\"}, \"$token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"event\": \"$identify\", \"properties\": {}}","now":"2023-05-22T16:04:37.167862+00:00","sent_at":"2023-05-22T16:04:37.165000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684771480232&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:40.235238+00:00","body":"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","output":[{"uuid":"01884434-a2ab-0000-af34-b060321fb9d0","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"pewv8vh77ktqcv8n\", \"$time\": 1684771477.161, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"session-reset-on-load\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 3067}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0001-adb0-d108b4f12665","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"977yp0jwt21hnwcg\", \"$time\": 1684771477.166, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"project\", \"$group_key\": \"0188430e-316e-0000-51a6-fd646548690e\", \"$group_set\": {\"id\": 1, \"uuid\": \"0188430e-316e-0000-51a6-fd646548690e\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 3062}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0002-1714-12b5f9fc4941","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"p6w8hxbuqycejwr9\", \"$time\": 1684771477.166, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"organization\", \"$group_key\": \"0188430e-2909-0000-4de1-995772a10454\", \"$group_set\": {\"id\": \"0188430e-2909-0000-4de1-995772a10454\", \"name\": \"x\", \"slug\": \"x\", \"created_at\": \"2023-05-22T10:43:01.514795Z\", \"available_features\": [], \"taxonomy_set_events_count\": 0, \"taxonomy_set_properties_count\": 0, \"instance_tag\": \"none\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 3062}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0003-6004-77b16abfc46e","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"b3aji1adgd006z7q\", \"$time\": 1684771477.232, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"posthog-3000\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2996}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0004-6b5c-d8e7d6e97bd8","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"3ud3ntuo2qae4tw7\", \"$time\": 1684771477.233, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"enable-prompts\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2995}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0005-3e04-fa6fc146673a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"v5q0dt8g357ju35s\", \"$time\": 1684771477.24, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"notebooks\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2987}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0006-5a0e-177fb26860c5","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"evyz0oq5z2yo9zl8\", \"$time\": 1684771477.243, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"cloud-announcement\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2985}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0007-dc5e-bb723cae292a","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"fwcsezx3qw2jryru\", \"$time\": 1684771477.252, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"hogql\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2976}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0008-5f7e-6efafcb7b2be","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"gzltwvh6qtff36oz\", \"$time\": 1684771477.266, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"hackathon-apm\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2962}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0009-c180-af233b44b000","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"gjxvn1u0l3l5fxqc\", \"$time\": 1684771477.267, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"early-access-feature\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2961}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000a-bd12-c96d4565d278","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"lt2fhodtixw6kfuv\", \"$time\": 1684771477.267, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"feedback-scene\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2961}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000b-02bb-7a705b509049","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"f255icw4jshgl94d\", \"$time\": 1684771477.282, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"require-email-verification\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2946}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000c-e137-db1cc0161182","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"6rvyf37nr7704osj\", \"$time\": 1684771477.349, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"session-recording-infinite-list\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2879}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000d-7bab-cce00f8e396c","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"yu008bx11z13mm65\", \"$time\": 1684771477.349, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"session-recording-summary-listing\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2879}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000e-4ed9-e08a5a7ecb60","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"68vvaoju15ug02zn\", \"$time\": 1684771477.349, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"recordings-list-v2-enabled\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"188443496a613cb-06346748a78cbc-412d2c3d-164b08-188443496a72183\"}, \"offset\": 2879}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-000f-15e6-c0c8a84bd8d3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ejk2l0xdtzty23na\", \"$time\": 1684771477.382, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2846}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0010-a367-b217cfce9758","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"q4csan5ar3uyxu1c\", \"$time\": 1684771477.402, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"instance\", \"$group_key\": \"http://localhost:8000\", \"$group_set\": {\"site_url\": \"http://localhost:8000\"}, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2826}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0011-ab12-d83075b735b3","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"client_request_failure\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"fux7i2k80t2uyqah\", \"$time\": 1684771477.622, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"pathname\": \"/api/billing-v2/\", \"method\": \"GET\", \"duration\": 341, \"status\": 404, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2606}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0012-7b5a-642b99f7c343","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"recording list fetched\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"jdnaxi7p1xu8abp6\", \"$time\": 1684771477.793, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"load_time\": 311, \"listing_version\": \"1\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2435}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0013-9ed2-da153edee668","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"lvp6x0x9grc01m4s\", \"$time\": 1684771478.077, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"data-exploration-insights\", \"$feature_flag_response\": false, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2151}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0014-b658-f962d9c6de38","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"dashboard loading time\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ywaj59v3bl8q9scj\", \"$time\": 1684771478.16, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"loadingMilliseconds\": 816, \"dashboardId\": 1, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 2068}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-a2ab-0015-fdde-825234a6f71f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"viewed dashboard\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"wb213rksdxcsobh3\", \"$time\": 1684771478.906, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"created_at\": \"2023-05-22T10:43:03.675923Z\", \"is_shared\": false, \"pinned\": true, \"creation_mode\": \"default\", \"sample_items_count\": 6, \"item_count\": 6, \"created_by_system\": true, \"dashboard_id\": 1, \"lastRefreshed\": \"2023-05-22T16:02:16.882Z\", \"refreshAge\": 142, \"trends_count\": 3, \"retention_count\": 1, \"lifecycle_count\": 1, \"funnels_count\": 1, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 1322}","now":"2023-05-22T16:04:40.235238+00:00","sent_at":"2023-05-22T16:04:40.232000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684771480251&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:40.254254+00:00","body":"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","output":[{"uuid":"01884434-a2db-0000-c91c-f13ff43dd8c8","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-a2da-0000-ee43-723c53b20b8f\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"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\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": true, \"events_summary\": [{\"timestamp\": 1684771476282, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476788, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476788, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476789, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476790, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476791, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476792, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476793, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476794, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476795, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476795, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476800, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476800, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771476896, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477161, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477166, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477172, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477174, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477175, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477175, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477208, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477225, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477229, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477229, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477231, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477238, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477238, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477239, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477247, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477247, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477248, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477252, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477252, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477262, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477281, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477286, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477300, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477327, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477328, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477343, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477370, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477402, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477402, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477410, \"type\": 4, \"data\": {\"href\": \"http://localhost:8000/home\", \"width\": 1433, \"height\": 843}}, {\"timestamp\": 1684771477428, \"type\": 2, \"data\": {}}, {\"timestamp\": 1684771477481, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477482, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477483, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771477488, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477508, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477543, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477565, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477569, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477576, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477600, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477603, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477624, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771477631, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477664, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477701, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477773, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771477796, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478421, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478426, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478429, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478432, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478433, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478447, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478449, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478472, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478476, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478478, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478480, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478481, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478491, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478494, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478501, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478552, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771478779, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771479028, \"type\": 3, \"data\": {\"source\": 4, \"width\": 1433, \"height\": 540}}, {\"timestamp\": 1684771479110, \"type\": 3, \"data\": {\"source\": 2, \"type\": 6}}, {\"timestamp\": 1684771479754, \"type\": 3, \"data\": {\"source\": 1}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"offset\": 2839}","now":"2023-05-22T16:04:40.254254+00:00","sent_at":"2023-05-22T16:04:40.251000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684771483262&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:43.268842+00:00","body":"H4sIAAAAAAAAA+1abW/kthH+KwvB982yRb3LQFGc7669Jtce0ssFRYJAoEhqJS9XVChq1+vD/ffOSLuytF6ndZAPTqwvXnFeOMPhzDyk5J++WGIjKmNdWWe0NYrR2rRaWOdWrVUttClFY119sc4U/Fj/pGzx8dPiP8AGQroRuilVBQziXJDgwkF6ptW2ERqIfyu1yNUtErnYlEykZlcLYLwVzcqoGhms1RrMp62WwCiMqa8uLyW4IQvVmKvYcZzLQq3RoTOkgNCUi4yamqKiIHRlDcJ7N+59JMQbkSWtli1dooqo7M+fUKVhWogqLUS5LMBQEvn3xG3JTQGTBI4DxE0ptrXSZpAN/An5IO17aFOWGZjZigyNwGAct4sgunCRXlbglklLDtTMZwXdZZTdxVGWkA3yTYnrI2HsRxHxY/ci8t1zi5eNKSu211t+9/a9rG9/efN++XlVBI334eMPn78LXrN/GbJa/yCd281HeuM2ebgebUqnSuLY9xzh+VngMWY73GMhi5zE90nAY9snLneZx20S+pkT2/fiIfdinKzFqP4GL8om5WKtUki3G8EglDmVjYAJl1q1dZd7A8ty9nZtj4TCht137IDQ0M556IeBH4eJg5uv9JJW5R01fZAHLTdxkl7L54LYSRJEkUuJ4wc+elI1hlYMU+JkHlpfwatRjaQQfJpJwVNYOmxp2pQclA/+U2bKjUhzQTvhXNIlrOann4E1pqU13UlFOa4UDNRgrVDLSYr4XldZWlC5RudARHCbyZKtCgVxB55Y01J2fuCO0g2M0LXBm0ZStvoV/hlTVaOkSCXY1oIpzctqmYrqxAKNblGjEQ16OJLun+5rbqpmbbo8h5YgoOSxP5xxaBCwrwMVJ+EKloLrHnG7DnVoHt26u9YhRsWNUlKsQQ6D/MUyENl9S6DdFDI14hZz6N+ilnQHNCZpA4sAcesDZGB13RoDc52PR7ZtsAdSjQoTOuSKaRv4gV2BWj/i5q2Udt8FjjgFbewSom1BIlBjdHpYV3Yw31M774A8Ul6cdGxKnbo15d07NaUPLh1sc2qojY9gH0La2qURa1sfAtcLFbBn2HB7MvwwhJFzqzJFyopSQiuAXcGRyvdrJF/PJzsjy6l8eCTvH8m38mnz8xJb52ifP0EmXlOMrqoMujtswiHcDyR+J4ONhCf9K/b2Ar+LOSAO88LeqPbxhS6OBScOAABOHPBOOvBkj3tPOrDQSv0Pm8dTZIrfJ6EpoOa7TEIYnswD6Hg0D8TAqJXA5lIXLL15z9620dr7cVP9o6pX2e36G/3tu2z3Tbvavf7xzd3uW/HBab6/vfa6dnPodwNc+p4bZhEnGSG2I6IsJLFPXS8OAB9P4WUvL4jg3aFlW1ZcbcfT+UlI/cx28ijyqUj8OHGT8LG5UDhwHY/0B6AlYLk4mi2K/dALbADxPKAkTxIeRvnj80Vx4LpBB3MqzxuBxxoSY/SHE+LBzrM6Hk4a0F/zUkJnbP7yKrp+5boPQYq3ujsXAPOV97qXQrv7sesOokhx38DfldgN3LF6x9xQ2e6VQ6cnYWCoUXpQWhp8jN727PvI7QWC61fBntdF+gQdDxOA0A8Z0KrhHKHVejBmu4T35v7vM/NxC/9DH55b4fHc0zkLb0gRxHiye3h4ThzoVPPheT48/+kPzy8J8Sh3EpI5tuOJzAeoc8OAs0d9Q3HqOok7RjwvdMaIN+zPAo0Jvmja9bq/DcwAOAPgswTAW57LOFhlmVC5yRTeBk4AIAlmAJwB8AUAYN+4U6yAdA1xCoIQyuphP+yYUIdaQ62lW6or5DLVIhAAHSwo3Y9R595lfML2Ae9rV53kDLlPg1w/nC+ZM8b+oTB2F25leasjwzS/icjNaYzFRjNj7Iyxf3qMfVmIFxCPeJCmYSZITojLYtfPH0e8wCUiDMeI5yaT16qQJwg/WvzSisakOWz9c/sEP8PfDH8j+GNuRU1cVU4VVHJZd8XyEP6S+Yo5w99LgL9xndO6vNznQ3NJLh86gW1AQlU0oLgWEEtMyb+/+x6Gh8a37wX9t3z4IupAnc8Q+zSIDSeXSqNoYxbdFX7G1RlXnyuuulGsq5s7EuZNqJekW88JXE1mXJ1x9QXg6hos9aX6AQK1GJBzgVckwa8W7ypeq7Iyi0qZRQ6vZvnFfBt9OlS6pz95YqwXuTCsEHxGzRk1nytqlkGQe3QZbqO7LNuR7v99j1DTuyBuNKPmjJovADUxPN3nTsh/D+oM2ziKjmI0g+RTQZJ4/tef/ws4HAtKLTUAAA==","output":[{"uuid":"01884434-ae85-0000-0790-3f208b1ec925","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/home\", \"$host\": \"localhost:8000\", \"$pathname\": \"/home\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"b4chaybacz87b91v\", \"$time\": 1684771482.742, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"a\", \"$el_text\": \"Replay\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__type\": \"button\", \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__data-attr\": \"menu-item-replay\", \"attr__href\": \"/replay/recent\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 6, \"nth_of_type\": 4}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar__layout\"], \"attr__class\": \"SideBar SideBar__layout\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 4, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"18844349784635-094f5a1f99d67f8-412d2c3d-164b08-188443497852250\"}, \"offset\": 518}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0001-5a90-cd53a406401d","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"ue3df3rfc6j1h584\", \"$time\": 1684771482.901, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad091b0-03eb47f8265dc08-412d2c3d-164b08-1884434ad0a2092\"}, \"offset\": 360}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0002-988a-cc85b5fb42f1","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"recording viewed summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"xdfl85kbbeoftbor\", \"$time\": 1684771482.915, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 5563, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad091b0-03eb47f8265dc08-412d2c3d-164b08-1884434ad0a2092\"}, \"offset\": 346}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0003-e5d8-07b69a479391","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"y6wlixr7tcrdj71j\", \"$time\": 1684771482.963, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 298}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0004-c4f5-64ddb410d79f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"client_request_failure\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"c2nat8nn0n5nlgp1\", \"$time\": 1684771482.995, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"pathname\": \"/api/projects/1/session_recording_playlists\", \"method\": \"GET\", \"duration\": 113, \"status\": 404, \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 266}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0005-2691-56f4c4e7d4bb","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"toast error\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"278rnjz16fs6rg1t\", \"$time\": 1684771482.999, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"message\": \"Load playlists failed: Endpoint not found.\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 262}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"},{"uuid":"01884434-ae85-0006-1698-9970569297af","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"recording list fetched\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 540, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"i55f3ag6w7zbby1a\", \"$time\": 1684771483.127, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"load_time\": 130, \"listing_version\": \"1\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"offset\": 134}","now":"2023-05-22T16:04:43.268842+00:00","sent_at":"2023-05-22T16:04:43.262000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684771483260&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:43.265573+00:00","body":"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","output":[{"uuid":"01884434-ae8b-0001-daec-3c60a40202fa","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-ae8b-0000-78d7-f52731c49734\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"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\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684771480258, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771480759, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771481261, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771481896, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771482397, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771482683, \"type\": 3, \"data\": {\"source\": 2, \"type\": 5}}, {\"timestamp\": 1684771482683, \"type\": 3, \"data\": {\"source\": 2, \"type\": 1}}, {\"timestamp\": 1684771482684, \"type\": 3, \"data\": {\"source\": 2, \"type\": 5}}, {\"timestamp\": 1684771482739, \"type\": 3, \"data\": {\"source\": 2, \"type\": 0}}, {\"timestamp\": 1684771482743, \"type\": 3, \"data\": {\"source\": 2, \"type\": 2}}, {\"timestamp\": 1684771482757, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771482767, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771482881, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771482901, \"type\": 5, \"data\": {\"tag\": \"$pageview\", \"payload\": {\"href\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\"}}}, {\"timestamp\": 1684771482953, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482953, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482954, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482955, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482956, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482956, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482956, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482957, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482957, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482957, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482958, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482958, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482959, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482960, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482961, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482963, \"type\": 5, \"data\": {\"tag\": \"$pageview\", \"payload\": {\"href\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\"}}}, {\"timestamp\": 1684771482968, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482974, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482983, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482990, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771482991, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771482993, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771482995, \"type\": 6, \"data\": {\"plugin\": \"posthog/network@1\", \"payload\": {}}}, {\"timestamp\": 1684771483000, \"type\": 6, \"data\": {\"plugin\": \"rrweb/console@1\", \"payload\": {\"level\": \"error\"}}}, {\"timestamp\": 1684771483002, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483013, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483131, \"type\": 3, \"data\": {\"source\": 0}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"offset\": 2993}","now":"2023-05-22T16:04:43.265573+00:00","sent_at":"2023-05-22T16:04:43.260000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684771486268&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:46.270510+00:00","body":"H4sIAAAAAAAAA+1VXW+bMBT9K5OVR5BsbMDkrWsnpR9aVKntQ6sqMuAkFDAMOyks4r/vOmRq0ynZHqY+5Qn73nPONZwj87RBci2VQWM00krUelkZ5KC6qWrZmExqNN68dWapMMJWTFdLNKYO+l3Q1apJoIQdZGRrgPb07CBhTJPFK2Nl7L6RZbXebjaoFg3MvUzRmATMc1A2rGhveWm6ZfSglpVSG1HWtstZGBLGKaU+tEZaap1VamapiHDOGPWCOExJTIiLZRgHhDPhUe5j7jLipV5CUxeGxLB/w0siUwwvPXrNVFq9vpdjUSBY7OJ5GDIhI8YjLwoOaVmw72FKQMtUuVQgUy+T2cskuViFJX1cq0tV53FbXjXX3+LuapV3Z4/nP7treYP1XfuV5sBMM20ylZjhGIvbi0lRtz/OJ4v7fOlrejN9uL/1z5LvhuTlQ4Hb9VS8eHoelAg+STWfawlmehHFvfN53m4G+8IIaO8bQDZdAdwNEiorhQG73LoQnQueGqijZqVUphao7+15dyoQh32VpBAaFuiuEvB15t1sZuzqy4e9a5aylK5bZIvln01XNk3VwKS9JP5D3EJCT3E7GjffZ/89bgS4lc5sZIaItWgcAK6DeSE8h6wEg2fT3Ulwf9DFyD+5eNxFanP+CS5y/y8uusyHC36L9ThcBhbMQWn3j4DKPhpv0wdohnfSdsQOHX5AU8jqoZAw5vNTSI6FhHDM++dfwg/U6TcIAAA=","output":[{"uuid":"01884434-ba3e-0001-712d-9832c78876cc","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-ba3e-0000-51fd-ebb6a55b6144\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAJ6Sa2QC/8VU22qDQBA9nxJ8rhA1RtM/6FNf+hZKsbk0QqKipkSCv972zLpuYy6thEAYVndn5syZmb18f02xh4USFTIsOHvEAB4e+LUwR0RLpLXiVyDFFjlmxneofUtqdvwWWj/Fq7ZIjJKYGO/Eit+pT07thrE/O1ZhzIgXa0LkEzNqbA7GGMHV6PhI76E+YJ9TDhlrk3FMTuGTGjdk+o0RMkpAcfgPGU/E18h79mt/VG2AiWG/jGsykZzXJo+9QiSqB5HqRcqVzarWXFecNX0pDUJ2aUsfwST44LpW0vbkOC+3V14zxVcYnYUXZhIp9hhLZvJGKY1u8I/d5lip07Tg3Gb0mLmueiFtYnJKytFU9/f5vO0Zk5l3kzPmaN+MloIZtLvbPUc7vRqb2JXWuMzFO3O3Avp263pmH5eMu6CmPd/1lfVPeMPuUX9I3tvVb7Mav/NW7EzU0NyJ6kDnXHjF3F5cQ45TLtH7Z7j8C1xBDy55AUdX7u5IdSVU6B+N+AMddAYAAA==\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684771483335, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483713, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771483953, \"type\": 3, \"data\": {\"source\": 1}}, {\"timestamp\": 1684771484458, \"type\": 3, \"data\": {\"source\": 1}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"offset\": 2930}","now":"2023-05-22T16:04:46.270510+00:00","sent_at":"2023-05-22T16:04:46.268000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/s/?compression=gzip-js&ip=1&_=1684771486823&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:46.831635+00:00","body":"H4sIAAAAAAAAA+2US0/jMBSF/4vVZTLyK85jx8BIHUCDkIBFEaqc2DQmTeKJ3TaZqv99bngIWHRTqbvuknPvOb7Kd53HLdJr3XiUoYlrpHVl61GAbNda3XmjHcq2n5W5kl6Oih+sRhkL0Ifg2lVXgESD95oIkFEoI7HYgWRq7bysLQgi4XFMeCJihqE0cdo50zbzsRuRJOGcUZHHiuSEhFjHuSAJl5QlEU5CTqiiBVMhETyH989+TbTCMPlkYxrVbr7G8VRInof4OY651ClPUpqKfVljc0QxI5Dl20o3EGPLYv4yLS5Wcc1m6+Z3Y6u8ry+7q1/5cLmqhrPZ+b/hSl9jd9f/ZBU4lXHeNIV/G2NxezFd2v7v+XRxX5WRY9c3D/e30Vnxx5Oqfljifn0jX6h7FjX6/rUQxZSFOAopvSMiwzzj4kfM6Az6jk1uPzd64nYYN3YEbhgO1r0H2+NTgKT3nclXfoyBu/12BVOwfS2A2Q9L8G6RbEwtPXAM7VIOIYzvQUdWrpxWaLfbQWan63b9GjgeoNTr0/7tSE/bcdh2pEfYDh6gjVG+BEScQb3UZlFCesLZXoLR6b98GMEIA8Gn/+KghNVTBwAA","output":[{"uuid":"01884434-bc70-0001-6e8d-1718c6247aff","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$snapshot\", \"properties\": {\"$snapshot_data\": {\"chunk_id\": \"01884434-bc70-0000-a5ba-6c77cd70a82b\", \"chunk_index\": 0, \"chunk_count\": 1, \"data\": \"H4sIAJ6Sa2QC/71Syw6CMBCcTzGcNUFBRH/FcMBHhETE0PoghF9Xh6VUjfFCjGlStrOzs9Mt99sSFRxolDhiy2iBATwMuTvYIGYmNmjDU8hxQoG15U4M910hMGhKjRYZY0a0tuwUGdlK9DNWdqwAIXxyZ4x9xgEjD66p/JfXvj4nP/Hp2u5bXLkrgy8RmUyjoVmTYsVaLQ47TvUx97nt/r2uddJ43lsflVQcZAaxzCLnacRb7XkuGbVz0bbCYS6msiLSOKhlda4Lohk1zm9+n3facL3ifV9h/pNX8A33ItPUSGxXnzqdUkJ+ih2/2uRDyffzPpU/PcIDAvlBHZgDAAA=\", \"compression\": \"gzip-base64\", \"has_full_snapshot\": false, \"events_summary\": [{\"timestamp\": 1684771486730, \"type\": 3, \"data\": {\"source\": 2, \"type\": 6}}, {\"timestamp\": 1684771486732, \"type\": 3, \"data\": {\"source\": 2, \"type\": 6}}, {\"timestamp\": 1684771486739, \"type\": 3, \"data\": {\"source\": 0}}, {\"timestamp\": 1684771486750, \"type\": 3, \"data\": {\"source\": 4, \"width\": 1433, \"height\": 843}}]}, \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\"}, \"timestamp\": \"2023-05-22T16:04:46.732Z\"}","now":"2023-05-22T16:04:46.831635+00:00","sent_at":"2023-05-22T16:04:46.823000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} -{"path":"/e/?compression=gzip-js&ip=1&_=1684771486824&ver=1.57.2","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-05-22T16:04:46.833134+00:00","body":"H4sIAAAAAAAAA51U2W7cNhT9lUCw3yKbpKhtgKLw0sJN0hpB4qBwEAgUeaWRhyOqFKXxOMi/91IzmQWd9CEvWs65Ow/v568BjNC6YBacdaIGDWKE4HXQWdOBdQ30wexrcGbwFfwp5Kv7D6/+RhqBYgTbN6ZFgpILGl8Qj5fWrHqwCP7eWKjMswcVjI2Ewq07QOIW+oUznSfkYC0mLwarkZg7180uL7WRQs9N72YZIeTSQqfFGl8SLX+tGu0w7y/n6fU5Yz30voQCSWNV09aFGqxwCCF5Hl1trHze7T9jO1OPsBt8LmC9Yw/dJ3IUetg6J2QD+cEIZ+zOqXb+M73d0PvJbQ3i6/N4y02jPoEL6ZOeIJRwUFTWLHfJQkbVJp0foB8TTu54ZJ7ohJu3YunnfTzBg0PanyCl0QGsRVsPqAX0hTZ8+OBdemkB2mIOTT3HjHnK9+CqUW6OQWJCEBwbWHXGup1txn3wHfzdmkce1k2JaVZQ+iT4c6iqizi9YB5vWizLFY1CVKRYXTTq1Yq6l7KtPO8a3yhNMp6mlGfJRcbY60A1vWtaufWr39/e6e75n5u7+mExj/vo3f2nh/fxlfzL0cXykybP4714Yn2VLA8kO7nSDFsgEPEyjqQMiYpkIlOSc05jlYWcMsVkpEKa8JJk4d48UVHmgw1+qj9RRdMXCpamQEk9gcRRVkL3gAFra4Zuupk7KiDbvGFEEwhRBiSMqUjCSiU8iXEqOfEX29hatM3LpPJDL5aTfOPFFdAwz+M0ZYISHnNfSds70UoviZO3NPiGVYnBIdi5wUKBwxelBlVg63ikRd8odP5ev9f7iMIGMRlXWtTYzecvSB1iRSfW2gjlO8UEHWabm/pIIjya9o4FoZe+ODQBFUrdyMXc4NyRg6Vo9FSHP1Ex4p8vbVdNr4Vc/A9/JvFqGg2Fxtz7TQPtiQadHbzHf/fS5mt/547dgnHSOS5MwIXot+eZwvWJ57pDp+VmsBXf9551ZgEe6eayeLqTt0O6jB7H9o+2W5TPyzf27W/l+s2wWF893rys38I70n98vo4W05XeFrnTOI9YUqaKlpSGBNIyoRkXLMpiFPUpkW/sgYKaVs6qaZVZHYbjeSJ4GZIqTbmAnGc5y5MfxfLGMSMR3ayvGi8gHEcTKqYRjVCmSQm0opTJjPHqR/HQnFFIEq9NvyFQwcsOozHCopDEIWMfaTIjfManhfEYfPvyL6II7K8OBwAA","output":[{"uuid":"01884434-bc71-0000-a710-92ebd1e26e1f","distinct_id":"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"event\": \"$pageleave\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/replay/recent?filters=%7B%22session_recording_duration%22%3A%7B%22type%22%3A%22recording%22%2C%22key%22%3A%22duration%22%2C%22value%22%3A60%2C%22operator%22%3A%22gt%22%7D%2C%22properties%22%3A%5B%5D%2C%22events%22%3A%5B%5D%2C%22actions%22%3A%5B%5D%2C%22date_from%22%3A%22-21d%22%7D\", \"$host\": \"localhost:8000\", \"$pathname\": \"/replay/recent\", \"$browser_version\": 113, \"$browser_language\": \"en-US\", \"$screen_height\": 974, \"$screen_width\": 1500, \"$viewport_height\": 843, \"$viewport_width\": 1433, \"$lib\": \"web\", \"$lib_version\": \"1.57.2\", \"$insert_id\": \"a7ang3vlww1tzbnf\", \"$time\": 1684771486.822, \"distinct_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"$device_id\": \"188430e34b53cc-0d3c6c7094415d8-412d2c3d-164b08-188430e34b6d38\", \"$user_id\": \"gQDHlpxqCHgUkh5s3LOVUQ5AcNt1kmVl0xvOaj2sf6m\", \"is_demo_project\": false, \"$groups\": {\"project\": \"0188430e-316e-0000-51a6-fd646548690e\", \"organization\": \"0188430e-2909-0000-4de1-995772a10454\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"posthog_version\": \"1.43.0\", \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$console_log_recording_enabled_server_side\": true, \"$session_recording_recorder_version_server_side\": \"v2\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k\", \"$session_id\": \"18844326b7d1b11-0e7b6184a238508-412d2c3d-164b08-18844326b7e1ed0\", \"$window_id\": \"188443496a4b-0f774ae94892968-412d2c3d-164b08-188443496a52031\", \"$pageview_id\": \"1884434ad513139-06be1f112c824f-412d2c3d-164b08-1884434ad521e66\"}, \"timestamp\": \"2023-05-22T16:04:46.822Z\"}","now":"2023-05-22T16:04:46.833134+00:00","sent_at":"2023-05-22T16:04:46.824000+00:00","token":"phc_jHcDu7m3ZvnInpkbxmJrKEbyJukyAZCzyKeL0sTxB3k"}]} +{"path":"/e/?ip=1&_=1694769302325&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.328551+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l6TlMwM1l6WTJMVGd5TldVdE9HSXdaV1ZoWlRZMU56RTBJaXdpWlhabGJuUWlPaUlrYVdSbGJuUnBabmtpTENKd2NtOXdaWEowYVdWeklqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR1JsZG1salpWOTBlWEJsSWpvaVJHVnphM1J2Y0NJc0lpUmpkWEp5Wlc1MFgzVnliQ0k2SW1oMGRIQTZMeTlzYjJOaGJHaHZjM1E2T0RBd01DOGlMQ0lrYUc5emRDSTZJbXh2WTJGc2FHOXpkRG80TURBd0lpd2lKSEJoZEdodVlXMWxJam9pTHlJc0lpUmljbTkzYzJWeVgzWmxjbk5wYjI0aU9qRXhOeXdpSkdKeWIzZHpaWEpmYkdGdVozVmhaMlVpT2lKbGJpMVZVeUlzSWlSelkzSmxaVzVmYUdWcFoyaDBJam94TURVeUxDSWtjMk55WldWdVgzZHBaSFJvSWpveE5qSXdMQ0lrZG1sbGQzQnZjblJmYUdWcFoyaDBJam81TVRFc0lpUjJhV1YzY0c5eWRGOTNhV1IwYUNJNk1UVTBPQ3dpSkd4cFlpSTZJbmRsWWlJc0lpUnNhV0pmZG1WeWMybHZiaUk2SWpFdU56Z3VOU0lzSWlScGJuTmxjblJmYVdRaU9pSTBNSEIwTVhWamNHczNORFpwYkdWd0lpd2lKSFJwYldVaU9qRTJPVFEzTmprek1ESXVNekkxTENKa2FYTjBhVzVqZEY5cFpDSTZJbkJYUVd0SlZIbFJNME5PTnpNek1sVnhVWGh1U0Rad00wWldPRlpLWkRkd1dUWTBOMFZrVG10NFYyTWlMQ0lrWkdWMmFXTmxYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaQzAzT0dGbExUazBOMkl0WW1Wa1ltRmhNREpoTUdZMElpd2lKSFZ6WlhKZmFXUWlPaUp3VjBGclNWUjVVVE5EVGpjek16SlZjVkY0YmtnMmNETkdWamhXU21RM2NGazJORGRGWkU1cmVGZGpJaXdpSkhKbFptVnljbVZ5SWpvaUpHUnBjbVZqZENJc0lpUnlaV1psY25KcGJtZGZaRzl0WVdsdUlqb2lKR1JwY21WamRDSXNJaVJoYm05dVgyUnBjM1JwYm1OMFgybGtJam9pTURFNFlUazRNV1l0TkdJeVpDMDNPR0ZsTFRrME4ySXRZbVZrWW1GaE1ESmhNR1kwSWl3aWRHOXJaVzRpT2lKd2FHTmZjV2RWUm5BMWQzb3lRbXBETkZKelJtcE5SM0JSTTFCSFJISnphVFpSTUVOSE1FTlFORTVCWVdNd1NTSXNJaVJ6WlhOemFXOXVYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaUzAzWW1RekxXSmlNekF0TmpZeE4ySm1ORGc0T0RZNUlpd2lKSGRwYm1SdmQxOXBaQ0k2SWpBeE9HRTVPREZtTFRSaU1tVXROMkprTXkxaVlqTXdMVFkyTVRneE1HWmxaRFkxWmlKOUxDSWtjMlYwSWpwN2ZTd2lKSE5sZEY5dmJtTmxJanA3ZlN3aWRHbHRaWE4wWVcxd0lqb2lNakF5TXkwd09TMHhOVlF3T1RveE5Ub3dNaTR6TWpWYUluMCUzRA==","output":[{"uuid":"018a981f-4b35-7c66-825e-8b0eeae65714","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b35-7c66-825e-8b0eeae65714\", \"event\": \"$identify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"40pt1ucpk746ilep\", \"$time\": 1694769302.325, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$anon_distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"$set\": {}, \"$set_once\": {}, \"timestamp\": \"2023-09-15T09:15:02.325Z\"}","now":"2023-09-15T09:15:02.328551+00:00","sent_at":"2023-09-15T09:15:02.325000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?ip=1&_=1694769302319&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.322717+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l5WmkwM09URmhMVGxsWWpFdE5qZGhaVFZpT1dWalpqZzBJaXdpWlhabGJuUWlPaUlrY0dGblpYWnBaWGNpTENKd2NtOXdaWEowYVdWeklqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR1JsZG1salpWOTBlWEJsSWpvaVJHVnphM1J2Y0NJc0lpUmpkWEp5Wlc1MFgzVnliQ0k2SW1oMGRIQTZMeTlzYjJOaGJHaHZjM1E2T0RBd01DOGlMQ0lrYUc5emRDSTZJbXh2WTJGc2FHOXpkRG80TURBd0lpd2lKSEJoZEdodVlXMWxJam9pTHlJc0lpUmljbTkzYzJWeVgzWmxjbk5wYjI0aU9qRXhOeXdpSkdKeWIzZHpaWEpmYkdGdVozVmhaMlVpT2lKbGJpMVZVeUlzSWlSelkzSmxaVzVmYUdWcFoyaDBJam94TURVeUxDSWtjMk55WldWdVgzZHBaSFJvSWpveE5qSXdMQ0lrZG1sbGQzQnZjblJmYUdWcFoyaDBJam81TVRFc0lpUjJhV1YzY0c5eWRGOTNhV1IwYUNJNk1UVTBPQ3dpSkd4cFlpSTZJbmRsWWlJc0lpUnNhV0pmZG1WeWMybHZiaUk2SWpFdU56Z3VOU0lzSWlScGJuTmxjblJmYVdRaU9pSnpjSEozT0RVM2JHVnVOM0ZxTXpSMklpd2lKSFJwYldVaU9qRTJPVFEzTmprek1ESXVNekU1TENKa2FYTjBhVzVqZEY5cFpDSTZJakF4T0dFNU9ERm1MVFJpTW1RdE56aGhaUzA1TkRkaUxXSmxaR0poWVRBeVlUQm1OQ0lzSWlSa1pYWnBZMlZmYVdRaU9pSXdNVGhoT1RneFppMDBZakprTFRjNFlXVXRPVFEzWWkxaVpXUmlZV0V3TW1Fd1pqUWlMQ0lrY21WbVpYSnlaWElpT2lJa1pHbHlaV04wSWl3aUpISmxabVZ5Y21sdVoxOWtiMjFoYVc0aU9pSWtaR2x5WldOMElpd2lkR2wwYkdVaU9pSlFiM04wU0c5bklpd2lkRzlyWlc0aU9pSndhR05mY1dkVlJuQTFkM295UW1wRE5GSnpSbXBOUjNCUk0xQkhSSEp6YVRaUk1FTkhNRU5RTkU1QllXTXdTU0lzSWlSelpYTnphVzl1WDJsa0lqb2lNREU0WVRrNE1XWXROR0l5WlMwM1ltUXpMV0ppTXpBdE5qWXhOMkptTkRnNE9EWTVJaXdpSkhkcGJtUnZkMTlwWkNJNklqQXhPR0U1T0RGbUxUUmlNbVV0TjJKa015MWlZak13TFRZMk1UZ3hNR1psWkRZMVppSjlMQ0owYVcxbGMzUmhiWEFpT2lJeU1ESXpMVEE1TFRFMVZEQTVPakUxT2pBeUxqTXhPVm9pZlElM0QlM0Q=","output":[{"uuid":"018a981f-4b2f-791a-9eb1-67ae5b9ecf84","distinct_id":"018a981f-4b2d-78ae-947b-bedbaa02a0f4","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b2f-791a-9eb1-67ae5b9ecf84\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"sprw857len7qj34v\", \"$time\": 1694769302.319, \"distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"title\": \"PostHog\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"timestamp\": \"2023-09-15T09:15:02.319Z\"}","now":"2023-09-15T09:15:02.322717+00:00","sent_at":"2023-09-15T09:15:02.319000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?ip=1&_=1694769302319&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.321230+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l5WlMwM1ltUXpMV0ppTXpBdE5qWXhOalkxTjJNek9HWmhJaXdpWlhabGJuUWlPaUlrYjNCMFgybHVJaXdpY0hKdmNHVnlkR2xsY3lJNmV5SWtiM01pT2lKTllXTWdUMU1nV0NJc0lpUnZjMTkyWlhKemFXOXVJam9pTVRBdU1UVXVNQ0lzSWlSaWNtOTNjMlZ5SWpvaVJtbHlaV1p2ZUNJc0lpUmtaWFpwWTJWZmRIbHdaU0k2SWtSbGMydDBiM0FpTENJa1kzVnljbVZ1ZEY5MWNtd2lPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPamd3TURBdklpd2lKR2h2YzNRaU9pSnNiMk5oYkdodmMzUTZPREF3TUNJc0lpUndZWFJvYm1GdFpTSTZJaThpTENJa1luSnZkM05sY2w5MlpYSnphVzl1SWpveE1UY3NJaVJpY205M2MyVnlYMnhoYm1kMVlXZGxJam9pWlc0dFZWTWlMQ0lrYzJOeVpXVnVYMmhsYVdkb2RDSTZNVEExTWl3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRZeU1Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPVEV4TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFMU5EZ3NJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpjNExqVWlMQ0lrYVc1elpYSjBYMmxrSWpvaU1XVTRaV1p5WkdSbE1HSTBNV2RtYkNJc0lpUjBhVzFsSWpveE5qazBOelk1TXpBeUxqTXhPQ3dpWkdsemRHbHVZM1JmYVdRaU9pSXdNVGhoT1RneFppMDBZakprTFRjNFlXVXRPVFEzWWkxaVpXUmlZV0V3TW1Fd1pqUWlMQ0lrWkdWMmFXTmxYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaQzAzT0dGbExUazBOMkl0WW1Wa1ltRmhNREpoTUdZMElpd2lKSEpsWm1WeWNtVnlJam9pSkdScGNtVmpkQ0lzSWlSeVpXWmxjbkpwYm1kZlpHOXRZV2x1SWpvaUpHUnBjbVZqZENJc0luUnZhMlZ1SWpvaWNHaGpYM0ZuVlVad05YZDZNa0pxUXpSU2MwWnFUVWR3VVROUVIwUnljMmsyVVRCRFJ6QkRVRFJPUVdGak1Fa2lMQ0lrYzJWemMybHZibDlwWkNJNklqQXhPR0U1T0RGbUxUUmlNbVV0TjJKa015MWlZak13TFRZMk1UZGlaalE0T0RnMk9TSXNJaVIzYVc1a2IzZGZhV1FpT2lJd01UaGhPVGd4WmkwMFlqSmxMVGRpWkRNdFltSXpNQzAyTmpFNE1UQm1aV1EyTldZaWZTd2lkR2x0WlhOMFlXMXdJam9pTWpBeU15MHdPUzB4TlZRd09Ub3hOVG93TWk0ek1UaGFJbjAlM0Q=","output":[{"uuid":"018a981f-4b2e-7bd3-bb30-6616657c38fa","distinct_id":"018a981f-4b2d-78ae-947b-bedbaa02a0f4","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b2e-7bd3-bb30-6616657c38fa\", \"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"1e8efrdde0b41gfl\", \"$time\": 1694769302.318, \"distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"timestamp\": \"2023-09-15T09:15:02.318Z\"}","now":"2023-09-15T09:15:02.321230+00:00","sent_at":"2023-09-15T09:15:02.319000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769305355&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:05.358129+00:00","body":"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","output":[{"uuid":"018a981f-4b35-7c66-825e-8b0fb6d0406d","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b35-7c66-825e-8b0fb6d0406d\", \"event\": \"$set\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"v1wz6rl7mwzx5hn7\", \"$time\": 1694769302.325, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$set\": {\"email\": \"xavier@posthog.com\"}, \"$set_once\": {}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 3026}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b36-7798-a4ce-f3cb0e052fd3","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b36-7798-a4ce-f3cb0e052fd3\", \"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"zgsdh9ltk0051fig\", \"$time\": 1694769302.326, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\"}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"project\", \"$group_key\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"$group_set\": {\"id\": 1, \"uuid\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"name\": \"Default Project\", \"ingested_event\": false, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 3026}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b36-7798-a4ce-f3cc42d530ac","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b36-7798-a4ce-f3cc42d530ac\", \"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"h1e1lbzy5qglp9in\", \"$time\": 1694769302.326, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"organization\", \"$group_key\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"$group_set\": {\"id\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"name\": \"X\", \"slug\": \"x\", \"created_at\": \"2023-09-15T09:14:40.355611Z\", \"available_features\": [], \"instance_tag\": \"none\"}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 3026}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b4f-7cfd-be2b-71853c3ea63e","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b4f-7cfd-be2b-71853c3ea63e\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"i1h7rrk825f6dzpx\", \"$time\": 1694769302.351, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"PostHog\"}, \"offset\": 3001}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b53-7336-acab-5dedd8d0bc31","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b53-7336-acab-5dedd8d0bc31\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"2ca5nelxe3pnc5u7\", \"$time\": 1694769302.355, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"posthog-3000\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2996}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b55-710c-b2de-daadad208d1d","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b55-710c-b2de-daadad208d1d\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"4jxul46uknv3lils\", \"$time\": 1694769302.357, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"enable-prompts\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2995}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b57-7be4-9d6a-4e0c9cec9e59","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b57-7be4-9d6a-4e0c9cec9e59\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"yr718381rj33ace5\", \"$time\": 1694769302.359, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"early-access-feature\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2993}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b57-7be4-9d6a-4e0d1fd7807e","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b57-7be4-9d6a-4e0d1fd7807e\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"qtsk6vnwbc4z8wxk\", \"$time\": 1694769302.359, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"surveys\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2992}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b58-7c64-a5b5-1a0d736ecb3d","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b58-7c64-a5b5-1a0d736ecb3d\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"xleie3rii515xshs\", \"$time\": 1694769302.36, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"data-warehouse\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2992}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b58-7c64-a5b5-1a0e3373e1d1","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b58-7c64-a5b5-1a0e3373e1d1\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"mzvpi0oqok5sdsi7\", \"$time\": 1694769302.36, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"feedback-scene\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2992}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b59-7cbb-9e7e-9ab6d22f0016","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b59-7cbb-9e7e-9ab6d22f0016\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"qib1d9bxeezlwxlh\", \"$time\": 1694769302.361, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"notebooks\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2991}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b6e-73e8-a868-0d42a82c2114","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b6e-73e8-a868-0d42a82c2114\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"r1c1s558txtqnd22\", \"$time\": 1694769302.382, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"onboarding-v2-demo\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2970}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4b87-7b8f-8061-c9ea4fd0c2d9","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b87-7b8f-8061-c9ea4fd0c2d9\", \"event\": \"ingestion landing seen\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"qquqyq7yl5w32rq8\", \"$time\": 1694769302.408, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2944}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-4ba9-7223-968c-d2989f231c1a","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4ba9-7223-968c-d2989f231c1a\", \"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"nd8jaiiwa9dg02pf\", \"$time\": 1694769302.442, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"instance\", \"$group_key\": \"http://localhost:8000\", \"$group_set\": {\"site_url\": \"http://localhost:8000\"}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2910}","now":"2023-09-15T09:15:05.358129+00:00","sent_at":"2023-09-15T09:15:05.355000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769308363&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:08.365426+00:00","body":"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","output":[{"uuid":"018a981f-5bd2-735a-b74b-65f538d7536a","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-5bd2-735a-b74b-65f538d7536a\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"eet6doxfgbi6510a\", \"$time\": 1694769306.58, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"p\", \"classes\": [\"font-normal\", \"text-xs\"], \"attr__class\": \"font-normal text-xs\", \"nth_child\": 2, \"nth_of_type\": 2, \"$el_text\": \"Available for JavaScript, Android, iOS, React Native, Node.js, Ruby, Go, and more.\"}, {\"tag_name\": \"div\", \"classes\": [\"mt-4\", \"mb-0\"], \"attr__class\": \"mt-4 mb-0\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--has-side-icon\", \"mb-4\"], \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--has-side-icon mb-4\", \"attr__type\": \"button\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"mb-6\"], \"attr__class\": \"flex flex-col mb-6\", \"nth_child\": 4, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1779}","now":"2023-09-15T09:15:08.365426+00:00","sent_at":"2023-09-15T09:15:08.363000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-5bd9-7cb4-9fd5-5e574bb918eb","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-5bd9-7cb4-9fd5-5e574bb918eb\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/platform\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/platform\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ilfvutq6qvqh58ow\", \"$time\": 1694769306.585, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Ingestion wizard \\u2022 PostHog\"}, \"offset\": 1774}","now":"2023-09-15T09:15:08.365426+00:00","sent_at":"2023-09-15T09:15:08.363000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-5e75-7a07-a79f-1eee24012718","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-5e75-7a07-a79f-1eee24012718\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/platform\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/platform\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"37x61aqqtn9k28a5\", \"$time\": 1694769307.254, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"mobile\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"attr__type\": \"button\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"mb-6\"], \"attr__class\": \"flex flex-col mb-6\", \"nth_child\": 4, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1105}","now":"2023-09-15T09:15:08.365426+00:00","sent_at":"2023-09-15T09:15:08.363000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-5e79-7f17-be1a-8b450229830f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-5e79-7f17-be1a-8b450229830f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/mobile\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/mobile\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"oawqrgmuo369cqvg\", \"$time\": 1694769307.257, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Ingestion wizard \\u2022 PostHog\"}, \"offset\": 1102}","now":"2023-09-15T09:15:08.365426+00:00","sent_at":"2023-09-15T09:15:08.363000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-60ea-7698-b26c-b3735fa37bd8","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-60ea-7698-b26c-b3735fa37bd8\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/mobile\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/mobile\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"017vn18mgnnd0rss\", \"$time\": 1694769307.883, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"React Native\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"attr__type\": \"button\", \"attr__data-attr\": \"select-framework-REACT_NATIVE\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"FrameworkPanel\"], \"attr__class\": \"FrameworkPanel\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 476}","now":"2023-09-15T09:15:08.365426+00:00","sent_at":"2023-09-15T09:15:08.363000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-60ee-7e1a-aadf-8011aaa47a22","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-60ee-7e1a-aadf-8011aaa47a22\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/mobile/react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/mobile/react_native\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"2w8r5rse14eqrg7f\", \"$time\": 1694769307.886, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Ingestion wizard \\u2022 PostHog\"}, \"offset\": 473}","now":"2023-09-15T09:15:08.365426+00:00","sent_at":"2023-09-15T09:15:08.363000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769311367&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:11.372150+00:00","body":"H4sIAAAAAAAAA+1XW1PbRhT+KxoNj1mQrKvpUyAlpdOk0DRJO5mMZqU9shYkrSKtbAjDf+858gXJNiYFXjLDk61z2fOd6579cmO2rRTmoWnZIR+Hdsr8cMRZYLmCjceew8C1wxAciF0rNV+ZMIVSo/geb7VKeKXbGpBc1aqCWktozMMbc0/hj/mOJ8afH4x/kI2EaAp1I1WJDNvat719i+hxrWYN1Eg8kTWk6oqIAqYygUhfV4CMN9BcalURI2nrGs1HbZ0jI9O6Ojw4yBFGnqlGH4aWZR3IcgKNRkMHhYplDgc18ERHJddySkj3SBS1h2rEqLjOSl6QzYdPWQC/88q2gx455+Wk5RM6C0r28QOpNEkNUEYZyEmGCGzLG91RZ1LoDIn+yELiVMKsUrVeCY9tu09eSntuiORcxmhnBjFZwY9+qPeDcN8juiwRl466XPtpkI7TYppdX6RZk02JryV5bvtjN/DHjjXeD5zxK1NIjEKJnnd61efXl6d/X587x+8Dxxl9/HZ+Vf7mV87Jp/DT7yKo/vXd4Ffx/vLqc9LL47C83HgkWBByYGgpZjGImHNrxK3UJZ2WovcIY7KJBBQqwkK8gAQjlvK8ATxwUqu26qpyxVqCAZaG8Yhh+i0mPMCCd0ILwONcuBRLVU94Kb9jzrtY3ml5XjLX4qEn2MhLbA88zxOeICRlo3mZUOq3Vqh5i6h63RNhjHmcg4jQdcxc1EiBykv8WHZYclEKvBNOcz5Bb758RVafFlX8OldckKdoAMs1LwgBWkXHklwml5nC4FILF1zmnTHKDp/iF9lfmWxynlzu4O9howI2InXtnsC2xZiuqNg4kVBogSLW43ZzY9nSHZyuoaHXQCSVQ4Fy5OCNqdGrRT82FS9RPsl509CI+WL+gckuj1qtVRlFiSo1jSUMCte6RgIJot5WqVdmqbMoyWSOZYZW6UulC2xzFJGGKyqU03IqNRjc0MALo4AihtrQysggr4yZ1JmhM9kYGOMK0zqAHHdWO9cfddx93iKj98WYprHL6+t1OhahbhtW1bLYws15jcNpjZi2ec7mo2WNk2DcoAYq7wE94w2jcmUSg4vMQjN/ZxaMrdiH1CHyIa/DPSTdoR7Sl5iH1AFio8O7hLsozlXielWCg3pQJaO1ZAtJM/R/K/QyjAUOOUuVQsybERxwd9fvQ8hwqA8U3K0Kc/ONvqa+Nwt+NY/woYETrLr65TEYes6e4oisWxxsqmzOyDUqnZiFm45vSBqd3NPMH9VSTOAMr+cdo2Ob0NNSvOVENqt5hYvTD5lfCT+f992c3mm7k3gug0g/XS5Wx6uYZl0L74JhrGsZC52nJSTNgfbN++wT+1kN0Q9LFJ2WsfneR1sG3oBprmYsk0IgZTuOpS4Cmmsa63pPSxLlmWF5oY1lXtZJjFX51npZFzTu03waxA84to842b2vZzckBgbdH5p8mwaRuPiH18c9OVoIGOuCT/O4VyXrJbSBYFUXg3J5DID5ud0OXuONMzzCeeCIWAlaNhY3aobrHL21ukfMzh6yb9EjrS7RWbzusiT6Nvl4Unmz76Oji2P3r+bk4t3b6tw5e/sG10X/3Dp+ax2fue9f88Q67R5X0NAaufnYABbEwmFx7FjM9+0gTt0wDP0xKc1kKdTsQZ3QtlIQvpfS6q7StAF6vvkjn3zf8nzG9w0PAsaTUDBuBUnMExGMfbq2ls9nOd8E5ytgY8x3DqPbi7st6+U9/TO9p4M6m1qz70kynUGbTyfb39Muwnt5T7+8p5/jPf2TTUrn9ut/K0nIYGoUAAA=","output":[{"uuid":"018a981f-682a-704d-9953-e4188e3eb40f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-682a-704d-9953-e4188e3eb40f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/mobile/react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/mobile/react_native\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"6f7f9fmvhyjfhshv\", \"$time\": 1694769309.739, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Invite a team member to help with this step\"}, {\"tag_name\": \"button\", \"$el_text\": \"Invite a team member to help with this step\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"LemonButton--has-side-icon\", \"mt-6\"], \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered LemonButton--has-side-icon mt-6\", \"attr__type\": \"button\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"panel-footer\"], \"attr__class\": \"panel-footer\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 9, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"InstructionsPanel\", \"mb-8\"], \"attr__class\": \"InstructionsPanel mb-8\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1626}","now":"2023-09-15T09:15:11.372150+00:00","sent_at":"2023-09-15T09:15:11.367000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-682d-7a77-ac8d-a07cbacd7968","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-682d-7a77-ac8d-a07cbacd7968\", \"event\": \"invite members button clicked\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/mobile/react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/mobile/react_native\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"7rhv0wzccvweulvg\", \"$time\": 1694769309.742, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1623}","now":"2023-09-15T09:15:11.372150+00:00","sent_at":"2023-09-15T09:15:11.367000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769314370&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:14.373644+00:00","body":"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","output":[{"uuid":"018a981f-6ecc-7851-ae7f-be5bd1d53d6f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-6ecc-7851-ae7f-be5bd1d53d6f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/mobile/react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/mobile/react_native\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"draysluz0s4jwg2f\", \"$time\": 1694769311.437, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"svg\", \"classes\": [\"LemonIcon\"], \"attr__class\": \"LemonIcon\", \"attr__width\": \"1em\", \"attr__height\": \"1em\", \"attr__fill\": \"none\", \"attr__viewBox\": \"0 0 24 24\", \"attr__xmlns\": \"http://www.w3.org/2000/svg\", \"attr__focusable\": \"false\", \"attr__aria-hidden\": \"true\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__icon\"], \"attr__class\": \"LemonButton__icon\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--small\", \"LemonButton--no-content\", \"LemonButton--has-icon\"], \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--small LemonButton--no-content LemonButton--has-icon\", \"attr__type\": \"button\", \"attr__aria-label\": \"close\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonModal__close\"], \"attr__class\": \"LemonModal__close\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonModal__container\"], \"attr__class\": \"LemonModal__container\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ReactModal__Content\", \"ReactModal__Content--after-open\", \"LemonModal\"], \"attr__style\": \"width: 800px;\", \"attr__class\": \"ReactModal__Content ReactModal__Content--after-open LemonModal\", \"attr__tabindex\": \"-1\", \"attr__role\": \"dialog\", \"attr__aria-modal\": \"true\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ReactModal__Overlay\", \"ReactModal__Overlay--after-open\", \"LemonModal__overlay\"], \"attr__class\": \"ReactModal__Overlay ReactModal__Overlay--after-open LemonModal__overlay\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"ReactModalPortal\"], \"attr__class\": \"ReactModalPortal\", \"nth_child\": 6, \"nth_of_type\": 3}, {\"tag_name\": \"body\", \"classes\": [\"ReactModal__Body--open\"], \"attr__theme\": \"light\", \"attr__class\": \"ReactModal__Body--open\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2931}","now":"2023-09-15T09:15:14.373644+00:00","sent_at":"2023-09-15T09:15:14.370000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-71d0-7347-82a5-d2d2bda4ec45","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-71d0-7347-82a5-d2d2bda4ec45\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/mobile/react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/mobile/react_native\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"umn253uxubh0ewgt\", \"$time\": 1694769312.208, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"Continue\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"attr__type\": \"button\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"panel-footer\"], \"attr__class\": \"panel-footer\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 9, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"InstructionsPanel\", \"mb-8\"], \"attr__class\": \"InstructionsPanel mb-8\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2160}","now":"2023-09-15T09:15:14.373644+00:00","sent_at":"2023-09-15T09:15:14.370000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-71d3-7016-aa90-636d1a0c436f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-71d3-7016-aa90-636d1a0c436f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/verify?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/verify\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"zrmm7vg964aytvon\", \"$time\": 1694769312.212, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Ingestion wizard \\u2022 PostHog\"}, \"offset\": 2156}","now":"2023-09-15T09:15:14.373644+00:00","sent_at":"2023-09-15T09:15:14.370000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-79f4-7425-a1d3-9319851b0f8b","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-79f4-7425-a1d3-9319851b0f8b\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/verify?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/verify\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"t2dicfb3gnjl2dro\", \"$time\": 1694769314.293, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"or continue without verifying\"}, {\"tag_name\": \"button\", \"$el_text\": \"or continue without verifying\", \"classes\": [\"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-primary\", \"LemonButton--full-width\", \"LemonButton--centered\"], \"attr__class\": \"LemonButton LemonButton--tertiary LemonButton--status-primary LemonButton--full-width LemonButton--centered\", \"attr__type\": \"button\", \"nth_child\": 5, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"ingestion-listening-for-events\"], \"attr__class\": \"ingestion-listening-for-events\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"text-center\"], \"attr__class\": \"text-center\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 75}","now":"2023-09-15T09:15:14.373644+00:00","sent_at":"2023-09-15T09:15:14.370000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-79fc-7fe6-8aad-eadda9657a13","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-79fc-7fe6-8aad-eadda9657a13\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"xdmxcm4pq22gxv2k\", \"$time\": 1694769314.3, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Ingestion wizard \\u2022 PostHog\"}, \"offset\": 68}","now":"2023-09-15T09:15:14.373644+00:00","sent_at":"2023-09-15T09:15:14.370000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-79fd-761a-92e8-fd36e1159302","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-79fd-761a-92e8-fd36e1159302\", \"event\": \"ingestion continue without verifying\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"1nexbjim0008j2qy\", \"$time\": 1694769314.301, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 67}","now":"2023-09-15T09:15:14.373644+00:00","sent_at":"2023-09-15T09:15:14.370000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769317374&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:17.382070+00:00","body":"H4sIAAAAAAAAA+1X627bNhR+FUMI9qt0JOueoRiadMkyrFmyLO2GoBAokbIY06JCUZadoO++Q/kS+RKnSzJgW/3L1rl+50byXN8bVcWIcWCYVoDDwEpRYJIY+ZZtoSAkBKUBdpMU2z03MY03Bh3RXIH4Hq6USHChKkmBXEhRUKkYLY2De2NPwI/xASedXy87fwAbCNGIypKJHBiW2bXcrra2F0tRl1QC8ZhJmoqxJhI6YgmN1KSgwHhPy4EShWYklZTgPqokB0amVHGwv88BBs9EqQ4C0zT3Wd6npQJH+2UFkApRg98fCo5VKuTw7VDEjNPvUomHtBZy8FZSnKgox4qNdCB72hIYX7aqGQVWWQ5awNzspBXPQ7CW5bfIHOf9Cve1DZqjq0utUiaS0jzKKOtn4Nky3d4DtWZEZUD0eiYQR4zWhZBqIRxaVps8l3adAMicxeCnprH2Ah/tCnT9oOtqOssBl4qaFrDDO280roY9yUzlxDear5iO2PJCx/dC2/K6phu+MQiD6HPIW6NXfHo3OP19cmEfnfm23bu6vRjnP3mFffwx+Pgz8Ys/Pcf/kZwNxp+SVnmXu86JewT5AaYIPMUopiTG2OxhM3W0TqWz9wxnrIwIHYoI+vOGJpCxFPOSgsG+FFXRNOuCNQdDoefjHoKym4i4lKDQDkxKXYyJo3MpZB/n7A46psnlg5brJlMtHLgEwcBYLnVdl7hEI8lLhfNEl35j4xpfAFVrqCLIMY45JRGEDpWLSkZAeY4fmhYaNkopboRTjvsQzfVnYLVpUYEnXGCiIwUH0Ox8qBGAVwgs4SwZZAKSqyd7iBlvnOnq4BF8af8LlyXHyWALfw/ml8J86mHeIzDNkNMFFQYmIgI86Iy1uM1xMp/0Bk4z57Q1QFqK0yHI6QDvDQVRzeaQsBGIJxyXpT54ro1foNb5Zc1UkkVRhnMC+CAlWCkZRY0caG0SemPkKouSjHHoMZg//SXSGTA9j5RHio51l0ChlkDElVIAdFnmcVQz8QWqpqkhPpGjshFB+rTZgnjhbyojha6BMdVdUAlWGOm/wBKFQgys01JnFEHqhSRQkJm/7bFbK9FuSzkwWl8IJRlNBlQ3/xI5rThH06NqW206m2wtE1uWloKAnvl7QQwnKFgH01C3d8ZGw1MrpZo0lRni8RTjQQfmvBh//0Ksh5KRPj2HWwSQilzp+3gN+iahZ0Sy3S2qJS7g+vsq9wvh14u+OU62+m4kXssh0E/n9/7RIqdZ04XbYHRWtToznZcVJOVUv5Ye86/Zr+pI/6BEaGsZmj5P9GUIB3XKRY0yRghQNuOY6wKgqWZnVe+F8wt1RtBe4GNel1USQvAG3NQvq4KdxzRfBvESru9DrP0+NrNrEksOnRWHzlc6BOLsH3q0RjOBzqrgyyJudclqC60hWPTFUrs8B0DrVpVCrCTRfsJELMhkYUNl8OrQl3Pz1l5F/NS1CSEqMYDo4cmaJdFt/+q4cOu73uHNkfNbeXzz4aS4sM9P3sMzx7swj07Mo3Pn7B1OzNNmKZhe1uuPZIr8mNgojm0TeZ7lx6kTBIEXaqWa5UTUT+oElplS4rmpfnKKNC2pXjtsq0nG2jbo4AD5vTBGAbYwCggJ4YEdY+Knu23wf70N3kLeLdtzxze15RB/0zbod00b4O22wd02+C/YBl99EbP+mUWsVdZvfQXbcjXbu9Vqt1rtVqvdarVbrXar1X96tbLt8MvnvwD69vHlaRsAAA==","output":[{"uuid":"018a981f-80db-7131-89dd-f8a5cfa325c0","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-80db-7131-89dd-f8a5cfa325c0\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"39z6vxum2ri0t4bj\", \"$time\": 1694769316.059, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"div\", \"classes\": [\"LemonSwitch__handle\"], \"attr__class\": \"LemonSwitch__handle\", \"nth_child\": 2, \"nth_of_type\": 2, \"$el_text\": \"\"}, {\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonSwitch__button\"], \"attr__id\": \"lemon-switch-0\", \"attr__class\": \"LemonSwitch__button\", \"attr__role\": \"switch\", \"attr__data-attr\": \"opt-in-session-recording-switch\", \"nth_child\": 2, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonSwitch\", \"LemonSwitch--checked\", \"LemonSwitch--full-width\"], \"attr__class\": \"LemonSwitch LemonSwitch--checked LemonSwitch--full-width\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"my-8\"], \"attr__class\": \"my-8\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1311}","now":"2023-09-15T09:15:17.382070+00:00","sent_at":"2023-09-15T09:15:17.374000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-84a8-729b-8a1a-8dd9647bad7f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-84a8-729b-8a1a-8dd9647bad7f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"qevi1365xjw14d7j\", \"$time\": 1694769317.032, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonSwitch__button\"], \"attr__id\": \"lemon-switch-1\", \"attr__class\": \"LemonSwitch__button\", \"attr__role\": \"switch\", \"attr__data-attr\": \"opt-in-autocapture-switch\", \"nth_child\": 2, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonSwitch\", \"LemonSwitch--checked\", \"LemonSwitch--full-width\"], \"attr__class\": \"LemonSwitch LemonSwitch--checked LemonSwitch--full-width\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 339}","now":"2023-09-15T09:15:17.382070+00:00","sent_at":"2023-09-15T09:15:17.374000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769320381&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:20.384514+00:00","body":"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","output":[{"uuid":"018a981f-8658-701c-88bf-f7bbb67f5787","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8658-701c-88bf-f7bbb67f5787\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"wnc1ly33pill989t\", \"$time\": 1694769317.464, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"\", \"classes\": [\"LemonSwitch__button\"], \"attr__id\": \"lemon-switch-1\", \"attr__class\": \"LemonSwitch__button\", \"attr__role\": \"switch\", \"attr__data-attr\": \"opt-in-autocapture-switch\", \"nth_child\": 2, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonSwitch\", \"LemonSwitch--full-width\"], \"attr__class\": \"LemonSwitch LemonSwitch--full-width\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2912}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-88be-7843-a2eb-9b3f8d6824c0","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-88be-7843-a2eb-9b3f8d6824c0\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"mf15pfzjumoijl97\", \"$time\": 1694769318.079, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"button\", \"$el_text\": \"Complete\", \"classes\": [\"LemonButton\", \"LemonButton--primary\", \"LemonButton--status-primary\", \"LemonButton--large\", \"LemonButton--full-width\", \"LemonButton--centered\", \"mb-2\"], \"attr__class\": \"LemonButton LemonButton--primary LemonButton--status-primary LemonButton--large LemonButton--full-width LemonButton--centered mb-2\", \"attr__type\": \"button\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"panel-footer\"], \"attr__class\": \"panel-footer\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"attr__style\": \"max-width: 800px;\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content\"], \"attr__class\": \"BridgePage__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__content-wrapper\"], \"attr__class\": \"BridgePage__content-wrapper\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage__main\"], \"attr__class\": \"BridgePage__main\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"BridgePage\", \"IngestionContent\", \"h-full\"], \"attr__class\": \"BridgePage IngestionContent h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"h-full\"], \"attr__class\": \"flex h-full\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"div\", \"classes\": [\"flex\", \"flex-col\", \"h-screen\", \"overflow-hidden\"], \"attr__class\": \"flex flex-col h-screen overflow-hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\", \"main-app-content--plain\"], \"attr__class\": \"main-app-content main-app-content--plain\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\", \"SideBar--hidden\"], \"attr__class\": \"SideBar SideBar--hidden\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2297}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-88c2-7b27-a725-5dacc56a1faa","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-88c2-7b27-a725-5dacc56a1faa\", \"event\": \"ingestion recordings turned off\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"palw1yb272630vgl\", \"$time\": 1694769318.082, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"session_recording_opt_in\": false, \"capture_console_log_opt_in\": false, \"capture_performance_opt_in\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2294}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-892b-73d6-8506-87999ee6c734","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-892b-73d6-8506-87999ee6c734\", \"event\": \"session_recording_opt_in team setting updated\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"hhhbbwdo7md9nbx1\", \"$time\": 1694769318.188, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"setting\": \"session_recording_opt_in\", \"value\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2188}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-892c-7c4e-a39c-794d54327278","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-892c-7c4e-a39c-794d54327278\", \"event\": \"capture_console_log_opt_in team setting updated\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"xox7cppwccqiijuc\", \"$time\": 1694769318.189, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"setting\": \"capture_console_log_opt_in\", \"value\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2187}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-892d-764e-8a29-511a81f29cbb","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-892d-764e-8a29-511a81f29cbb\", \"event\": \"capture_performance_opt_in team setting updated\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"9o2gsvxhpdfeec3h\", \"$time\": 1694769318.189, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"setting\": \"capture_performance_opt_in\", \"value\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2187}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-892e-77b6-b20f-b70f443ad14c","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-892e-77b6-b20f-b70f443ad14c\", \"event\": \"autocapture_opt_out team setting updated\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"7qm17fs8p0isd4oh\", \"$time\": 1694769318.19, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"setting\": \"autocapture_opt_out\", \"value\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2186}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-8946-75cd-a9ff-648d1207ff84","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8946-75cd-a9ff-648d1207ff84\", \"event\": \"completed_snippet_onboarding team setting updated\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/ingestion/superpowers?platform=mobile&framework=react_native\", \"$host\": \"localhost:8000\", \"$pathname\": \"/ingestion/superpowers\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"h4a6c8vyw3srmhbb\", \"$time\": 1694769318.215, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"setting\": \"completed_snippet_onboarding\", \"value\": true, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2161}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-8957-7215-bd8f-044c4fcf66d6","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8957-7215-bd8f-044c4fcf66d6\", \"event\": \"activation sidebar shown\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"jzurulh5u74oa367\", \"$time\": 1694769318.231, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"active_tasks_count\": 5, \"completed_tasks_count\": 2, \"completion_percent_count\": 29, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2145}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-896f-7d14-9381-5569293727c4","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-896f-7d14-9381-5569293727c4\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"4nliqvggjee1poz5\", \"$time\": 1694769318.256, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"cloud-announcement\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2120}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-8987-7364-810e-2d4c739c8102","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8987-7364-810e-2d4c739c8102\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"9ika8zb1mp9ogna8\", \"$time\": 1694769318.279, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"hogql-insights\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2097}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-8988-7ac8-b724-f40901aa8d06","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8988-7ac8-b724-f40901aa8d06\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"lsnvyrgxdnolgzgk\", \"$time\": 1694769318.28, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"query_running_time\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2096}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-8988-7ac8-b724-f40aafb78a72","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8988-7ac8-b724-f40aafb78a72\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"07i9wfbicoqcd79c\", \"$time\": 1694769318.28, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"query-timings\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2096}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-89ab-7a48-b670-bb2e0f756c5c","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-89ab-7a48-b670-bb2e0f756c5c\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"bmy5wgq2py5nr7sn\", \"$time\": 1694769318.316, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Event Explorer \\u2022 PostHog\"}, \"offset\": 2060}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-89d5-7f6a-8662-5e1c7a900226","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-89d5-7f6a-8662-5e1c7a900226\", \"event\": \"$set\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"11ek5qp3ka91h4on\", \"$time\": 1694769318.358, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$set\": {\"email\": \"xavier@posthog.com\"}, \"$set_once\": {}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2018}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-89d6-72b4-a717-d4c3033e8ac9","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-89d6-72b4-a717-d4c3033e8ac9\", \"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"m1irapsxiiixr3aw\", \"$time\": 1694769318.358, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"project\", \"$group_key\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"$group_set\": {\"id\": 1, \"uuid\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2018}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-89d6-72b4-a717-d4c40bff477e","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-89d6-72b4-a717-d4c40bff477e\", \"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"gqp8nywwqxzgj0te\", \"$time\": 1694769318.358, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"organization\", \"$group_key\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"$group_set\": {\"id\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"name\": \"X\", \"slug\": \"x\", \"created_at\": \"2023-09-15T09:14:40.355611Z\", \"available_features\": [], \"instance_tag\": \"none\"}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2018}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-89ea-7155-85cd-bab752a3eb57","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-89ea-7155-85cd-bab752a3eb57\", \"event\": \"$set\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0zm3r6g4ycnyghx3\", \"$time\": 1694769318.378, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$set\": {\"email\": \"xavier@posthog.com\"}, \"$set_once\": {}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1998}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-89ea-7155-85cd-bab8418f9641","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-89ea-7155-85cd-bab8418f9641\", \"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"p3taqjp0mpm4b61v\", \"$time\": 1694769318.378, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"project\", \"$group_key\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"$group_set\": {\"id\": 1, \"uuid\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"name\": \"Default Project\", \"ingested_event\": true, \"is_demo\": false, \"timezone\": \"UTC\", \"instance_tag\": \"none\"}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1998}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-89ea-7155-85cd-bab923f3d281","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-89ea-7155-85cd-bab923f3d281\", \"event\": \"$groupidentify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"p9s9cpaen4oa9vkm\", \"$time\": 1694769318.378, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$group_type\": \"organization\", \"$group_key\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"$group_set\": {\"id\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"name\": \"X\", \"slug\": \"x\", \"created_at\": \"2023-09-15T09:14:40.355611Z\", \"available_features\": [], \"instance_tag\": \"none\"}, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1998}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-8a99-7726-bb09-e5117809ae5c","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8a99-7726-bb09-e5117809ae5c\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"kcb99ofy2tm7p2dh\", \"$time\": 1694769318.553, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"EventsQuery\", \"select\": [\"*\", \"event\", \"person\", \"coalesce(properties.$current_url, properties.$screen_name) -- Url / Screen\", \"properties.$lib\", \"timestamp\"], \"orderBy\": [\"timestamp DESC\"], \"after\": \"-24h\"}, \"duration\": 274, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1823}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-8fd0-7117-a99f-9706b3cd1ee6","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-8fd0-7117-a99f-9706b3cd1ee6\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/events?onboarding_completed=true\", \"$host\": \"localhost:8000\", \"$pathname\": \"/events\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"y7z2v0ned8dk7uws\", \"$time\": 1694769319.888, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"a\", \"$el_text\": \"Persons & Groups\", \"classes\": [\"Link\", \"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__class\": \"Link LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__href\": \"/persons\", \"attr__data-attr\": \"menu-item-persons\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 12, \"nth_of_type\": 9}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider__content\"], \"attr__class\": \"SideBar__slider__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 487}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9078-7225-8884-0281e137c9ec","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9078-7225-8884-0281e137c9ec\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"491y0ux93px6ejqi\", \"$time\": 1694769320.056, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Persons & Groups \\u2022 PostHog\"}, \"offset\": 320}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-90cd-72c2-ba7e-e8ee82c2fae8","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-90cd-72c2-ba7e-e8ee82c2fae8\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"infmvij3g8c7bamx\", \"$time\": 1694769320.142, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"PersonsNode\"}, \"duration\": 131, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 234}","now":"2023-09-15T09:15:20.384514+00:00","sent_at":"2023-09-15T09:15:20.381000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769323385&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:23.391445+00:00","body":"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","output":[{"uuid":"018a981f-95fe-76af-9f1d-da5e526b4081","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-95fe-76af-9f1d-da5e526b4081\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0rsqs282xgp3wd4o\", \"$time\": 1694769321.47, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"text-default\", \"grow\"], \"attr__class\": \"text-default grow\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Cohorts\", \"attr__href\": \"/cohorts\"}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"a\", \"$el_text\": \"Cohorts\", \"classes\": [\"Link\", \"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__class\": \"Link LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__href\": \"/cohorts\", \"attr__data-attr\": \"menu-item-cohorts\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 13, \"nth_of_type\": 10}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider__content\"], \"attr__class\": \"SideBar__slider__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1913}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9664-7a21-9852-42ce19c880c6","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9664-7a21-9852-42ce19c880c6\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ymf6pk54unynhu8h\", \"$time\": 1694769321.573, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"show-product-intro-existing-products\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1810}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-966b-7dcc-abee-f41b896a74cc","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-966b-7dcc-abee-f41b896a74cc\", \"event\": \"recording viewed with no playtime summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"uz55qy2obbr2z36g\", \"$time\": 1694769321.58, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 3288, \"play_time_ms\": 0, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"engagement_score\": 0, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1803}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-966e-7272-8b9d-bffdc5c840d2","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-966e-7272-8b9d-bffdc5c840d2\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"5w3t82ytjay0nqiw\", \"$time\": 1694769321.582, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Cohorts \\u2022 PostHog\"}, \"offset\": 1801}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9d2f-72eb-8999-bec9f2a9f542","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9d2f-72eb-8999-bec9f2a9f542\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"tk1tnyoiz4gbnk2t\", \"$time\": 1694769323.311, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"a\", \"$el_text\": \"Persons & Groups\", \"classes\": [\"Link\", \"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__class\": \"Link LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__href\": \"/persons\", \"attr__data-attr\": \"menu-item-persons\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 12, \"nth_of_type\": 9}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider__content\"], \"attr__class\": \"SideBar__slider__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 71}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9d37-712e-b09d-61c3f8cf3624","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9d37-712e-b09d-61c3f8cf3624\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"648njm5gtwx2efjj\", \"$time\": 1694769323.319, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Persons & Groups \\u2022 PostHog\"}, \"offset\": 64}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769326396&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:26.539412+00:00","body":"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","output":[{"uuid":"018a981f-9db5-7188-8161-91e9fd602fd7","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9db5-7188-8161-91e9fd602fd7\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"c5yz9qfwa86vhxab\", \"$time\": 1694769323.445, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"PersonsNode\"}, \"duration\": 102, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2945}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a25d-743f-a813-6d909390f5c9","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a25d-743f-a813-6d909390f5c9\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"i100qaub5hceuld4\", \"$time\": 1694769324.637, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"cs-dashboards\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1753}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a264-7a2a-9439-198973cc7878","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a264-7a2a-9439-198973cc7878\", \"event\": \"recording viewed with no playtime summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"wzrv024h7b0m7a8c\", \"$time\": 1694769324.645, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 1295, \"play_time_ms\": 0, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"engagement_score\": 0, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1745}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a266-73d2-a66f-1fbcc32d9f02","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a266-73d2-a66f-1fbcc32d9f02\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ksstzx9julgopw7a\", \"$time\": 1694769324.647, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Persons \\u2022 PostHog\"}, \"offset\": 1743}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a4b3-7b40-b430-9495d1bbed93","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a4b3-7b40-b430-9495d1bbed93\", \"event\": \"person viewed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"s2fzjz6c7t0ekgtm\", \"$time\": 1694769325.236, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"properties_count\": 18, \"has_email\": true, \"has_name\": false, \"custom_properties_count\": 4, \"posthog_properties_count\": 14, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1154}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a676-7722-bece-2f9b3d6b84ce","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a676-7722-bece-2f9b3d6b84ce\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"npyk617r6ht5qzbh\", \"$time\": 1694769325.686, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"person-session-recordings-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Recordings\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 68.19999694824219px; --lemon-tabs-slider-offset: 0px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 704}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a67b-7a6f-9637-bcaacec2496c","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a67b-7a6f-9637-bcaacec2496c\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"fhsu6w5edy7tvvuy\", \"$time\": 1694769325.691, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 699}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a783-7924-b938-3a789f71e25a","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a783-7924-b938-3a789f71e25a\", \"event\": \"recording list fetched\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"fcebvj6tugbw47wk\", \"$time\": 1694769325.955, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"load_time\": 145, \"listing_version\": \"3\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 435}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?compression=gzip-js&ip=1&_=1694769329412&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:29.500667+00:00","body":"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","output":[{"uuid":"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0ovdk9xlnv9fhfak\", \"$time\": 1694769326.837, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"persons-related-flags-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Feature flags\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 74.26666259765625px; --lemon-tabs-slider-offset: 176.29998779296875px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2572}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-aafa-79e8-abf4-4e68eb64c30f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aafa-79e8-abf4-4e68eb64c30f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"y3x76ea6mqqi2q0a\", \"$time\": 1694769326.842, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 2567}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af3d-79d4-be4a-d729c776e0da","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af3d-79d4-be4a-d729c776e0da\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ltw7rl4fhi4bgq63\", \"$time\": 1694769327.934, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 86.23332214355469px; --lemon-tabs-slider-offset: 367.0999755859375px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1475}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af46-7832-9a69-c566751c6625","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af46-7832-9a69-c566751c6625\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"6yl35wdtv5v11o6c\", \"$time\": 1694769327.942, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 1467}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-b008-737a-bb40-6a6a81ba2244","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-b008-737a-bb40-6a6a81ba2244\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"8guwvm82yhwyhfo6\", \"$time\": 1694769328.136, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"EventsQuery\", \"select\": [\"*\", \"event\", \"person\", \"coalesce(properties.$current_url, properties.$screen_name) -- Url / Screen\", \"properties.$lib\", \"timestamp\"], \"personId\": \"018a981f-4c9a-0000-68ff-417481f7c5b5\", \"after\": \"-24h\"}, \"duration\": 171, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1273}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} From 9ad1d643c117d886641bef37fb2d0e28704136af Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 15 Sep 2023 13:00:02 +0200 Subject: [PATCH 031/249] drop the unused site_url field (#30) --- capture/src/capture.rs | 1 - capture/src/event.rs | 1 - capture/tests/django_compat.rs | 5 ++++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index d2f106fb9a657..d9831882550be 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -94,7 +94,6 @@ pub fn process_single_event( uuid: event.uuid.unwrap_or_else(uuid_v7), distinct_id: distinct_id.to_string(), ip: context.client_ip.clone(), - site_url: String::new(), data: String::from("hallo I am some data 😊"), now: context.now.clone(), sent_at: context.sent_at, diff --git a/capture/src/event.rs b/capture/src/event.rs index 5b0c08d1c2798..45a1bf128c15c 100644 --- a/capture/src/event.rs +++ b/capture/src/event.rs @@ -121,7 +121,6 @@ pub struct ProcessedEvent { pub uuid: Uuid, pub distinct_id: String, pub ip: String, - pub site_url: String, pub data: String, pub now: String, #[serde(with = "time::serde::rfc3339::option")] diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index c39f2c7a91dfa..41a024286651c 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -140,7 +140,10 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { OffsetDateTime::parse(value.as_str().expect("empty"), &Iso8601::DEFAULT)?; *value = Value::String(sent_at.format(&Rfc3339)?) } - + if let Some(object) = expected.as_object_mut() { + // site_url is unused in the pipeline now, let's drop it + object.remove("site_url"); + } let match_config = assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict); if let Err(e) = assert_json_matches_no_panic(&json!(expected), &json!(message), match_config) From 7e74df8b2ba3919f27e799f5259926bfb8adcf97 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 9 Oct 2023 19:01:59 +0200 Subject: [PATCH 032/249] fill the data field in analytics events (#31) * fill the data field in analytics events * fix todos --- capture/src/api.rs | 3 +++ capture/src/capture.rs | 26 +++++++++++++++++++++++++- capture/src/event.rs | 20 +++++++++++++++++--- capture/tests/django_compat.rs | 26 ++++++++++++++++++++++++-- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/capture/src/api.rs b/capture/src/api.rs index 9a18a89c4c71c..319056c993fd2 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -34,6 +34,8 @@ pub enum CaptureError { #[error("request holds no event")] EmptyBatch, + #[error("event submitted with an empty event name")] + MissingEventName, #[error("event submitted without a distinct_id")] MissingDistinctId, @@ -58,6 +60,7 @@ impl IntoResponse for CaptureError { CaptureError::RequestDecodingError(_) | CaptureError::RequestParsingError(_) | CaptureError::EmptyBatch + | CaptureError::MissingEventName | CaptureError::MissingDistinctId | CaptureError::EventTooBig | CaptureError::NonRetryableSinkError => (StatusCode::BAD_REQUEST, self.to_string()), diff --git a/capture/src/capture.rs b/capture/src/capture.rs index d9831882550be..98a61d3078913 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -89,12 +89,20 @@ pub fn process_single_event( _ => return Err(CaptureError::MissingDistinctId), }, }; + if event.event.is_empty() { + return Err(CaptureError::MissingEventName); + } + + let data = serde_json::to_string(&event).map_err(|e| { + tracing::error!("failed to encode data field: {}", e); + CaptureError::NonRetryableSinkError + })?; Ok(ProcessedEvent { uuid: event.uuid.unwrap_or_else(uuid_v7), distinct_id: distinct_id.to_string(), ip: context.client_ip.clone(), - data: String::from("hallo I am some data 😊"), + data, now: context.now.clone(), sent_at: context.sent_at, token: context.token.clone(), @@ -158,6 +166,10 @@ mod tests { uuid: None, event: String::new(), properties: HashMap::new(), + timestamp: None, + offset: None, + set: Default::default(), + set_once: Default::default(), }, RawEvent { token: None, @@ -165,6 +177,10 @@ mod tests { uuid: None, event: String::new(), properties: HashMap::from([(String::from("token"), json!("hello"))]), + timestamp: None, + offset: None, + set: Default::default(), + set_once: Default::default(), }, ]; @@ -181,6 +197,10 @@ mod tests { uuid: None, event: String::new(), properties: HashMap::new(), + timestamp: None, + offset: None, + set: Default::default(), + set_once: Default::default(), }, RawEvent { token: None, @@ -188,6 +208,10 @@ mod tests { uuid: None, event: String::new(), properties: HashMap::from([(String::from("token"), json!("goodbye"))]), + timestamp: None, + offset: None, + set: Default::default(), + set_once: Default::default(), }, ]; diff --git a/capture/src/event.rs b/capture/src/event.rs index 45a1bf128c15c..81eb754fd5bd3 100644 --- a/capture/src/event.rs +++ b/capture/src/event.rs @@ -37,12 +37,26 @@ pub struct EventFormData { #[derive(Default, Debug, Deserialize, Serialize)] pub struct RawEvent { - #[serde(alias = "$token", alias = "api_key")] + #[serde( + alias = "$token", + alias = "api_key", + skip_serializing_if = "Option::is_none" + )] pub token: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub distinct_id: Option, pub uuid: Option, pub event: String, + #[serde(default)] pub properties: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub timestamp: Option, // Passed through if provided, parsed by ingestion + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, // Passed through if provided, parsed by ingestion + #[serde(rename = "$set", skip_serializing_if = "Option::is_none")] + pub set: Option>, + #[serde(rename = "$set_once", skip_serializing_if = "Option::is_none")] + pub set_once: Option>, } #[derive(Deserialize)] @@ -51,14 +65,14 @@ enum RawRequest { /// Batch of events Batch(Vec), /// Single event - One(RawEvent), + One(Box), } impl RawRequest { pub fn events(self) -> Vec { match self { RawRequest::Batch(events) => events, - RawRequest::One(event) => vec![event], + RawRequest::One(event) => vec![*event], } } } diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index 41a024286651c..119777d238de6 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -71,7 +71,6 @@ impl EventSink for MemorySink { } #[tokio::test] -#[ignore] async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let file = File::open(REQUESTS_DUMP_FILE_NAME)?; let reader = BufReader::new(file); @@ -107,6 +106,7 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { if !case.ip.is_empty() { req = req.header("X-Forwarded-For", case.ip); } + let res = req.send().await; assert_eq!( res.status(), @@ -140,16 +140,38 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { OffsetDateTime::parse(value.as_str().expect("empty"), &Iso8601::DEFAULT)?; *value = Value::String(sent_at.format(&Rfc3339)?) } + if let Some(expected_data) = expected.get_mut("data") { + // Data is a serialized JSON map. Unmarshall both and compare them, + // instead of expecting the serialized bytes to be equal + let expected_props: Value = + serde_json::from_str(expected_data.as_str().expect("not str"))?; + let found_props: Value = serde_json::from_str(&message.data)?; + let match_config = + assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict); + if let Err(e) = + assert_json_matches_no_panic(&expected_props, &found_props, match_config) + { + println!( + "data field mismatch at line {}, event {}: {}", + line_number, event_number, e + ); + mismatches += 1; + } else { + *expected_data = json!(&message.data) + } + } + if let Some(object) = expected.as_object_mut() { // site_url is unused in the pipeline now, let's drop it object.remove("site_url"); } + let match_config = assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict); if let Err(e) = assert_json_matches_no_panic(&json!(expected), &json!(message), match_config) { println!( - "mismatch at line {}, event {}: {}", + "record mismatch at line {}, event {}: {}", line_number, event_number, e ); mismatches += 1; From c03638b18e1c9c088c9cc4f49919cf47e624044a Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 19 Oct 2023 17:21:49 +0100 Subject: [PATCH 033/249] Add billing limiter (#33) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Redis lib * `cargo update` * fmt * Add base implementation of billing limiter Supports 1. A fixed set of limits, with no redis update 2. A fixed set, subsequently updated from redis 3. No fixed set, updates from redis I still need to figure out how to nicely mock the redis connection that stll leaves enough not mocked to be worth testing. I really don't want integration tests on it :( Also still needs connecting to the API. Reading through the python for this is like 😵‍💫 * Rework I've reworked it a bunch. Honestly the background loop worked but it became really horrible and the locking behaviour a little sketchy. While this will slow down some requests a bit, unless it becomes measurably slow let's keep it that way rather than introducing a bit of a horrible pattern. * hook it all up * Add redis read timeout * Add non-cluster client * Respond to feedback --- Cargo.lock | 257 +++++++++++++++++++++++---------- capture-server/Cargo.toml | 1 + capture-server/src/main.rs | 27 +++- capture/Cargo.toml | 2 + capture/src/api.rs | 12 ++ capture/src/billing_limits.rs | 188 ++++++++++++++++++++++++ capture/src/capture.rs | 24 ++- capture/src/lib.rs | 2 + capture/src/redis.rs | 80 ++++++++++ capture/src/router.rs | 9 +- capture/tests/django_compat.rs | 11 +- 11 files changed, 527 insertions(+), 86 deletions(-) create mode 100644 capture/src/billing_limits.rs create mode 100644 capture/src/redis.rs diff --git a/Cargo.lock b/Cargo.lock index 6ef5c6c66b662..7ea2f9d255934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.5" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -61,7 +61,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] @@ -104,9 +104,9 @@ dependencies = [ [[package]] name = "axum-client-ip" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8e81eacc93f36480825da5f46a33b5fb2246ed024eacc9e8933425b80c5807" +checksum = "1ef117890a418b7832678d9ea1e1c08456dd7b2fd1dadb9676cd6f0fe7eb4b21" dependencies = [ "axum", "forwarded-header-value", @@ -165,9 +165,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bitflags" @@ -183,15 +183,15 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "capture" @@ -212,6 +212,8 @@ dependencies = [ "mockall", "rand", "rdkafka", + "redis", + "redis-test", "serde", "serde_json", "serde_urlencoded", @@ -231,6 +233,7 @@ version = "0.1.0" dependencies = [ "axum", "capture", + "time", "tokio", "tracing", "tracing-subscriber", @@ -260,6 +263,42 @@ dependencies = [ "cc", ] +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc16" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff" + [[package]] name = "crc32fast" version = "1.3.2" @@ -298,7 +337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "lock_api", "once_cell", "parking_lot_core", @@ -452,7 +491,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] @@ -562,15 +601,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "http" @@ -658,12 +697,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -704,9 +743,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libz-sys" @@ -756,15 +795,15 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" @@ -812,7 +851,7 @@ checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] @@ -929,9 +968,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1034,7 +1073,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] @@ -1109,9 +1148,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -1227,6 +1266,40 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "redis" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba" +dependencies = [ + "async-trait", + "bytes", + "combine", + "crc16", + "futures", + "futures-util", + "itoa", + "log", + "percent-encoding", + "pin-project-lite", + "rand", + "ryu", + "sha1_smol", + "socket2 0.4.9", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redis-test" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba266ca48ae66978bf439fd2ac0d7a36a8635823754e2bc73afaf9d2fc25272" +dependencies = [ + "redis", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -1238,9 +1311,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" dependencies = [ "aho-corasick", "memchr", @@ -1250,9 +1323,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "5d58da636bd923eae52b7e9120271cbefb16f399069ee566ca5ebf9c30e32238" dependencies = [ "aho-corasick", "memchr", @@ -1261,15 +1334,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "base64", "bytes", @@ -1291,6 +1364,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-util", "tower-service", @@ -1343,14 +1417,14 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -1379,11 +1453,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] @@ -1414,9 +1494,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" @@ -1430,9 +1510,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", @@ -1451,9 +1531,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.31" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -1466,6 +1546,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "termtree" version = "0.4.1" @@ -1474,22 +1575,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] @@ -1504,9 +1605,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -1517,15 +1618,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -1547,9 +1648,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", "bytes", @@ -1559,7 +1660,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "windows-sys", ] @@ -1572,14 +1673,14 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -1597,11 +1698,11 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "toml_datetime", "winnow", ] @@ -1694,7 +1795,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", ] [[package]] @@ -1755,9 +1856,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -1848,7 +1949,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -1882,7 +1983,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2006,9 +2107,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.15" +version = "0.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907" dependencies = [ "memchr", ] diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml index 04c618286c4cb..6378532482883 100644 --- a/capture-server/Cargo.toml +++ b/capture-server/Cargo.toml @@ -9,3 +9,4 @@ axum = { workspace = true } tokio = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } +time = { workspace = true } diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index e2232d5023655..9d60f89258e50 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -1,7 +1,9 @@ use std::env; use std::net::SocketAddr; +use std::sync::Arc; -use capture::{router, sink, time}; +use capture::{billing_limits::BillingLimiter, redis::RedisClient, router, sink}; +use time::Duration; use tokio::signal; async fn shutdown() { @@ -23,16 +25,35 @@ async fn shutdown() { async fn main() { let use_print_sink = env::var("PRINT_SINK").is_ok(); let address = env::var("ADDRESS").unwrap_or(String::from("127.0.0.1:3000")); + let redis_addr = env::var("REDIS").expect("redis required; please set the REDIS env var"); + + let redis_client = + Arc::new(RedisClient::new(redis_addr).expect("failed to create redis client")); + + let billing = BillingLimiter::new(Duration::seconds(5), redis_client.clone()) + .expect("failed to create billing limiter"); let app = if use_print_sink { - router::router(time::SystemTime {}, sink::PrintSink {}, true) + router::router( + capture::time::SystemTime {}, + sink::PrintSink {}, + redis_client, + billing, + true, + ) } else { let brokers = env::var("KAFKA_BROKERS").expect("Expected KAFKA_BROKERS"); let topic = env::var("KAFKA_TOPIC").expect("Expected KAFKA_TOPIC"); let sink = sink::KafkaSink::new(topic, brokers).unwrap(); - router::router(time::SystemTime {}, sink, true) + router::router( + capture::time::SystemTime {}, + sink, + redis_client, + billing, + true, + ) }; // initialize tracing diff --git a/capture/Cargo.toml b/capture/Cargo.toml index 60cca70824cff..7c84c6ce111ef 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -29,8 +29,10 @@ rdkafka = { workspace = true } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } thiserror = { workspace = true } +redis = { version="0.23.3", features=["tokio-comp", "cluster", "cluster-async"] } [dev-dependencies] assert-json-diff = "2.0.2" axum-test-helper = "0.2.0" mockall = "0.11.2" +redis-test = "0.2.3" diff --git a/capture/src/api.rs b/capture/src/api.rs index 319056c993fd2..ff245b5072ac1 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -52,6 +52,12 @@ pub enum CaptureError { EventTooBig, #[error("invalid event could not be processed")] NonRetryableSinkError, + + #[error("billing limit reached")] + BillingLimit, + + #[error("rate limited")] + RateLimited, } impl IntoResponse for CaptureError { @@ -64,10 +70,16 @@ impl IntoResponse for CaptureError { | CaptureError::MissingDistinctId | CaptureError::EventTooBig | CaptureError::NonRetryableSinkError => (StatusCode::BAD_REQUEST, self.to_string()), + CaptureError::NoTokenError | CaptureError::MultipleTokensError | CaptureError::TokenValidationError(_) => (StatusCode::UNAUTHORIZED, self.to_string()), + CaptureError::RetryableSinkError => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()), + + CaptureError::BillingLimit | CaptureError::RateLimited => { + (StatusCode::TOO_MANY_REQUESTS, self.to_string()) + } } .into_response() } diff --git a/capture/src/billing_limits.rs b/capture/src/billing_limits.rs new file mode 100644 index 0000000000000..44a997c0b4e4d --- /dev/null +++ b/capture/src/billing_limits.rs @@ -0,0 +1,188 @@ +use std::{collections::HashSet, ops::Sub, sync::Arc}; + +use crate::redis::Client; + +/// Limit accounts by team ID if they hit a billing limit +/// +/// We have an async celery worker that regularly checks on accounts + assesses if they are beyond +/// a billing limit. If this is the case, a key is set in redis. +/// +/// Requirements +/// +/// 1. Updates from the celery worker should be reflected in capture within a short period of time +/// 2. Capture should cope with redis being _totally down_, and fail open +/// 3. We should not hit redis for every single request +/// +/// The solution here is to read from the cache until a time interval is hit, and then fetch new +/// data. The write requires taking a lock that stalls all readers, though so long as redis reads +/// stay fast we're ok. +/// +/// Some small delay between an account being limited and the limit taking effect is acceptable. +/// However, ideally we should not allow requests from some pods but 429 from others. +use thiserror::Error; +use time::{Duration, OffsetDateTime}; +use tokio::sync::RwLock; + +// todo: fetch from env +const QUOTA_LIMITER_CACHE_KEY: &str = "@posthog/quota-limits/"; + +pub enum QuotaResource { + Events, + Recordings, +} + +impl QuotaResource { + fn as_str(&self) -> &'static str { + match self { + Self::Events => "events", + Self::Recordings => "recordings", + } + } +} + +#[derive(Error, Debug)] +pub enum LimiterError { + #[error("updater already running - there can only be one")] + UpdaterRunning, +} + +#[derive(Clone)] +pub struct BillingLimiter { + limited: Arc>>, + redis: Arc, + interval: Duration, + updated: Arc>, +} + +impl BillingLimiter { + /// Create a new BillingLimiter. + /// + /// This connects to a redis cluster - pass in a vec of addresses for the initial nodes. + /// + /// You can also initialize the limiter with a set of tokens to limit from the very beginning. + /// This may be overridden by Redis, if the sets differ, + /// + /// Pass an empty redis node list to only use this initial set. + pub fn new( + interval: Duration, + redis: Arc, + ) -> anyhow::Result { + let limited = Arc::new(RwLock::new(HashSet::new())); + + // Force an update immediately if we have any reasonable interval + let updated = OffsetDateTime::from_unix_timestamp(0)?; + let updated = Arc::new(RwLock::new(updated)); + + Ok(BillingLimiter { + interval, + limited, + updated, + redis, + }) + } + + async fn fetch_limited( + client: &Arc, + resource: QuotaResource, + ) -> anyhow::Result> { + let now = time::OffsetDateTime::now_utc().unix_timestamp(); + + client + .zrangebyscore( + format!("{QUOTA_LIMITER_CACHE_KEY}{}", resource.as_str()), + now.to_string(), + String::from("+Inf"), + ) + .await + } + + pub async fn is_limited(&self, key: &str, resource: QuotaResource) -> bool { + // hold the read lock to clone it, very briefly. clone is ok because it's very small 🤏 + // rwlock can have many readers, but one writer. the writer will wait in a queue with all + // the readers, so we want to hold read locks for the smallest time possible to avoid + // writers waiting for too long. and vice versa. + let updated = { + let updated = self.updated.read().await; + *updated + }; + + let now = OffsetDateTime::now_utc(); + let since_update = now.sub(updated); + + // If an update is due, fetch the set from redis + cache it until the next update is due. + // Otherwise, return a value from the cache + // + // This update will block readers! Keep it fast. + if since_update > self.interval { + let span = tracing::debug_span!("updating billing cache from redis"); + let _span = span.enter(); + + // a few requests might end up in here concurrently, but I don't think a few extra will + // be a big problem. If it is, we can rework the concurrency a bit. + // On prod atm we call this around 15 times per second at peak times, and it usually + // completes in <1ms. + + let set = Self::fetch_limited(&self.redis, resource).await; + + tracing::debug!("fetched set from redis, caching"); + + if let Ok(set) = set { + let set = HashSet::from_iter(set.iter().cloned()); + + let mut limited = self.limited.write().await; + *limited = set; + + tracing::debug!("updated cache from redis"); + + limited.contains(key) + } else { + tracing::error!("failed to fetch from redis in time, failing open"); + // If we fail to fetch the set, something really wrong is happening. To avoid + // dropping events that we don't mean to drop, fail open and accept data. Better + // than angry customers :) + // + // TODO: Consider backing off our redis checks + false + } + } else { + let l = self.limited.read().await; + + l.contains(key) + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use time::Duration; + + use crate::{ + billing_limits::{BillingLimiter, QuotaResource}, + redis::MockRedisClient, + }; + + #[tokio::test] + async fn test_dynamic_limited() { + let client = MockRedisClient::new().zrangebyscore_ret(vec![String::from("banana")]); + let client = Arc::new(client); + + let limiter = BillingLimiter::new(Duration::microseconds(1), client) + .expect("Failed to create billing limiter"); + + assert_eq!( + limiter + .is_limited("idk it doesn't matter", QuotaResource::Events) + .await, + false + ); + + assert_eq!( + limiter + .is_limited("some_org_hit_limits", QuotaResource::Events) + .await, + false + ); + assert!(limiter.is_limited("banana", QuotaResource::Events).await); + } +} diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 98a61d3078913..65e64c9135b4b 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -12,6 +12,7 @@ use axum_client_ip::InsecureClientIp; use base64::Engine; use time::OffsetDateTime; +use crate::billing_limits::QuotaResource; use crate::event::ProcessingContext; use crate::token::validate_token; use crate::{ @@ -44,7 +45,7 @@ pub async fn event( _ => RawEvent::from_bytes(&meta, body), }?; - println!("Got events {:?}", &events); + tracing::debug!("got events {:?}", &events); if events.is_empty() { return Err(CaptureError::EmptyBatch); @@ -61,6 +62,7 @@ pub async fn event( } None }); + let context = ProcessingContext { lib_version: meta.lib_version.clone(), sent_at, @@ -69,7 +71,25 @@ pub async fn event( client_ip: ip.to_string(), }; - println!("Got context {:?}", &context); + let limited = state + .billing + .is_limited(context.token.as_str(), QuotaResource::Events) + .await; + + if limited { + // for v0 we want to just return ok 🙃 + // this is because the clients are pretty dumb and will just retry over and over and + // over... + // + // for v1, we'll return a meaningful error code and error, so that the clients can do + // something meaningful with that error + + return Ok(Json(CaptureResponse { + status: CaptureResponseCode::Ok, + })); + } + + tracing::debug!("got context {:?}", &context); process_events(state.sink.clone(), &events, &context).await?; diff --git a/capture/src/lib.rs b/capture/src/lib.rs index d4ca041e5e671..fcd802b43f7cf 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -1,7 +1,9 @@ pub mod api; +pub mod billing_limits; pub mod capture; pub mod event; pub mod prometheus; +pub mod redis; pub mod router; pub mod sink; pub mod time; diff --git a/capture/src/redis.rs b/capture/src/redis.rs new file mode 100644 index 0000000000000..c83c0ad89a8ac --- /dev/null +++ b/capture/src/redis.rs @@ -0,0 +1,80 @@ +use std::time::Duration; + +use anyhow::Result; +use async_trait::async_trait; +use redis::AsyncCommands; +use tokio::time::timeout; + +// average for all commands is <10ms, check grafana +const REDIS_TIMEOUT_MILLISECS: u64 = 10; + +/// A simple redis wrapper +/// I'm currently just exposing the commands we use, for ease of implementation +/// Allows for testing + injecting failures +/// We can also swap it out for alternative implementations in the future +/// I tried using redis-rs Connection/ConnectionLike traits but honestly things just got really +/// awkward to work with. + +#[async_trait] +pub trait Client { + // A very simplified wrapper, but works for our usage + async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result>; +} + +pub struct RedisClient { + client: redis::Client, +} + +impl RedisClient { + pub fn new(addr: String) -> Result { + let client = redis::Client::open(addr)?; + + Ok(RedisClient { client }) + } +} + +#[async_trait] +impl Client for RedisClient { + async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result> { + let mut conn = self.client.get_async_connection().await?; + + let results = conn.zrangebyscore(k, min, max); + let fut = timeout(Duration::from_secs(REDIS_TIMEOUT_MILLISECS), results).await?; + + Ok(fut?) + } +} + +// mockall got really annoying with async and results so I'm just gonna do my own +#[derive(Clone)] +pub struct MockRedisClient { + zrangebyscore_ret: Vec, +} + +impl MockRedisClient { + pub fn new() -> MockRedisClient { + MockRedisClient { + zrangebyscore_ret: Vec::new(), + } + } + + pub fn zrangebyscore_ret(&mut self, ret: Vec) -> Self { + self.zrangebyscore_ret = ret; + + self.clone() + } +} + +impl Default for MockRedisClient { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl Client for MockRedisClient { + // A very simplified wrapper, but works for our usage + async fn zrangebyscore(&self, _k: String, _min: String, _max: String) -> Result> { + Ok(self.zrangebyscore_ret.clone()) + } +} diff --git a/capture/src/router.rs b/capture/src/router.rs index 0c40658c04647..757b975c2948f 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -7,7 +7,7 @@ use axum::{ }; use tower_http::trace::TraceLayer; -use crate::{capture, sink, time::TimeSource}; +use crate::{billing_limits::BillingLimiter, capture, redis::Client, sink, time::TimeSource}; use crate::prometheus::{setup_metrics_recorder, track_metrics}; @@ -15,6 +15,8 @@ use crate::prometheus::{setup_metrics_recorder, track_metrics}; pub struct State { pub sink: Arc, pub timesource: Arc, + pub redis: Arc, + pub billing: BillingLimiter, } async fn index() -> &'static str { @@ -24,14 +26,19 @@ async fn index() -> &'static str { pub fn router< TZ: TimeSource + Send + Sync + 'static, S: sink::EventSink + Send + Sync + 'static, + R: Client + Send + Sync + 'static, >( timesource: TZ, sink: S, + redis: Arc, + billing: BillingLimiter, metrics: bool, ) -> Router { let state = State { sink: Arc::new(sink), timesource: Arc::new(timesource), + redis, + billing, }; let router = Router::new() diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index 119777d238de6..d418996d6b06a 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -5,7 +5,9 @@ use axum_test_helper::TestClient; use base64::engine::general_purpose; use base64::Engine; use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode}; +use capture::billing_limits::BillingLimiter; use capture::event::ProcessedEvent; +use capture::redis::MockRedisClient; use capture::router::router; use capture::sink::EventSink; use capture::time::TimeSource; @@ -15,7 +17,7 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::sync::{Arc, Mutex}; use time::format_description::well_known::{Iso8601, Rfc3339}; -use time::OffsetDateTime; +use time::{Duration, OffsetDateTime}; #[derive(Debug, Deserialize)] struct RequestDump { @@ -93,7 +95,12 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let sink = MemorySink::default(); let timesource = FixedTime { time: case.now }; - let app = router(timesource, sink.clone(), false); + + let redis = Arc::new(MockRedisClient::new()); + let billing = BillingLimiter::new(Duration::weeks(1), redis.clone()) + .expect("failed to create billing limiter"); + + let app = router(timesource, sink.clone(), redis, billing, false); let client = TestClient::new(app); let mut req = client.post(&format!("/i/v0{}", case.path)).body(raw_body); From 30b49944fdb0c5fc0a37072735b6a2998213996f Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 23 Oct 2023 16:56:28 +0200 Subject: [PATCH 034/249] align envvars with plugin-server (#34) --- capture-server/src/main.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 9d60f89258e50..1a78f22ca0e02 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -25,7 +25,8 @@ async fn shutdown() { async fn main() { let use_print_sink = env::var("PRINT_SINK").is_ok(); let address = env::var("ADDRESS").unwrap_or(String::from("127.0.0.1:3000")); - let redis_addr = env::var("REDIS").expect("redis required; please set the REDIS env var"); + let redis_addr = + env::var("REDIS_URL").expect("redis required; please set the REDIS_URL env var"); let redis_client = Arc::new(RedisClient::new(redis_addr).expect("failed to create redis client")); @@ -42,7 +43,7 @@ async fn main() { true, ) } else { - let brokers = env::var("KAFKA_BROKERS").expect("Expected KAFKA_BROKERS"); + let brokers = env::var("KAFKA_HOSTS").expect("Expected KAFKA_HOSTS"); let topic = env::var("KAFKA_TOPIC").expect("Expected KAFKA_TOPIC"); let sink = sink::KafkaSink::new(topic, brokers).unwrap(); From 9ceabe77c00d2cc5d8267e16515115b443358be6 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 26 Oct 2023 17:12:55 +0200 Subject: [PATCH 035/249] add more metrics (#35) --- capture/src/billing_limits.rs | 2 +- capture/src/capture.rs | 13 +++++++++++-- capture/src/prometheus.rs | 5 +++++ capture/src/sink.rs | 7 ++++--- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/capture/src/billing_limits.rs b/capture/src/billing_limits.rs index 44a997c0b4e4d..4309c2174dd09 100644 --- a/capture/src/billing_limits.rs +++ b/capture/src/billing_limits.rs @@ -51,7 +51,7 @@ pub struct BillingLimiter { limited: Arc>>, redis: Arc, interval: Duration, - updated: Arc>, + updated: Arc>, } impl BillingLimiter { diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 65e64c9135b4b..0413d44ea252a 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -10,6 +10,8 @@ use axum::extract::{Query, State}; use axum::http::HeaderMap; use axum_client_ip::InsecureClientIp; use base64::Engine; +use metrics::counter; + use time::OffsetDateTime; use crate::billing_limits::QuotaResource; @@ -50,7 +52,13 @@ pub async fn event( if events.is_empty() { return Err(CaptureError::EmptyBatch); } - let token = extract_and_verify_token(&events)?; + + let token = extract_and_verify_token(&events).map_err(|err| { + counter!("capture_token_shape_invalid_total", events.len() as u64); + err + })?; + + counter!("capture_events_received_total", events.len() as u64); let sent_at = meta.sent_at.and_then(|value| { let value_nanos: i128 = i128::from(value) * 1_000_000; // Assuming the value is in milliseconds, latest posthog-js releases @@ -77,13 +85,14 @@ pub async fn event( .await; if limited { + counter!("capture_events_dropped_over_quota", 1); + // for v0 we want to just return ok 🙃 // this is because the clients are pretty dumb and will just retry over and over and // over... // // for v1, we'll return a meaningful error code and error, so that the clients can do // something meaningful with that error - return Ok(Json(CaptureResponse { status: CaptureResponseCode::Ok, })); diff --git a/capture/src/prometheus.rs b/capture/src/prometheus.rs index 1fcdb7d7ca30b..0cb4750995b7c 100644 --- a/capture/src/prometheus.rs +++ b/capture/src/prometheus.rs @@ -10,6 +10,9 @@ pub fn setup_metrics_recorder() -> PrometheusHandle { const EXPONENTIAL_SECONDS: &[f64] = &[ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0, ]; + const BATCH_SIZES: &[f64] = &[ + 1.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, + ]; PrometheusBuilder::new() .set_buckets_for_metric( @@ -17,6 +20,8 @@ pub fn setup_metrics_recorder() -> PrometheusHandle { EXPONENTIAL_SECONDS, ) .unwrap() + .set_buckets_for_metric(Matcher::Suffix("_batch_size".to_string()), BATCH_SIZES) + .unwrap() .install_recorder() .unwrap() } diff --git a/capture/src/sink.rs b/capture/src/sink.rs index e6f4b7bb4e8ed..6f0cbefd4659b 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use metrics::{counter, histogram}; use tokio::task::JoinSet; use crate::api::CaptureError; @@ -20,8 +21,7 @@ pub struct PrintSink {} impl EventSink for PrintSink { async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { tracing::info!("single event: {:?}", event); - - metrics::increment_counter!("capture_events_total"); + counter!("capture_events_ingested_total", 1); Ok(()) } @@ -29,8 +29,9 @@ impl EventSink for PrintSink { let span = tracing::span!(tracing::Level::INFO, "batch of events"); let _enter = span.enter(); + histogram!("capture_event_batch_size", events.len() as f64); + counter!("capture_events_ingested_total", events.len() as u64); for event in events { - metrics::increment_counter!("capture_events_total"); tracing::info!("event: {:?}", event); } From c9e835eee2c2e81e3707320ad88afacde1c7c5bc Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 26 Oct 2023 17:57:04 +0200 Subject: [PATCH 036/249] add envconfig to parse envvars (#36) --- Cargo.lock | 21 +++++++++++++++++++++ capture-server/Cargo.toml | 1 + capture-server/src/main.rs | 33 +++++++++++++++++++-------------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ea2f9d255934..2caeb20b2faba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,7 @@ version = "0.1.0" dependencies = [ "axum", "capture", + "envconfig", "time", "tokio", "tracing", @@ -379,6 +380,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "envconfig" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea81cc7e21f55a9d9b1efb6816904978d0bfbe31a50347cb24b2e75564bcac9b" +dependencies = [ + "envconfig_derive", +] + +[[package]] +name = "envconfig_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfca278e5f84b45519acaaff758ebfa01f18e96998bc24b8f1b722dd804b9bf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "equivalent" version = "1.0.1" diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml index 6378532482883..fa7151ed497da 100644 --- a/capture-server/Cargo.toml +++ b/capture-server/Cargo.toml @@ -10,3 +10,4 @@ tokio = { workspace = true } tracing-subscriber = { workspace = true } tracing = { workspace = true } time = { workspace = true } +envconfig = "0.10.0" diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 1a78f22ca0e02..46d8e28496cc7 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -1,4 +1,4 @@ -use std::env; +use envconfig::Envconfig; use std::net::SocketAddr; use std::sync::Arc; @@ -6,6 +6,17 @@ use capture::{billing_limits::BillingLimiter, redis::RedisClient, router, sink}; use time::Duration; use tokio::signal; +#[derive(Envconfig)] +struct Config { + #[envconfig(default = "false")] + print_sink: bool, + #[envconfig(default = "127.0.0.1:3000")] + address: SocketAddr, + redis_url: String, + kafka_hosts: String, + kafka_topic: String, +} + async fn shutdown() { let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to register SIGTERM handler"); @@ -23,18 +34,15 @@ async fn shutdown() { #[tokio::main] async fn main() { - let use_print_sink = env::var("PRINT_SINK").is_ok(); - let address = env::var("ADDRESS").unwrap_or(String::from("127.0.0.1:3000")); - let redis_addr = - env::var("REDIS_URL").expect("redis required; please set the REDIS_URL env var"); + let config = Config::init_from_env().expect("Invalid configuration:"); let redis_client = - Arc::new(RedisClient::new(redis_addr).expect("failed to create redis client")); + Arc::new(RedisClient::new(config.redis_url).expect("failed to create redis client")); let billing = BillingLimiter::new(Duration::seconds(5), redis_client.clone()) .expect("failed to create billing limiter"); - let app = if use_print_sink { + let app = if config.print_sink { router::router( capture::time::SystemTime {}, sink::PrintSink {}, @@ -43,10 +51,7 @@ async fn main() { true, ) } else { - let brokers = env::var("KAFKA_HOSTS").expect("Expected KAFKA_HOSTS"); - let topic = env::var("KAFKA_TOPIC").expect("Expected KAFKA_TOPIC"); - - let sink = sink::KafkaSink::new(topic, brokers).unwrap(); + let sink = sink::KafkaSink::new(config.kafka_topic, config.kafka_hosts).unwrap(); router::router( capture::time::SystemTime {}, @@ -58,14 +63,14 @@ async fn main() { }; // initialize tracing - tracing_subscriber::fmt::init(); + // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` - tracing::info!("listening on {}", address); + tracing::info!("listening on {}", config.address); - axum::Server::bind(&address.parse().unwrap()) + axum::Server::bind(&config.address) .serve(app.into_make_service_with_connect_info::()) .with_graceful_shutdown(shutdown()) .await From 7cd2d6132d39150608bd3720f510c9734bafc6f6 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 27 Oct 2023 16:33:32 +0100 Subject: [PATCH 037/249] Create LICENSE (#38) --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000..d1e439cba370e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 PostHog + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From e6ea52f494331793342ebd2b9e9c49059c05247f Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Fri, 27 Oct 2023 16:35:06 +0100 Subject: [PATCH 038/249] Fix the redis billing update (#39) Turns out we never reset updated. --- capture/src/billing_limits.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/capture/src/billing_limits.rs b/capture/src/billing_limits.rs index 4309c2174dd09..5f1540009d0d7 100644 --- a/capture/src/billing_limits.rs +++ b/capture/src/billing_limits.rs @@ -114,6 +114,10 @@ impl BillingLimiter { // // This update will block readers! Keep it fast. if since_update > self.interval { + // open the update lock to change the update, and prevent anyone else from doing so + let mut updated = self.updated.write().await; + *updated = OffsetDateTime::now_utc(); + let span = tracing::debug_span!("updating billing cache from redis"); let _span = span.enter(); From fb6fa7cdd5673435526b6eb052861baf7cc2ecb3 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 27 Oct 2023 18:04:02 +0200 Subject: [PATCH 039/249] kafka: check reachability + collect metrics (#37) --- capture-server/src/main.rs | 7 ++-- capture/src/capture.rs | 8 +--- capture/src/sink.rs | 78 +++++++++++++++++++++++++++++++++++--- 3 files changed, 79 insertions(+), 14 deletions(-) diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 46d8e28496cc7..1f659c87c9d89 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -13,6 +13,7 @@ struct Config { #[envconfig(default = "127.0.0.1:3000")] address: SocketAddr, redis_url: String, + kafka_hosts: String, kafka_topic: String, } @@ -34,6 +35,9 @@ async fn shutdown() { #[tokio::main] async fn main() { + // initialize tracing + tracing_subscriber::fmt::init(); + let config = Config::init_from_env().expect("Invalid configuration:"); let redis_client = @@ -62,9 +66,6 @@ async fn main() { ) }; - // initialize tracing - tracing_subscriber::fmt::init(); - // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 0413d44ea252a..e3c2716a8306d 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -31,8 +31,6 @@ pub async fn event( headers: HeaderMap, body: Bytes, ) -> Result, CaptureError> { - tracing::debug!(len = body.len(), "new event request"); - let events = match headers .get("content-type") .map_or("", |v| v.to_str().unwrap_or("")) @@ -47,8 +45,6 @@ pub async fn event( _ => RawEvent::from_bytes(&meta, body), }?; - tracing::debug!("got events {:?}", &events); - if events.is_empty() { return Err(CaptureError::EmptyBatch); } @@ -98,7 +94,7 @@ pub async fn event( })); } - tracing::debug!("got context {:?}", &context); + tracing::debug!(context=?context, events=?events, "decoded request"); process_events(state.sink.clone(), &events, &context).await?; @@ -169,7 +165,7 @@ pub async fn process_events<'a>( .map(|e| process_single_event(e, context)) .collect::, CaptureError>>()?; - println!("Processed events: {:?}", events); + tracing::debug!(events=?events, "processed {} events", events.len()); if events.len() == 1 { sink.send(events[0].clone()).await?; diff --git a/capture/src/sink.rs b/capture/src/sink.rs index 6f0cbefd4659b..8de9ff128b0ca 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -1,11 +1,15 @@ use async_trait::async_trait; -use metrics::{counter, histogram}; +use metrics::{absolute_counter, counter, gauge, histogram}; +use std::time::Duration; use tokio::task::JoinSet; use crate::api::CaptureError; use rdkafka::config::ClientConfig; use rdkafka::error::RDKafkaErrorCode; use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; +use rdkafka::producer::Producer; +use rdkafka::util::Timeout; +use tracing::info; use crate::event::ProcessedEvent; @@ -39,17 +43,81 @@ impl EventSink for PrintSink { } } +struct KafkaContext; + +impl rdkafka::ClientContext for KafkaContext { + fn stats(&self, stats: rdkafka::Statistics) { + gauge!("capture_kafka_callback_queue_depth", stats.replyq as f64); + gauge!("capture_kafka_producer_queue_depth", stats.msg_cnt as f64); + gauge!( + "capture_kafka_producer_queue_depth_limit", + stats.msg_max as f64 + ); + gauge!("capture_kafka_producer_queue_bytes", stats.msg_max as f64); + gauge!( + "capture_kafka_producer_queue_bytes_limit", + stats.msg_size_max as f64 + ); + + for (topic, stats) in stats.topics { + gauge!( + "capture_kafka_produce_avg_batch_size_bytes", + stats.batchsize.avg as f64, + "topic" => topic.clone() + ); + gauge!( + "capture_kafka_produce_avg_batch_size_events", + stats.batchcnt.avg as f64, + "topic" => topic + ); + } + + for (_, stats) in stats.brokers { + let id_string = format!("{}", stats.nodeid); + gauge!( + "capture_kafka_broker_requests_pending", + stats.outbuf_cnt as f64, + "broker" => id_string.clone() + ); + gauge!( + "capture_kafka_broker_responses_awaiting", + stats.waitresp_cnt as f64, + "broker" => id_string.clone() + ); + absolute_counter!( + "capture_kafka_broker_tx_errors_total", + stats.txerrs, + "broker" => id_string.clone() + ); + absolute_counter!( + "capture_kafka_broker_rx_errors_total", + stats.rxerrs, + "broker" => id_string + ); + } + } +} + #[derive(Clone)] pub struct KafkaSink { - producer: FutureProducer, + producer: FutureProducer, topic: String, } impl KafkaSink { pub fn new(topic: String, brokers: String) -> anyhow::Result { - let producer: FutureProducer = ClientConfig::new() + info!("connecting to Kafka brokers at {}...", brokers); + let producer: FutureProducer = ClientConfig::new() .set("bootstrap.servers", &brokers) - .create()?; + .set("statistics.interval.ms", "10000") + .create_with_context(KafkaContext)?; + + // Ping the cluster to make sure we can reach brokers + _ = producer.client().fetch_metadata( + Some("__consumer_offsets"), + Timeout::After(Duration::new(10, 0)), + )?; + info!("connected to Kafka brokers"); Ok(KafkaSink { producer, topic }) } @@ -57,7 +125,7 @@ impl KafkaSink { impl KafkaSink { async fn kafka_send( - producer: FutureProducer, + producer: FutureProducer, topic: String, event: ProcessedEvent, ) -> Result<(), CaptureError> { From 53017d7505e0b368c98c9ed832b8b722bb017835 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 27 Oct 2023 18:39:26 +0200 Subject: [PATCH 040/249] kafka: add ssl support (#40) --- Cargo.lock | 13 +++++++++++++ Cargo.toml | 2 +- capture-server/src/main.rs | 5 ++++- capture/src/sink.rs | 23 +++++++++++++++-------- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2caeb20b2faba..3fca31c1399a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,6 +1042,18 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl-sys" +version = "0.9.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1284,6 +1296,7 @@ dependencies = [ "libc", "libz-sys", "num_enum", + "openssl-sys", "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 117abaa970f3f..b8b4d08b05474 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ uuid = { version = "1.3.3", features = ["serde"] } async-trait = "0.1.68" serde_urlencoded = "0.7.1" rand = "0.8.5" -rdkafka = { version = "0.34", features = ["cmake-build"] } +rdkafka = { version = "0.34", features = ["cmake-build", "ssl"] } metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" thiserror = "1.0.48" diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 1f659c87c9d89..c2d342d2bb13d 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -16,6 +16,8 @@ struct Config { kafka_hosts: String, kafka_topic: String, + #[envconfig(default = "false")] + kafka_tls: bool, } async fn shutdown() { @@ -55,7 +57,8 @@ async fn main() { true, ) } else { - let sink = sink::KafkaSink::new(config.kafka_topic, config.kafka_hosts).unwrap(); + let sink = + sink::KafkaSink::new(config.kafka_topic, config.kafka_hosts, config.kafka_tls).unwrap(); router::router( capture::time::SystemTime {}, diff --git a/capture/src/sink.rs b/capture/src/sink.rs index 8de9ff128b0ca..f9172dbdb4c7e 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -4,7 +4,7 @@ use std::time::Duration; use tokio::task::JoinSet; use crate::api::CaptureError; -use rdkafka::config::ClientConfig; +use rdkafka::config::{ClientConfig, FromClientConfigAndContext}; use rdkafka::error::RDKafkaErrorCode; use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; use rdkafka::producer::Producer; @@ -105,12 +105,20 @@ pub struct KafkaSink { } impl KafkaSink { - pub fn new(topic: String, brokers: String) -> anyhow::Result { + pub fn new(topic: String, brokers: String, tls: bool) -> anyhow::Result { info!("connecting to Kafka brokers at {}...", brokers); - let producer: FutureProducer = ClientConfig::new() + let mut config = ClientConfig::new(); + config .set("bootstrap.servers", &brokers) - .set("statistics.interval.ms", "10000") - .create_with_context(KafkaContext)?; + .set("statistics.interval.ms", "10000"); + + if tls { + config + .set("security.protocol", "ssl") + .set("enable.ssl.certificate.verification", "false"); + }; + + let producer = FutureProducer::from_config_and_context(&config, KafkaContext)?; // Ping the cluster to make sure we can reach brokers _ = producer.client().fetch_metadata( @@ -180,9 +188,8 @@ impl EventSink for KafkaSink { set.spawn(Self::kafka_send(producer, topic, event)); } - while let Some(res) = set.join_next().await { - println!("{:?}", res); - } + // Await on all the produce promises + while (set.join_next().await).is_some() {} Ok(()) } From 9cf09592ab639323e93741eb16270008e8954cde Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 30 Oct 2023 16:55:53 +0100 Subject: [PATCH 041/249] metrics: consolidate into a labelled capture_events_dropped_total metric --- capture/src/capture.rs | 5 +++-- capture/src/prometheus.rs | 4 ++++ capture/src/sink.rs | 10 ++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index e3c2716a8306d..647594005eaa5 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -16,6 +16,7 @@ use time::OffsetDateTime; use crate::billing_limits::QuotaResource; use crate::event::ProcessingContext; +use crate::prometheus::report_dropped_events; use crate::token::validate_token; use crate::{ api::{CaptureError, CaptureResponse, CaptureResponseCode}, @@ -50,7 +51,7 @@ pub async fn event( } let token = extract_and_verify_token(&events).map_err(|err| { - counter!("capture_token_shape_invalid_total", events.len() as u64); + report_dropped_events("token_shape_invalid", events.len() as u64); err })?; @@ -81,7 +82,7 @@ pub async fn event( .await; if limited { - counter!("capture_events_dropped_over_quota", 1); + report_dropped_events("over_quota", 1); // for v0 we want to just return ok 🙃 // this is because the clients are pretty dumb and will just retry over and over and diff --git a/capture/src/prometheus.rs b/capture/src/prometheus.rs index 0cb4750995b7c..d9dbea8831703 100644 --- a/capture/src/prometheus.rs +++ b/capture/src/prometheus.rs @@ -3,8 +3,12 @@ use std::time::Instant; use axum::{extract::MatchedPath, http::Request, middleware::Next, response::IntoResponse}; +use metrics::counter; use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; +pub fn report_dropped_events(cause: &'static str, quantity: u64) { + counter!("capture_events_dropped_total", quantity, "cause" => cause); +} pub fn setup_metrics_recorder() -> PrometheusHandle { // Ok I broke it at the end, but the limit on our ingress is 60 and that's a nicer way of reaching it const EXPONENTIAL_SECONDS: &[f64] = &[ diff --git a/capture/src/sink.rs b/capture/src/sink.rs index f9172dbdb4c7e..e0c89e3ccd4a0 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -12,6 +12,7 @@ use rdkafka::util::Timeout; use tracing::info; use crate::event::ProcessedEvent; +use crate::prometheus::report_dropped_events; #[async_trait] pub trait EventSink { @@ -152,18 +153,15 @@ impl KafkaSink { timestamp: None, headers: None, }) { - Ok(_) => { - metrics::increment_counter!("capture_events_ingested"); - Ok(()) - } + Ok(_) => Ok(()), Err((e, _)) => match e.rdkafka_error_code() { Some(RDKafkaErrorCode::InvalidMessageSize) => { - metrics::increment_counter!("capture_events_dropped_too_big"); + report_dropped_events("kafka_message_size", 1); Err(CaptureError::EventTooBig) } _ => { // TODO(maybe someday): Don't drop them but write them somewhere and try again - metrics::increment_counter!("capture_events_dropped"); + report_dropped_events("kafka_write_error", 1); tracing::error!("failed to produce event: {}", e); Err(CaptureError::RetryableSinkError) } From a841ac4ea11833971de33f96d6868d143b13c150 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 31 Oct 2023 09:01:09 +0100 Subject: [PATCH 042/249] metrics: report capture_event_batch_size in kafka sink too (#42) --- capture/src/sink.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/capture/src/sink.rs b/capture/src/sink.rs index e0c89e3ccd4a0..cc686eee44cf4 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -25,7 +25,7 @@ pub struct PrintSink {} #[async_trait] impl EventSink for PrintSink { async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { - tracing::info!("single event: {:?}", event); + info!("single event: {:?}", event); counter!("capture_events_ingested_total", 1); Ok(()) @@ -37,7 +37,7 @@ impl EventSink for PrintSink { histogram!("capture_event_batch_size", events.len() as f64); counter!("capture_events_ingested_total", events.len() as u64); for event in events { - tracing::info!("event: {:?}", event); + info!("event: {:?}", event); } Ok(()) @@ -173,12 +173,16 @@ impl KafkaSink { #[async_trait] impl EventSink for KafkaSink { async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { - Self::kafka_send(self.producer.clone(), self.topic.clone(), event).await + Self::kafka_send(self.producer.clone(), self.topic.clone(), event).await?; + + histogram!("capture_event_batch_size", 1.0); + counter!("capture_events_ingested_total", 1); + Ok(()) } async fn send_batch(&self, events: Vec) -> Result<(), CaptureError> { let mut set = JoinSet::new(); - + let batch_size = events.len(); for event in events { let producer = self.producer.clone(); let topic = self.topic.clone(); @@ -189,6 +193,8 @@ impl EventSink for KafkaSink { // Await on all the produce promises while (set.join_next().await).is_some() {} + histogram!("capture_event_batch_size", batch_size as f64); + counter!("capture_events_ingested_total", batch_size as u64); Ok(()) } } From 4b3634b8e77b974a0b3f515bbb72c7430f0488fb Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 31 Oct 2023 16:32:13 +0100 Subject: [PATCH 043/249] add cors support (#43) --- Cargo.toml | 2 +- capture/src/capture.rs | 6 ++++++ capture/src/router.rs | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b8b4d08b05474..318b18098c46f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ serde_json = "1.0.96" governor = "0.5.1" tower_governor = "0.0.4" time = { version = "0.3.20", features = ["formatting", "macros", "parsing", "serde"] } -tower-http = { version = "0.4.0", features = ["trace"] } +tower-http = { version = "0.4.0", features = ["cors", "trace"] } bytes = "1" anyhow = "1.0" flate2 = "1.0" diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 647594005eaa5..45fc5f70f889a 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -104,6 +104,12 @@ pub async fn event( })) } +pub async fn options() -> Result, CaptureError> { + Ok(Json(CaptureResponse { + status: CaptureResponseCode::Ok, + })) +} + pub fn process_single_event( event: &RawEvent, context: &ProcessingContext, diff --git a/capture/src/router.rs b/capture/src/router.rs index 757b975c2948f..8fc080d95d6fb 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -1,10 +1,12 @@ use std::future::ready; use std::sync::Arc; +use axum::http::Method; use axum::{ routing::{get, post}, Router, }; +use tower_http::cors::{AllowOrigin, Any, CorsLayer}; use tower_http::trace::TraceLayer; use crate::{billing_limits::BillingLimiter, capture, redis::Client, sink, time::TimeSource}; @@ -41,12 +43,20 @@ pub fn router< billing, }; + // Very permissive CORS policy, as old SDK versions + // and reverse proxies might send funky headers. + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) + .allow_headers(Any) + .allow_origin(AllowOrigin::mirror_request()); + let router = Router::new() // TODO: use NormalizePathLayer::trim_trailing_slash .route("/", get(index)) - .route("/i/v0/e", post(capture::event)) - .route("/i/v0/e/", post(capture::event)) + .route("/i/v0/e", post(capture::event).options(capture::options)) + .route("/i/v0/e/", post(capture::event).options(capture::options)) .layer(TraceLayer::new_for_http()) + .layer(cors) .layer(axum::middleware::from_fn(track_metrics)) .with_state(state); From 4f85f1ab32317c15c272d6ca430fea3e574fc585 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 3 Nov 2023 10:50:53 +0100 Subject: [PATCH 044/249] cors: enable allow_credentials (#44) --- capture/src/router.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/capture/src/router.rs b/capture/src/router.rs index 8fc080d95d6fb..58fef22a61e3f 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -6,7 +6,7 @@ use axum::{ routing::{get, post}, Router, }; -use tower_http::cors::{AllowOrigin, Any, CorsLayer}; +use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; use tower_http::trace::TraceLayer; use crate::{billing_limits::BillingLimiter, capture, redis::Client, sink, time::TimeSource}; @@ -47,7 +47,8 @@ pub fn router< // and reverse proxies might send funky headers. let cors = CorsLayer::new() .allow_methods([Method::GET, Method::POST, Method::OPTIONS]) - .allow_headers(Any) + .allow_headers(AllowHeaders::mirror_request()) + .allow_credentials(true) .allow_origin(AllowOrigin::mirror_request()); let router = Router::new() From bcc45497e2bbd2f1f9c2deba4602e40a9c8619d9 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 6 Nov 2023 14:21:29 +0100 Subject: [PATCH 045/249] add end2end testing harness to capture-server crate (#45) --- .github/workflows/rust.yml | 5 + Cargo.lock | 227 ++++++++++++++++++++++++++++++--- Cargo.toml | 2 + capture-server/Cargo.toml | 18 ++- capture-server/src/main.rs | 62 +-------- capture-server/tests/common.rs | 169 ++++++++++++++++++++++++ capture-server/tests/events.rs | 75 +++++++++++ capture/Cargo.toml | 3 +- capture/src/config.rs | 19 +++ capture/src/lib.rs | 2 + capture/src/router.rs | 1 - capture/src/server.rs | 52 ++++++++ docker-compose.yml | 51 ++++++++ 13 files changed, 605 insertions(+), 81 deletions(-) create mode 100644 capture-server/tests/common.rs create mode 100644 capture-server/tests/events.rs create mode 100644 capture/src/config.rs create mode 100644 capture/src/server.rs create mode 100644 docker-compose.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 478017af701f2..10636f11554e9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -38,6 +38,11 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Setup end2end dependencies + run: | + docker compose up -d --wait + echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts + - name: Install rust uses: dtolnay/rust-toolchain@master with: diff --git a/Cargo.lock b/Cargo.lock index 3fca31c1399a7..685687f0932e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,7 @@ dependencies = [ "axum-test-helper", "base64", "bytes", + "envconfig", "flate2", "governor", "metrics", @@ -231,9 +232,17 @@ dependencies = [ name = "capture-server" version = "0.1.0" dependencies = [ + "anyhow", + "assert-json-diff", "axum", "capture", "envconfig", + "futures", + "once_cell", + "rand", + "rdkafka", + "reqwest", + "serde_json", "time", "tokio", "tracing", @@ -406,6 +415,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + [[package]] name = "flate2" version = "1.0.27" @@ -431,6 +456,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -458,9 +498,9 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -473,9 +513,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -483,15 +523,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -500,15 +540,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", @@ -517,15 +557,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" @@ -535,9 +575,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -696,6 +736,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "idna" version = "0.4.0" @@ -780,6 +833,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + [[package]] name = "lock_api" version = "0.4.10" @@ -953,6 +1012,24 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -1042,6 +1119,38 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "openssl-sys" version = "0.9.93" @@ -1078,7 +1187,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets", ] @@ -1343,6 +1452,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.10.0" @@ -1387,11 +1505,13 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-tls", "ipnet", "js-sys", "log", "mime", "mime_guess", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -1400,6 +1520,7 @@ dependencies = [ "serde_urlencoded", "system-configuration", "tokio", + "tokio-native-tls", "tokio-util", "tower-service", "url", @@ -1416,6 +1537,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -1428,12 +1562,44 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.188" @@ -1601,6 +1767,19 @@ dependencies = [ "libc", ] +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.4.1", + "rustix", + "windows-sys", +] + [[package]] name = "termtree" version = "0.4.1" @@ -1710,6 +1889,16 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.9" diff --git a/Cargo.toml b/Cargo.toml index 318b18098c46f..402243e69515c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ ] [workspace.dependencies] +assert-json-diff = "2.0.2" axum = "0.6.15" axum-client-ip = "0.4.1" tokio = { version = "1.0", features = ["full"] } @@ -28,3 +29,4 @@ rdkafka = { version = "0.34", features = ["cmake-build", "ssl"] } metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" thiserror = "1.0.48" +envconfig = "0.10.0" diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml index fa7151ed497da..19200043f577f 100644 --- a/capture-server/Cargo.toml +++ b/capture-server/Cargo.toml @@ -4,10 +4,20 @@ version = "0.1.0" edition = "2021" [dependencies] -capture = { path = "../capture" } axum = { workspace = true } +capture = { path = "../capture" } +envconfig = { workspace = true } +time = { workspace = true } tokio = { workspace = true } -tracing-subscriber = { workspace = true } tracing = { workspace = true } -time = { workspace = true } -envconfig = "0.10.0" +tracing-subscriber = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true, features = [] } +assert-json-diff = { workspace = true } +futures = "0.3.29" +once_cell = "1.18.0" +rand = { workspace = true } +rdkafka = { workspace = true } +reqwest = "0.11.22" +serde_json = { workspace = true } diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index c2d342d2bb13d..4874a43a32aa7 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -1,24 +1,10 @@ -use envconfig::Envconfig; -use std::net::SocketAddr; -use std::sync::Arc; +use std::net::TcpListener; -use capture::{billing_limits::BillingLimiter, redis::RedisClient, router, sink}; -use time::Duration; +use envconfig::Envconfig; use tokio::signal; -#[derive(Envconfig)] -struct Config { - #[envconfig(default = "false")] - print_sink: bool, - #[envconfig(default = "127.0.0.1:3000")] - address: SocketAddr, - redis_url: String, - - kafka_hosts: String, - kafka_topic: String, - #[envconfig(default = "false")] - kafka_tls: bool, -} +use capture::config::Config; +use capture::server::serve; async fn shutdown() { let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) @@ -41,42 +27,6 @@ async fn main() { tracing_subscriber::fmt::init(); let config = Config::init_from_env().expect("Invalid configuration:"); - - let redis_client = - Arc::new(RedisClient::new(config.redis_url).expect("failed to create redis client")); - - let billing = BillingLimiter::new(Duration::seconds(5), redis_client.clone()) - .expect("failed to create billing limiter"); - - let app = if config.print_sink { - router::router( - capture::time::SystemTime {}, - sink::PrintSink {}, - redis_client, - billing, - true, - ) - } else { - let sink = - sink::KafkaSink::new(config.kafka_topic, config.kafka_hosts, config.kafka_tls).unwrap(); - - router::router( - capture::time::SystemTime {}, - sink, - redis_client, - billing, - true, - ) - }; - - // run our app with hyper - // `axum::Server` is a re-export of `hyper::Server` - - tracing::info!("listening on {}", config.address); - - axum::Server::bind(&config.address) - .serve(app.into_make_service_with_connect_info::()) - .with_graceful_shutdown(shutdown()) - .await - .unwrap(); + let listener = TcpListener::bind(config.address).unwrap(); + serve(config, listener, shutdown()).await } diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs new file mode 100644 index 0000000000000..40836ca7f6b73 --- /dev/null +++ b/capture-server/tests/common.rs @@ -0,0 +1,169 @@ +#![allow(dead_code)] + +use std::default::Default; +use std::net::{SocketAddr, TcpListener}; +use std::str::FromStr; +use std::string::ToString; +use std::sync::{Arc, Once}; +use std::time::Duration; + +use anyhow::bail; +use once_cell::sync::Lazy; +use rand::distributions::Alphanumeric; +use rand::Rng; +use rdkafka::admin::{AdminClient, AdminOptions, NewTopic, TopicReplication}; +use rdkafka::config::{ClientConfig, FromClientConfig}; +use rdkafka::consumer::{BaseConsumer, Consumer}; +use rdkafka::util::Timeout; +use rdkafka::{Message, TopicPartitionList}; +use tokio::sync::Notify; +use tracing::debug; + +use capture::config::Config; +use capture::server::serve; + +pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { + print_sink: false, + address: SocketAddr::from_str("127.0.0.1:0").unwrap(), + export_prometheus: false, + redis_url: "redis://localhost:6379/".to_string(), + kafka_hosts: "kafka:9092".to_string(), + kafka_topic: "events_plugin_ingestion".to_string(), + kafka_tls: false, +}); + +static TRACING_INIT: Once = Once::new(); +pub fn setup_tracing() { + TRACING_INIT.call_once(|| { + tracing_subscriber::fmt() + .with_writer(tracing_subscriber::fmt::TestWriter::new()) + .init() + }); +} +pub struct ServerHandle { + pub addr: SocketAddr, + shutdown: Arc, +} + +impl ServerHandle { + pub fn for_topic(topic: &EphemeralTopic) -> Self { + let mut config = DEFAULT_CONFIG.clone(); + config.kafka_topic = topic.topic_name().to_string(); + Self::for_config(config) + } + pub fn for_config(config: Config) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let notify = Arc::new(Notify::new()); + let shutdown = notify.clone(); + + tokio::spawn( + async move { serve(config, listener, async { notify.notified().await }).await }, + ); + Self { addr, shutdown } + } + + pub async fn capture_events>(&self, body: T) -> reqwest::Response { + let client = reqwest::Client::new(); + client + .post(format!("http://{:?}/i/v0/e", self.addr)) + .body(body) + .send() + .await + .expect("failed to send request") + } +} + +impl Drop for ServerHandle { + fn drop(&mut self) { + self.shutdown.notify_one() + } +} + +pub struct EphemeralTopic { + consumer: BaseConsumer, + read_timeout: Timeout, + topic_name: String, +} + +impl EphemeralTopic { + pub async fn new() -> Self { + let mut config = ClientConfig::new(); + config.set("group.id", "capture_integration_tests"); + config.set("bootstrap.servers", DEFAULT_CONFIG.kafka_hosts.clone()); + config.set("debug", "all"); + + // TODO: check for name collision? + let topic_name = random_string("events_", 16); + let admin = AdminClient::from_config(&config).expect("failed to create admin client"); + admin + .create_topics( + &[NewTopic { + name: &topic_name, + num_partitions: 1, + replication: TopicReplication::Fixed(1), + config: vec![], + }], + &AdminOptions::default(), + ) + .await + .expect("failed to create topic"); + + let consumer: BaseConsumer = config.create().expect("failed to create consumer"); + let mut assignment = TopicPartitionList::new(); + assignment.add_partition(&topic_name, 0); + consumer + .assign(&assignment) + .expect("failed to assign topic"); + + Self { + consumer, + read_timeout: Timeout::After(Duration::from_secs(5)), + topic_name, + } + } + + pub fn next_event(&self) -> anyhow::Result { + match self.consumer.poll(self.read_timeout) { + Some(Ok(message)) => { + let body = message.payload().expect("empty kafka message"); + let event = serde_json::from_slice(body)?; + Ok(event) + } + Some(Err(err)) => bail!("kafka read error: {}", err), + None => bail!("kafka read timeout"), + } + } + + pub fn topic_name(&self) -> &str { + &self.topic_name + } +} + +impl Drop for EphemeralTopic { + fn drop(&mut self) { + debug!("dropping EphemeralTopic {}...", self.topic_name); + _ = self.consumer.unassign(); + futures::executor::block_on(delete_topic(self.topic_name.clone())); + debug!("dropped topic"); + } +} + +async fn delete_topic(topic: String) { + let mut config = ClientConfig::new(); + config.set("bootstrap.servers", DEFAULT_CONFIG.kafka_hosts.clone()); + let admin = AdminClient::from_config(&config).expect("failed to create admin client"); + admin + .delete_topics(&[&topic], &AdminOptions::default()) + .await + .expect("failed to delete topic"); +} + +pub fn random_string(prefix: &str, length: usize) -> String { + let suffix: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(length) + .map(char::from) + .collect(); + format!("{}_{}", prefix, suffix) +} diff --git a/capture-server/tests/events.rs b/capture-server/tests/events.rs new file mode 100644 index 0000000000000..42facd86554ec --- /dev/null +++ b/capture-server/tests/events.rs @@ -0,0 +1,75 @@ +use anyhow::Result; +use assert_json_diff::assert_json_include; +use reqwest::StatusCode; +use serde_json::json; + +use crate::common::*; +mod common; + +#[tokio::test] +async fn it_captures_one_event() -> Result<()> { + setup_tracing(); + let token = random_string("token", 16); + let distinct_id = random_string("id", 16); + let topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topic(&topic); + + let event = json!({ + "token": token, + "event": "testing", + "distinct_id": distinct_id + }); + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + let event = topic.next_event()?; + assert_json_include!( + actual: event, + expected: json!({ + "token": token, + "distinct_id": distinct_id + }) + ); + + Ok(()) +} + +#[tokio::test] +async fn it_captures_a_batch() -> Result<()> { + setup_tracing(); + let token = random_string("token", 16); + let distinct_id1 = random_string("id", 16); + let distinct_id2 = random_string("id", 16); + + let topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topic(&topic); + + let event = json!([{ + "token": token, + "event": "event1", + "distinct_id": distinct_id1 + },{ + "token": token, + "event": "event2", + "distinct_id": distinct_id2 + }]); + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + assert_json_include!( + actual: topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": distinct_id1 + }) + ); + assert_json_include!( + actual: topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": distinct_id2 + }) + ); + + Ok(()) +} diff --git a/capture/Cargo.toml b/capture/Cargo.toml index 7c84c6ce111ef..6bdfc39b20216 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -30,9 +30,10 @@ metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } thiserror = { workspace = true } redis = { version="0.23.3", features=["tokio-comp", "cluster", "cluster-async"] } +envconfig = { workspace = true } [dev-dependencies] -assert-json-diff = "2.0.2" +assert-json-diff = { workspace = true } axum-test-helper = "0.2.0" mockall = "0.11.2" redis-test = "0.2.3" diff --git a/capture/src/config.rs b/capture/src/config.rs new file mode 100644 index 0000000000000..6edf438264f13 --- /dev/null +++ b/capture/src/config.rs @@ -0,0 +1,19 @@ +use std::net::SocketAddr; + +use envconfig::Envconfig; + +#[derive(Envconfig, Clone)] +pub struct Config { + #[envconfig(default = "false")] + pub print_sink: bool, + #[envconfig(default = "127.0.0.1:3000")] + pub address: SocketAddr, + pub redis_url: String, + #[envconfig(default = "true")] + pub export_prometheus: bool, + + pub kafka_hosts: String, + pub kafka_topic: String, + #[envconfig(default = "false")] + pub kafka_tls: bool, +} diff --git a/capture/src/lib.rs b/capture/src/lib.rs index fcd802b43f7cf..70f754807bc13 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -1,10 +1,12 @@ pub mod api; pub mod billing_limits; pub mod capture; +pub mod config; pub mod event; pub mod prometheus; pub mod redis; pub mod router; +pub mod server; pub mod sink; pub mod time; pub mod token; diff --git a/capture/src/router.rs b/capture/src/router.rs index 58fef22a61e3f..9acc7f4f6f920 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -66,7 +66,6 @@ pub fn router< // does not work well. if metrics { let recorder_handle = setup_metrics_recorder(); - router.route("/metrics", get(move || ready(recorder_handle.render()))) } else { router diff --git a/capture/src/server.rs b/capture/src/server.rs new file mode 100644 index 0000000000000..bee579ef3774e --- /dev/null +++ b/capture/src/server.rs @@ -0,0 +1,52 @@ +use std::future::Future; +use std::net::{SocketAddr, TcpListener}; +use std::sync::Arc; + +use time::Duration; + +use crate::billing_limits::BillingLimiter; +use crate::config::Config; +use crate::redis::RedisClient; +use crate::{router, sink}; + +pub async fn serve(config: Config, listener: TcpListener, shutdown: F) +where + F: Future, +{ + let redis_client = + Arc::new(RedisClient::new(config.redis_url).expect("failed to create redis client")); + + let billing = BillingLimiter::new(Duration::seconds(5), redis_client.clone()) + .expect("failed to create billing limiter"); + + let app = if config.print_sink { + router::router( + crate::time::SystemTime {}, + sink::PrintSink {}, + redis_client, + billing, + config.export_prometheus, + ) + } else { + let sink = + sink::KafkaSink::new(config.kafka_topic, config.kafka_hosts, config.kafka_tls).unwrap(); + + router::router( + crate::time::SystemTime {}, + sink, + redis_client, + billing, + config.export_prometheus, + ) + }; + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + tracing::info!("listening on {:?}", listener.local_addr().unwrap()); + axum::Server::from_tcp(listener) + .unwrap() + .serve(app.into_make_service_with_connect_info::()) + .with_graceful_shutdown(shutdown) + .await + .unwrap() +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000..804ae78ec7512 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: "3" + +services: + zookeeper: + image: zookeeper:3.7.0 + restart: on-failure + + kafka: + image: ghcr.io/posthog/kafka-container:v2.8.2 + restart: on-failure + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1001 + KAFKA_CFG_RESERVED_BROKER_MAX_ID: 1001 + KAFKA_CFG_LISTENERS: PLAINTEXT://:9092 + KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_CFG_ZOOKEEPER_CONNECT: zookeeper:2181 + ALLOW_PLAINTEXT_LISTENER: 'true' + ports: + - '9092:9092' + healthcheck: + test: kafka-cluster.sh cluster-id --bootstrap-server localhost:9092 || exit 1 + interval: 3s + timeout: 10s + retries: 10 + + redis: + image: redis:6.2.7-alpine + restart: on-failure + command: redis-server --maxmemory-policy allkeys-lru --maxmemory 200mb + ports: + - '6379:6379' + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 3s + timeout: 10s + retries: 10 + + kafka-ui: + image: provectuslabs/kafka-ui:latest + profiles: ["ui"] + ports: + - '8080:8080' + depends_on: + - zookeeper + - kafka + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 + KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 From 8f6003267357b7ca1eb497b184adbbae7cce8057 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 6 Nov 2023 16:46:44 +0100 Subject: [PATCH 046/249] kafka sink: expose more rdkafka settings (#46) --- capture-server/tests/common.rs | 25 ++++++++++++----- capture/src/config.rs | 11 ++++++++ capture/src/server.rs | 4 +-- capture/src/sink.rs | 49 +++++++++++++++++++++------------- 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index 40836ca7f6b73..d4665cfedc748 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -19,7 +19,7 @@ use rdkafka::{Message, TopicPartitionList}; use tokio::sync::Notify; use tracing::debug; -use capture::config::Config; +use capture::config::{Config, KafkaConfig}; use capture::server::serve; pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { @@ -27,9 +27,14 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { address: SocketAddr::from_str("127.0.0.1:0").unwrap(), export_prometheus: false, redis_url: "redis://localhost:6379/".to_string(), - kafka_hosts: "kafka:9092".to_string(), - kafka_topic: "events_plugin_ingestion".to_string(), - kafka_tls: false, + kafka: KafkaConfig { + kafka_producer_linger_ms: 0, // Send messages as soon as possible + kafka_producer_queue_mib: 10, + kafka_compression_codec: "none".to_string(), + kafka_hosts: "kafka:9092".to_string(), + kafka_topic: "events_plugin_ingestion".to_string(), + kafka_tls: false, + }, }); static TRACING_INIT: Once = Once::new(); @@ -48,7 +53,7 @@ pub struct ServerHandle { impl ServerHandle { pub fn for_topic(topic: &EphemeralTopic) -> Self { let mut config = DEFAULT_CONFIG.clone(); - config.kafka_topic = topic.topic_name().to_string(); + config.kafka.kafka_topic = topic.topic_name().to_string(); Self::for_config(config) } pub fn for_config(config: Config) -> Self { @@ -90,7 +95,10 @@ impl EphemeralTopic { pub async fn new() -> Self { let mut config = ClientConfig::new(); config.set("group.id", "capture_integration_tests"); - config.set("bootstrap.servers", DEFAULT_CONFIG.kafka_hosts.clone()); + config.set( + "bootstrap.servers", + DEFAULT_CONFIG.kafka.kafka_hosts.clone(), + ); config.set("debug", "all"); // TODO: check for name collision? @@ -151,7 +159,10 @@ impl Drop for EphemeralTopic { async fn delete_topic(topic: String) { let mut config = ClientConfig::new(); - config.set("bootstrap.servers", DEFAULT_CONFIG.kafka_hosts.clone()); + config.set( + "bootstrap.servers", + DEFAULT_CONFIG.kafka.kafka_hosts.clone(), + ); let admin = AdminClient::from_config(&config).expect("failed to create admin client"); admin .delete_topics(&[&topic], &AdminOptions::default()) diff --git a/capture/src/config.rs b/capture/src/config.rs index 6edf438264f13..e3ea1e8461a27 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -11,7 +11,18 @@ pub struct Config { pub redis_url: String, #[envconfig(default = "true")] pub export_prometheus: bool, + #[envconfig(nested = true)] + pub kafka: KafkaConfig, +} +#[derive(Envconfig, Clone)] +pub struct KafkaConfig { + #[envconfig(default = "20")] + pub kafka_producer_linger_ms: u32, // Maximum time between producer batches during low traffic + #[envconfig(default = "400")] + pub kafka_producer_queue_mib: u32, // Size of the in-memory producer queue in mebibytes + #[envconfig(default = "none")] + pub kafka_compression_codec: String, // none, gzip, snappy, lz4, zstd pub kafka_hosts: String, pub kafka_topic: String, #[envconfig(default = "false")] diff --git a/capture/src/server.rs b/capture/src/server.rs index bee579ef3774e..e4372ae0b584a 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -28,9 +28,7 @@ where config.export_prometheus, ) } else { - let sink = - sink::KafkaSink::new(config.kafka_topic, config.kafka_hosts, config.kafka_tls).unwrap(); - + let sink = sink::KafkaSink::new(config.kafka).unwrap(); router::router( crate::time::SystemTime {}, sink, diff --git a/capture/src/sink.rs b/capture/src/sink.rs index cc686eee44cf4..d6b60d82024b4 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -1,16 +1,17 @@ -use async_trait::async_trait; -use metrics::{absolute_counter, counter, gauge, histogram}; use std::time::Duration; -use tokio::task::JoinSet; -use crate::api::CaptureError; -use rdkafka::config::{ClientConfig, FromClientConfigAndContext}; +use async_trait::async_trait; +use metrics::{absolute_counter, counter, gauge, histogram}; +use rdkafka::config::ClientConfig; use rdkafka::error::RDKafkaErrorCode; use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; use rdkafka::producer::Producer; use rdkafka::util::Timeout; -use tracing::info; +use tokio::task::JoinSet; +use tracing::{debug, info}; +use crate::api::CaptureError; +use crate::config::KafkaConfig; use crate::event::ProcessedEvent; use crate::prometheus::report_dropped_events; @@ -106,29 +107,41 @@ pub struct KafkaSink { } impl KafkaSink { - pub fn new(topic: String, brokers: String, tls: bool) -> anyhow::Result { - info!("connecting to Kafka brokers at {}...", brokers); - let mut config = ClientConfig::new(); - config - .set("bootstrap.servers", &brokers) - .set("statistics.interval.ms", "10000"); - - if tls { - config + pub fn new(config: KafkaConfig) -> anyhow::Result { + info!("connecting to Kafka brokers at {}...", config.kafka_hosts); + + let mut client_config = ClientConfig::new(); + client_config + .set("bootstrap.servers", &config.kafka_hosts) + .set("statistics.interval.ms", "10000") + .set("linger.ms", config.kafka_producer_linger_ms.to_string()) + .set("compression.codec", config.kafka_compression_codec) + .set( + "queue.buffering.max.kbytes", + (config.kafka_producer_queue_mib * 1024).to_string(), + ); + + if config.kafka_tls { + client_config .set("security.protocol", "ssl") .set("enable.ssl.certificate.verification", "false"); }; - let producer = FutureProducer::from_config_and_context(&config, KafkaContext)?; + debug!("rdkafka configuration: {:?}", client_config); + let producer: FutureProducer = + client_config.create_with_context(KafkaContext)?; - // Ping the cluster to make sure we can reach brokers + // Ping the cluster to make sure we can reach brokers, fail after 10 seconds _ = producer.client().fetch_metadata( Some("__consumer_offsets"), Timeout::After(Duration::new(10, 0)), )?; info!("connected to Kafka brokers"); - Ok(KafkaSink { producer, topic }) + Ok(KafkaSink { + producer, + topic: config.kafka_topic, + }) } } From 780f3909c08662a0be5d08a0274cd9efe0ff7f59 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 7 Nov 2023 13:53:26 +0100 Subject: [PATCH 047/249] implement liveness checks based on rdkafka health (#41) --- capture/src/health.rs | 344 +++++++++++++++++++++++++++++++++ capture/src/lib.rs | 1 + capture/src/router.rs | 4 + capture/src/server.rs | 17 +- capture/src/sink.rs | 13 +- capture/tests/django_compat.rs | 11 +- 6 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 capture/src/health.rs diff --git a/capture/src/health.rs b/capture/src/health.rs new file mode 100644 index 0000000000000..dcddbe477e7cc --- /dev/null +++ b/capture/src/health.rs @@ -0,0 +1,344 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use std::collections::HashMap; +use std::ops::Add; +use std::sync::{Arc, RwLock}; + +use time::Duration; +use tokio::sync::mpsc; +use tracing::{info, warn}; + +/// Health reporting for components of the service. +/// +/// The capture server contains several asynchronous loops, and +/// the process can only be trusted with user data if all the +/// loops are properly running and reporting. +/// +/// HealthRegistry allows an arbitrary number of components to +/// be registered and report their health. The process' health +/// status is the combination of these individual health status: +/// - if any component is unhealthy, the process is unhealthy +/// - if all components recently reported healthy, the process is healthy +/// - if a component failed to report healthy for its defined deadline, +/// it is considered unhealthy, and the check fails. +/// +/// Trying to merge the k8s concepts of liveness and readiness in +/// a single state is full of foot-guns, so HealthRegistry does not +/// try to do it. Each probe should have its separate instance of +/// the registry to avoid confusions. + +#[derive(Default, Debug)] +pub struct HealthStatus { + /// The overall status: true of all components are healthy + pub healthy: bool, + /// Current status of each registered component, for display + pub components: HashMap, +} +impl IntoResponse for HealthStatus { + /// Computes the axum status code based on the overall health status, + /// and prints each component status in the body for debugging. + fn into_response(self) -> Response { + let body = format!("{:?}", self); + match self.healthy { + true => (StatusCode::OK, body), + false => (StatusCode::INTERNAL_SERVER_ERROR, body), + } + .into_response() + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ComponentStatus { + /// Automatically set when a component is newly registered + Starting, + /// Recently reported healthy, will need to report again before the date + HealthyUntil(time::OffsetDateTime), + /// Reported unhealthy + Unhealthy, + /// Automatically set when the HealthyUntil deadline is reached + Stalled, +} +struct HealthMessage { + component: String, + status: ComponentStatus, +} + +pub struct HealthHandle { + component: String, + deadline: Duration, + sender: mpsc::Sender, +} + +impl HealthHandle { + /// Asynchronously report healthy, returns when the message is queued. + /// Must be called more frequently than the configured deadline. + pub async fn report_healthy(&self) { + self.report_status(ComponentStatus::HealthyUntil( + time::OffsetDateTime::now_utc().add(self.deadline), + )) + .await + } + + /// Asynchronously report component status, returns when the message is queued. + pub async fn report_status(&self, status: ComponentStatus) { + let message = HealthMessage { + component: self.component.clone(), + status, + }; + if let Err(err) = self.sender.send(message).await { + warn!("failed to report heath status: {}", err) + } + } + + /// Synchronously report as healthy, returns when the message is queued. + /// Must be called more frequently than the configured deadline. + pub fn report_healthy_blocking(&self) { + self.report_status_blocking(ComponentStatus::HealthyUntil( + time::OffsetDateTime::now_utc().add(self.deadline), + )) + } + + /// Asynchronously report component status, returns when the message is queued. + pub fn report_status_blocking(&self, status: ComponentStatus) { + let message = HealthMessage { + component: self.component.clone(), + status, + }; + if let Err(err) = self.sender.blocking_send(message) { + warn!("failed to report heath status: {}", err) + } + } +} + +#[derive(Clone)] +pub struct HealthRegistry { + name: String, + components: Arc>>, + sender: mpsc::Sender, +} + +impl HealthRegistry { + pub fn new(name: &str) -> Self { + let (tx, mut rx) = mpsc::channel::(16); + let registry = Self { + name: name.to_owned(), + components: Default::default(), + sender: tx, + }; + + let components = registry.components.clone(); + tokio::spawn(async move { + while let Some(message) = rx.recv().await { + if let Ok(mut map) = components.write() { + _ = map.insert(message.component, message.status); + } else { + // Poisoned mutex: Just warn, the probes will fail and the process restart + warn!("poisoned HeathRegistry mutex") + } + } + }); + + registry + } + + /// Registers a new component in the registry. The returned handle should be passed + /// to the component, to allow it to frequently report its health status. + pub async fn register(&self, component: String, deadline: Duration) -> HealthHandle { + let handle = HealthHandle { + component, + deadline, + sender: self.sender.clone(), + }; + handle.report_status(ComponentStatus::Starting).await; + handle + } + + /// Returns the overall process status, computed from the status of all the components + /// currently registered. Can be used as an axum handler. + pub fn get_status(&self) -> HealthStatus { + let components = self + .components + .read() + .expect("poisoned HeathRegistry mutex"); + + let result = HealthStatus { + healthy: !components.is_empty(), // unhealthy if no component has registered yet + components: Default::default(), + }; + let now = time::OffsetDateTime::now_utc(); + + let result = components + .iter() + .fold(result, |mut result, (name, status)| { + match status { + ComponentStatus::HealthyUntil(until) => { + if until.gt(&now) { + _ = result.components.insert(name.clone(), status.clone()) + } else { + result.healthy = false; + _ = result + .components + .insert(name.clone(), ComponentStatus::Stalled) + } + } + _ => { + result.healthy = false; + _ = result.components.insert(name.clone(), status.clone()) + } + } + result + }); + match result.healthy { + true => info!("{} health check ok", self.name), + false => warn!("{} health check failed: {:?}", self.name, result.components), + } + result + } +} + +#[cfg(test)] +mod tests { + use crate::health::{ComponentStatus, HealthRegistry, HealthStatus}; + use axum::http::StatusCode; + use axum::response::IntoResponse; + use std::ops::{Add, Sub}; + use time::{Duration, OffsetDateTime}; + + async fn assert_or_retry(check: F) + where + F: Fn() -> bool, + { + assert_or_retry_for_duration(check, Duration::seconds(5)).await + } + + async fn assert_or_retry_for_duration(check: F, timeout: Duration) + where + F: Fn() -> bool, + { + let deadline = OffsetDateTime::now_utc().add(timeout); + while !check() && OffsetDateTime::now_utc().lt(&deadline) { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + assert!(check()) + } + #[tokio::test] + async fn defaults_to_unhealthy() { + let registry = HealthRegistry::new("liveness"); + assert!(!registry.get_status().healthy); + } + + #[tokio::test] + async fn one_component() { + let registry = HealthRegistry::new("liveness"); + + // New components are registered in Starting + let handle = registry + .register("one".to_string(), Duration::seconds(30)) + .await; + assert_or_retry(|| registry.get_status().components.len() == 1).await; + let mut status = registry.get_status(); + assert!(!status.healthy); + assert_eq!( + status.components.get("one"), + Some(&ComponentStatus::Starting) + ); + + // Status goes healthy once the component reports + handle.report_healthy().await; + assert_or_retry(|| registry.get_status().healthy).await; + status = registry.get_status(); + assert_eq!(status.components.len(), 1); + + // Status goes unhealthy if the components says so + handle.report_status(ComponentStatus::Unhealthy).await; + assert_or_retry(|| !registry.get_status().healthy).await; + status = registry.get_status(); + assert_eq!(status.components.len(), 1); + assert_eq!( + status.components.get("one"), + Some(&ComponentStatus::Unhealthy) + ); + } + + #[tokio::test] + async fn staleness_check() { + let registry = HealthRegistry::new("liveness"); + let handle = registry + .register("one".to_string(), Duration::seconds(30)) + .await; + + // Status goes healthy once the component reports + handle.report_healthy().await; + assert_or_retry(|| registry.get_status().healthy).await; + let mut status = registry.get_status(); + assert_eq!(status.components.len(), 1); + + // If the component's ping is too old, it is considered stalled and the healthcheck fails + // FIXME: we should mock the time instead + handle + .report_status(ComponentStatus::HealthyUntil( + OffsetDateTime::now_utc().sub(Duration::seconds(1)), + )) + .await; + assert_or_retry(|| !registry.get_status().healthy).await; + status = registry.get_status(); + assert_eq!(status.components.len(), 1); + assert_eq!( + status.components.get("one"), + Some(&ComponentStatus::Stalled) + ); + } + + #[tokio::test] + async fn several_components() { + let registry = HealthRegistry::new("liveness"); + let handle1 = registry + .register("one".to_string(), Duration::seconds(30)) + .await; + let handle2 = registry + .register("two".to_string(), Duration::seconds(30)) + .await; + assert_or_retry(|| registry.get_status().components.len() == 2).await; + + // First component going healthy is not enough + handle1.report_healthy().await; + assert_or_retry(|| { + registry.get_status().components.get("one").unwrap() != &ComponentStatus::Starting + }) + .await; + assert!(!registry.get_status().healthy); + + // Second component going healthy brings the health to green + handle2.report_healthy().await; + assert_or_retry(|| { + registry.get_status().components.get("two").unwrap() != &ComponentStatus::Starting + }) + .await; + assert!(registry.get_status().healthy); + + // First component going unhealthy takes down the health to red + handle1.report_status(ComponentStatus::Unhealthy).await; + assert_or_retry(|| !registry.get_status().healthy).await; + + // First component recovering returns the health to green + handle1.report_healthy().await; + assert_or_retry(|| registry.get_status().healthy).await; + + // Second component going unhealthy takes down the health to red + handle2.report_status(ComponentStatus::Unhealthy).await; + assert_or_retry(|| !registry.get_status().healthy).await; + } + + #[tokio::test] + async fn into_response() { + let nok = HealthStatus::default().into_response(); + assert_eq!(nok.status(), StatusCode::INTERNAL_SERVER_ERROR); + + let ok = HealthStatus { + healthy: true, + components: Default::default(), + } + .into_response(); + assert_eq!(ok.status(), StatusCode::OK); + } +} diff --git a/capture/src/lib.rs b/capture/src/lib.rs index 70f754807bc13..50f670567c33b 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -3,6 +3,7 @@ pub mod billing_limits; pub mod capture; pub mod config; pub mod event; +pub mod health; pub mod prometheus; pub mod redis; pub mod router; diff --git a/capture/src/router.rs b/capture/src/router.rs index 9acc7f4f6f920..bae787c113853 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -9,6 +9,7 @@ use axum::{ use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; use tower_http::trace::TraceLayer; +use crate::health::HealthRegistry; use crate::{billing_limits::BillingLimiter, capture, redis::Client, sink, time::TimeSource}; use crate::prometheus::{setup_metrics_recorder, track_metrics}; @@ -31,6 +32,7 @@ pub fn router< R: Client + Send + Sync + 'static, >( timesource: TZ, + liveness: HealthRegistry, sink: S, redis: Arc, billing: BillingLimiter, @@ -54,6 +56,8 @@ pub fn router< let router = Router::new() // TODO: use NormalizePathLayer::trim_trailing_slash .route("/", get(index)) + .route("/_readiness", get(index)) + .route("/_liveness", get(move || ready(liveness.get_status()))) .route("/i/v0/e", post(capture::event).options(capture::options)) .route("/i/v0/e/", post(capture::event).options(capture::options)) .layer(TraceLayer::new_for_http()) diff --git a/capture/src/server.rs b/capture/src/server.rs index e4372ae0b584a..8c40fd3fa4112 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -6,13 +6,15 @@ use time::Duration; use crate::billing_limits::BillingLimiter; use crate::config::Config; +use crate::health::{ComponentStatus, HealthRegistry}; use crate::redis::RedisClient; use crate::{router, sink}; - pub async fn serve(config: Config, listener: TcpListener, shutdown: F) where F: Future, { + let liveness = HealthRegistry::new("liveness"); + let redis_client = Arc::new(RedisClient::new(config.redis_url).expect("failed to create redis client")); @@ -20,17 +22,28 @@ where .expect("failed to create billing limiter"); let app = if config.print_sink { + // Print sink is only used for local debug, don't allow a container with it to run on prod + liveness + .register("print_sink".to_string(), Duration::seconds(30)) + .await + .report_status(ComponentStatus::Unhealthy) + .await; router::router( crate::time::SystemTime {}, + liveness, sink::PrintSink {}, redis_client, billing, config.export_prometheus, ) } else { - let sink = sink::KafkaSink::new(config.kafka).unwrap(); + let sink_liveness = liveness + .register("rdkafka".to_string(), Duration::seconds(30)) + .await; + let sink = sink::KafkaSink::new(config.kafka, sink_liveness).unwrap(); router::router( crate::time::SystemTime {}, + liveness, sink, redis_client, billing, diff --git a/capture/src/sink.rs b/capture/src/sink.rs index d6b60d82024b4..f044df0047c7f 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -13,6 +13,7 @@ use tracing::{debug, info}; use crate::api::CaptureError; use crate::config::KafkaConfig; use crate::event::ProcessedEvent; +use crate::health::HealthHandle; use crate::prometheus::report_dropped_events; #[async_trait] @@ -45,10 +46,16 @@ impl EventSink for PrintSink { } } -struct KafkaContext; +struct KafkaContext { + liveness: HealthHandle, +} impl rdkafka::ClientContext for KafkaContext { fn stats(&self, stats: rdkafka::Statistics) { + // Signal liveness, as the main rdkafka loop is running and calling us + self.liveness.report_healthy_blocking(); + + // Update exported metrics gauge!("capture_kafka_callback_queue_depth", stats.replyq as f64); gauge!("capture_kafka_producer_queue_depth", stats.msg_cnt as f64); gauge!( @@ -107,7 +114,7 @@ pub struct KafkaSink { } impl KafkaSink { - pub fn new(config: KafkaConfig) -> anyhow::Result { + pub fn new(config: KafkaConfig, liveness: HealthHandle) -> anyhow::Result { info!("connecting to Kafka brokers at {}...", config.kafka_hosts); let mut client_config = ClientConfig::new(); @@ -129,7 +136,7 @@ impl KafkaSink { debug!("rdkafka configuration: {:?}", client_config); let producer: FutureProducer = - client_config.create_with_context(KafkaContext)?; + client_config.create_with_context(KafkaContext { liveness })?; // Ping the cluster to make sure we can reach brokers, fail after 10 seconds _ = producer.client().fetch_metadata( diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index d418996d6b06a..b95c78e48501f 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -7,6 +7,7 @@ use base64::Engine; use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode}; use capture::billing_limits::BillingLimiter; use capture::event::ProcessedEvent; +use capture::health::HealthRegistry; use capture::redis::MockRedisClient; use capture::router::router; use capture::sink::EventSink; @@ -76,6 +77,7 @@ impl EventSink for MemorySink { async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let file = File::open(REQUESTS_DUMP_FILE_NAME)?; let reader = BufReader::new(file); + let liveness = HealthRegistry::new("dummy"); let mut mismatches = 0; @@ -100,7 +102,14 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let billing = BillingLimiter::new(Duration::weeks(1), redis.clone()) .expect("failed to create billing limiter"); - let app = router(timesource, sink.clone(), redis, billing, false); + let app = router( + timesource, + liveness.clone(), + sink.clone(), + redis, + billing, + false, + ); let client = TestClient::new(app); let mut req = client.post(&format!("/i/v0{}", case.path)).body(raw_body); From e56e1f1dd7d7bd3c730165869b39b27618efb34f Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 13 Nov 2023 14:20:01 +0000 Subject: [PATCH 048/249] Add tracing setup (#47) * Add tracing setup * fix --------- Co-authored-by: Xavier Vello --- Cargo.lock | 276 ++++++++++++++++++++++++++++++++- capture-server/Cargo.toml | 8 +- capture-server/src/main.rs | 55 ++++++- capture-server/tests/common.rs | 4 +- capture/src/billing_limits.rs | 4 + capture/src/capture.rs | 5 + capture/src/config.rs | 9 +- capture/src/sink.rs | 4 +- 8 files changed, 352 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 685687f0932e3..fab963c8fe5b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,9 @@ dependencies = [ "envconfig", "futures", "once_cell", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rand", "rdkafka", "reqwest", @@ -246,6 +249,7 @@ dependencies = [ "time", "tokio", "tracing", + "tracing-opentelemetry", "tracing-subscriber", ] @@ -318,6 +322,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-epoch" version = "0.9.15" @@ -608,6 +622,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "governor" version = "0.5.1" @@ -736,6 +756,18 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -873,6 +905,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matchit" version = "0.7.3" @@ -1163,6 +1204,93 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" +dependencies = [ + "futures-core", + "futures-sink", + "indexmap 2.0.2", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", + "urlencoding", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24cda83b20ed2433c68241f918d0f6fdec8b1d43b7a9590ab4420c5095ca930" +dependencies = [ + "async-trait", + "futures-core", + "http", + "opentelemetry", + "opentelemetry-proto", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "prost", + "thiserror", + "tokio", + "tonic", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e155ce5cc812ea3d1dffbd1539aed653de4bf4882d60e6e04dcf0901d674e1" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5774f1ef1f982ef2a447f6ee04ec383981a3ab99c8e77a1a7b30182e65bbc84" +dependencies = [ + "opentelemetry", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b3ce3f5705e2ae493be467a0b23be4bc563c193cdb7713e55372c89a906b34" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "ordered-float", + "percent-encoding", + "rand", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "ordered-float" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536900a8093134cf9ccf00a27deb3532421099e958d9dd431135d0c7543ca1e8" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -1297,6 +1425,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quanta" version = "0.9.3" @@ -1469,8 +1620,17 @@ checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.0", + "regex-syntax 0.8.0", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1481,9 +1641,15 @@ checksum = "5d58da636bd923eae52b7e9120271cbefb16f399069ee566ca5ebf9c30e32238" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.0", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.0" @@ -1878,6 +2044,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.1.0" @@ -1899,6 +2075,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.9" @@ -1930,6 +2117,34 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -1938,9 +2153,13 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "indexmap 1.9.3", "pin-project", "pin-project-lite", + "rand", + "slab", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -2042,18 +2261,51 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c67ac25c5407e7b961fafc6f7e9aa5958fd297aada2d20fa2ae1737357e55596" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log 0.2.0", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-subscriber" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.1.3", ] [[package]] @@ -2103,6 +2355,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" version = "1.4.1" @@ -2240,6 +2498,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57099a701fb3a8043f993e8228dc24229c7b942e2b009a1b962e54489ba1d3bf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml index 19200043f577f..e8aa5595486a6 100644 --- a/capture-server/Cargo.toml +++ b/capture-server/Cargo.toml @@ -7,10 +7,14 @@ edition = "2021" axum = { workspace = true } capture = { path = "../capture" } envconfig = { workspace = true } +opentelemetry = { version = "0.21.0", features = ["trace"]} +opentelemetry-otlp = "0.14.0" +opentelemetry_sdk = { version = "0.21.0", features = ["trace", "rt-tokio"] } time = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-opentelemetry = "0.22.0" +tracing-subscriber = { workspace = true, features = ["env-filter"] } [dev-dependencies] anyhow = { workspace = true, features = [] } @@ -20,4 +24,4 @@ once_cell = "1.18.0" rand = { workspace = true } rdkafka = { workspace = true } reqwest = "0.11.22" -serde_json = { workspace = true } +serde_json = { workspace = true } \ No newline at end of file diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 4874a43a32aa7..402fc3245d883 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -1,7 +1,18 @@ use std::net::TcpListener; +use std::time::Duration; use envconfig::Envconfig; +use opentelemetry::KeyValue; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::trace::{BatchConfig, RandomIdGenerator, Sampler, Tracer}; +use opentelemetry_sdk::{runtime, Resource}; use tokio::signal; +use tracing::level_filters::LevelFilter; +use tracing::Level; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, Layer}; use capture::config::Config; use capture::server::serve; @@ -21,12 +32,50 @@ async fn shutdown() { tracing::info!("Shutting down gracefully..."); } +fn init_tracer(sink_url: &str, sampling_rate: f64) -> Tracer { + opentelemetry_otlp::new_pipeline() + .tracing() + .with_trace_config( + opentelemetry_sdk::trace::Config::default() + .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased( + sampling_rate, + )))) + .with_id_generator(RandomIdGenerator::default()) + .with_resource(Resource::new(vec![KeyValue::new( + "service.name", + "capture", + )])), + ) + .with_batch_config(BatchConfig::default()) + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint(sink_url) + .with_timeout(Duration::from_secs(3)), + ) + .install_batch(runtime::Tokio) + .unwrap() +} + #[tokio::main] async fn main() { - // initialize tracing - tracing_subscriber::fmt::init(); - let config = Config::init_from_env().expect("Invalid configuration:"); + + // Instantiate tracing outputs: + // - stdout with a level configured by the RUST_LOG envvar (default=ERROR) + // - OpenTelemetry if enabled, for levels INFO and higher + let log_layer = tracing_subscriber::fmt::layer().with_filter(EnvFilter::from_default_env()); + let otel_layer = config + .otel_url + .clone() + .map(|url| OpenTelemetryLayer::new(init_tracer(&url, config.otel_sampling_rate))) + .with_filter(LevelFilter::from_level(Level::INFO)); + tracing_subscriber::registry() + .with(log_layer) + .with(otel_layer) + .init(); + + // Open the TCP port and start the server let listener = TcpListener::bind(config.address).unwrap(); serve(config, listener, shutdown()).await } diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index d4665cfedc748..e9329cf857f6d 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -25,7 +25,6 @@ use capture::server::serve; pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { print_sink: false, address: SocketAddr::from_str("127.0.0.1:0").unwrap(), - export_prometheus: false, redis_url: "redis://localhost:6379/".to_string(), kafka: KafkaConfig { kafka_producer_linger_ms: 0, // Send messages as soon as possible @@ -35,6 +34,9 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { kafka_topic: "events_plugin_ingestion".to_string(), kafka_tls: false, }, + otel_url: None, + otel_sampling_rate: 0.0, + export_prometheus: false, }); static TRACING_INIT: Once = Once::new(); diff --git a/capture/src/billing_limits.rs b/capture/src/billing_limits.rs index 5f1540009d0d7..9fa0fdd0e953e 100644 --- a/capture/src/billing_limits.rs +++ b/capture/src/billing_limits.rs @@ -22,10 +22,12 @@ use crate::redis::Client; use thiserror::Error; use time::{Duration, OffsetDateTime}; use tokio::sync::RwLock; +use tracing::instrument; // todo: fetch from env const QUOTA_LIMITER_CACHE_KEY: &str = "@posthog/quota-limits/"; +#[derive(Debug)] pub enum QuotaResource { Events, Recordings, @@ -81,6 +83,7 @@ impl BillingLimiter { }) } + #[instrument(skip_all)] async fn fetch_limited( client: &Arc, resource: QuotaResource, @@ -96,6 +99,7 @@ impl BillingLimiter { .await } + #[instrument(skip_all, fields(key = key))] pub async fn is_limited(&self, key: &str, resource: QuotaResource) -> bool { // hold the read lock to clone it, very briefly. clone is ok because it's very small 🤏 // rwlock can have many readers, but one writer. the writer will wait in a queue with all diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 45fc5f70f889a..dbff970feffc0 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -13,6 +13,7 @@ use base64::Engine; use metrics::counter; use time::OffsetDateTime; +use tracing::instrument; use crate::billing_limits::QuotaResource; use crate::event::ProcessingContext; @@ -25,6 +26,7 @@ use crate::{ utils::uuid_v7, }; +#[instrument(skip_all)] pub async fn event( state: State, InsecureClientIp(ip): InsecureClientIp, @@ -110,6 +112,7 @@ pub async fn options() -> Result, CaptureError> { })) } +#[instrument(skip_all)] pub fn process_single_event( event: &RawEvent, context: &ProcessingContext, @@ -141,6 +144,7 @@ pub fn process_single_event( }) } +#[instrument(skip_all, fields(events = events.len()))] pub fn extract_and_verify_token(events: &[RawEvent]) -> Result { let distinct_tokens: HashSet> = HashSet::from_iter( events @@ -162,6 +166,7 @@ pub fn extract_and_verify_token(events: &[RawEvent]) -> Result( sink: Arc, events: &'a [RawEvent], diff --git a/capture/src/config.rs b/capture/src/config.rs index e3ea1e8461a27..da8377afa3a14 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -9,10 +9,15 @@ pub struct Config { #[envconfig(default = "127.0.0.1:3000")] pub address: SocketAddr, pub redis_url: String, - #[envconfig(default = "true")] - pub export_prometheus: bool, + #[envconfig(nested = true)] pub kafka: KafkaConfig, + + pub otel_url: Option, + #[envconfig(default = "1.0")] + pub otel_sampling_rate: f64, + #[envconfig(default = "true")] + pub export_prometheus: bool, } #[derive(Envconfig, Clone)] diff --git a/capture/src/sink.rs b/capture/src/sink.rs index f044df0047c7f..a7be4588e0a20 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -8,7 +8,7 @@ use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; use rdkafka::producer::Producer; use rdkafka::util::Timeout; use tokio::task::JoinSet; -use tracing::{debug, info}; +use tracing::{debug, info, instrument}; use crate::api::CaptureError; use crate::config::KafkaConfig; @@ -192,6 +192,7 @@ impl KafkaSink { #[async_trait] impl EventSink for KafkaSink { + #[instrument(skip_all)] async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { Self::kafka_send(self.producer.clone(), self.topic.clone(), event).await?; @@ -200,6 +201,7 @@ impl EventSink for KafkaSink { Ok(()) } + #[instrument(skip_all)] async fn send_batch(&self, events: Vec) -> Result<(), CaptureError> { let mut set = JoinSet::new(); let batch_size = events.len(); From 014456dbaafa6e21d579dcabef83546825fd1160 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 13 Nov 2023 15:01:19 +0000 Subject: [PATCH 049/249] feat: add partition limiter + overflow (#48) * Partition limiter * add basic limiting * add tests * fmt * oops * test it more * not needed * Update capture/src/partition_limits.rs Co-authored-by: Xavier Vello * fmt * Update events.rs --------- Co-authored-by: Xavier Vello --- Cargo.lock | 1 + Cargo.toml | 2 +- capture-server/tests/common.rs | 21 ++++++++ capture-server/tests/events.rs | 93 +++++++++++++++++++++++++++++++++ capture/Cargo.toml | 1 + capture/src/capture.rs | 4 +- capture/src/config.rs | 13 ++++- capture/src/lib.rs | 1 + capture/src/partition_limits.rs | 58 ++++++++++++++++++++ capture/src/prometheus.rs | 5 ++ capture/src/server.rs | 8 ++- capture/src/sink.rs | 20 +++++-- 12 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 capture/src/partition_limits.rs diff --git a/Cargo.lock b/Cargo.lock index fab963c8fe5b9..8e3b8737e2daa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,6 +205,7 @@ dependencies = [ "axum-test-helper", "base64", "bytes", + "dashmap", "envconfig", "flate2", "governor", diff --git a/Cargo.toml b/Cargo.toml index 402243e69515c..5a0d5015ee8b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ tracing = "0.1" tracing-subscriber = "0.3" serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" -governor = "0.5.1" +governor = {version = "0.5.1", features=["dashmap"]} tower_governor = "0.0.4" time = { version = "0.3.20", features = ["formatting", "macros", "parsing", "serde"] } tower-http = { version = "0.4.0", features = ["cors", "trace"] } diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index e9329cf857f6d..c0ee9ba9f843a 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -2,6 +2,7 @@ use std::default::Default; use std::net::{SocketAddr, TcpListener}; +use std::num::NonZeroU32; use std::str::FromStr; use std::string::ToString; use std::sync::{Arc, Once}; @@ -26,6 +27,8 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { print_sink: false, address: SocketAddr::from_str("127.0.0.1:0").unwrap(), redis_url: "redis://localhost:6379/".to_string(), + burst_limit: NonZeroU32::new(5).unwrap(), + per_second_limit: NonZeroU32::new(10).unwrap(), kafka: KafkaConfig { kafka_producer_linger_ms: 0, // Send messages as soon as possible kafka_producer_queue_mib: 10, @@ -144,6 +147,24 @@ impl EphemeralTopic { None => bail!("kafka read timeout"), } } + pub fn next_message_key(&self) -> anyhow::Result> { + match self.consumer.poll(self.read_timeout) { + Some(Ok(message)) => { + let key = message.key(); + + if let Some(key) = key { + let key = std::str::from_utf8(key)?; + let key = String::from_str(key)?; + + Ok(Some(key)) + } else { + Ok(None) + } + } + Some(Err(err)) => bail!("kafka read error: {}", err), + None => bail!("kafka read timeout"), + } + } pub fn topic_name(&self) -> &str { &self.topic_name diff --git a/capture-server/tests/events.rs b/capture-server/tests/events.rs index 42facd86554ec..27db9a7c897de 100644 --- a/capture-server/tests/events.rs +++ b/capture-server/tests/events.rs @@ -1,3 +1,5 @@ +use std::num::NonZeroU32; + use anyhow::Result; use assert_json_diff::assert_json_include; use reqwest::StatusCode; @@ -73,3 +75,94 @@ async fn it_captures_a_batch() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn it_is_limited_with_burst() -> Result<()> { + setup_tracing(); + + let token = random_string("token", 16); + let distinct_id = random_string("id", 16); + + let topic = EphemeralTopic::new().await; + + let mut config = DEFAULT_CONFIG.clone(); + config.kafka.kafka_topic = topic.topic_name().to_string(); + config.burst_limit = NonZeroU32::new(2).unwrap(); + config.per_second_limit = NonZeroU32::new(1).unwrap(); + + let server = ServerHandle::for_config(config); + + let event = json!([{ + "token": token, + "event": "event1", + "distinct_id": distinct_id + },{ + "token": token, + "event": "event2", + "distinct_id": distinct_id + },{ + "token": token, + "event": "event3", + "distinct_id": distinct_id + }]); + + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + assert_eq!( + topic.next_message_key()?.unwrap(), + format!("{}:{}", token, distinct_id) + ); + + assert_eq!( + topic.next_message_key()?.unwrap(), + format!("{}:{}", token, distinct_id) + ); + + assert_eq!(topic.next_message_key()?, None); + + Ok(()) +} + +#[tokio::test] +async fn it_does_not_partition_limit_different_ids() -> Result<()> { + setup_tracing(); + + let token = random_string("token", 16); + let distinct_id = random_string("id", 16); + let distinct_id2 = random_string("id", 16); + + let topic = EphemeralTopic::new().await; + + let mut config = DEFAULT_CONFIG.clone(); + config.kafka.kafka_topic = topic.topic_name().to_string(); + config.burst_limit = NonZeroU32::new(1).unwrap(); + config.per_second_limit = NonZeroU32::new(1).unwrap(); + + let server = ServerHandle::for_config(config); + + let event = json!([{ + "token": token, + "event": "event1", + "distinct_id": distinct_id + },{ + "token": token, + "event": "event2", + "distinct_id": distinct_id2 + }]); + + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + assert_eq!( + topic.next_message_key()?.unwrap(), + format!("{}:{}", token, distinct_id) + ); + + assert_eq!( + topic.next_message_key()?.unwrap(), + format!("{}:{}", token, distinct_id2) + ); + + Ok(()) +} diff --git a/capture/Cargo.toml b/capture/Cargo.toml index 6bdfc39b20216..3b2b100074a60 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -31,6 +31,7 @@ metrics-exporter-prometheus = { workspace = true } thiserror = { workspace = true } redis = { version="0.23.3", features=["tokio-comp", "cluster", "cluster-async"] } envconfig = { workspace = true } +dashmap = "5.5.3" [dev-dependencies] assert-json-diff = { workspace = true } diff --git a/capture/src/capture.rs b/capture/src/capture.rs index dbff970feffc0..83466eb7653a5 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -78,12 +78,12 @@ pub async fn event( client_ip: ip.to_string(), }; - let limited = state + let billing_limited = state .billing .is_limited(context.token.as_str(), QuotaResource::Events) .await; - if limited { + if billing_limited { report_dropped_events("over_quota", 1); // for v0 we want to just return ok 🙃 diff --git a/capture/src/config.rs b/capture/src/config.rs index da8377afa3a14..8a471b3d288d6 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::{net::SocketAddr, num::NonZeroU32}; use envconfig::Envconfig; @@ -6,16 +6,25 @@ use envconfig::Envconfig; pub struct Config { #[envconfig(default = "false")] pub print_sink: bool, + #[envconfig(default = "127.0.0.1:3000")] pub address: SocketAddr, + pub redis_url: String, + pub otel_url: Option, + + #[envconfig(default = "100")] + pub per_second_limit: NonZeroU32, + + #[envconfig(default = "1000")] + pub burst_limit: NonZeroU32, #[envconfig(nested = true)] pub kafka: KafkaConfig, - pub otel_url: Option, #[envconfig(default = "1.0")] pub otel_sampling_rate: f64, + #[envconfig(default = "true")] pub export_prometheus: bool, } diff --git a/capture/src/lib.rs b/capture/src/lib.rs index 50f670567c33b..eea915c307f71 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -4,6 +4,7 @@ pub mod capture; pub mod config; pub mod event; pub mod health; +pub mod partition_limits; pub mod prometheus; pub mod redis; pub mod router; diff --git a/capture/src/partition_limits.rs b/capture/src/partition_limits.rs new file mode 100644 index 0000000000000..fe63ec157141c --- /dev/null +++ b/capture/src/partition_limits.rs @@ -0,0 +1,58 @@ +/// When a customer is writing too often to the same key, we get hot partitions. This negatively +/// affects our write latency and cluster health. We try to provide ordering guarantees wherever +/// possible, but this does require that we map key -> partition. +/// +/// If the write-rate reaches a certain amount, we need to be able to handle the hot partition +/// before it causes a negative impact. In this case, instead of passing the error to the customer +/// with a 429, we relax our ordering constraints and temporarily override the key, meaning the +/// customers data will be spread across all partitions. +use std::{num::NonZeroU32, sync::Arc}; + +use governor::{clock, state::keyed::DefaultKeyedStateStore, Quota, RateLimiter}; + +// See: https://docs.rs/governor/latest/governor/_guide/index.html#usage-in-multiple-threads +#[derive(Clone)] +pub struct PartitionLimiter { + limiter: Arc, clock::DefaultClock>>, +} + +impl PartitionLimiter { + pub fn new(per_second: NonZeroU32, burst: NonZeroU32) -> Self { + let quota = Quota::per_second(per_second).allow_burst(burst); + let limiter = Arc::new(governor::RateLimiter::dashmap(quota)); + + PartitionLimiter { limiter } + } + + pub fn is_limited(&self, key: &String) -> bool { + self.limiter.check_key(key).is_err() + } +} + +#[cfg(test)] +mod tests { + use crate::partition_limits::PartitionLimiter; + use std::num::NonZeroU32; + + #[tokio::test] + async fn low_limits() { + let limiter = + PartitionLimiter::new(NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap()); + let token = String::from("test"); + + assert!(!limiter.is_limited(&token)); + assert!(limiter.is_limited(&token)); + } + + #[tokio::test] + async fn bursting() { + let limiter = + PartitionLimiter::new(NonZeroU32::new(1).unwrap(), NonZeroU32::new(3).unwrap()); + let token = String::from("test"); + + assert!(!limiter.is_limited(&token)); + assert!(!limiter.is_limited(&token)); + assert!(!limiter.is_limited(&token)); + assert!(limiter.is_limited(&token)); + } +} diff --git a/capture/src/prometheus.rs b/capture/src/prometheus.rs index d9dbea8831703..f2255200f1c2f 100644 --- a/capture/src/prometheus.rs +++ b/capture/src/prometheus.rs @@ -9,6 +9,11 @@ use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; pub fn report_dropped_events(cause: &'static str, quantity: u64) { counter!("capture_events_dropped_total", quantity, "cause" => cause); } + +pub fn report_overflow_partition(quantity: u64) { + counter!("capture_partition_key_capacity_exceeded_total", quantity); +} + pub fn setup_metrics_recorder() -> PrometheusHandle { // Ok I broke it at the end, but the limit on our ingress is 60 and that's a nicer way of reaching it const EXPONENTIAL_SECONDS: &[f64] = &[ diff --git a/capture/src/server.rs b/capture/src/server.rs index 8c40fd3fa4112..c84bd20c94ca6 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -7,8 +7,10 @@ use time::Duration; use crate::billing_limits::BillingLimiter; use crate::config::Config; use crate::health::{ComponentStatus, HealthRegistry}; +use crate::partition_limits::PartitionLimiter; use crate::redis::RedisClient; use crate::{router, sink}; + pub async fn serve(config: Config, listener: TcpListener, shutdown: F) where F: Future, @@ -28,6 +30,7 @@ where .await .report_status(ComponentStatus::Unhealthy) .await; + router::router( crate::time::SystemTime {}, liveness, @@ -40,7 +43,10 @@ where let sink_liveness = liveness .register("rdkafka".to_string(), Duration::seconds(30)) .await; - let sink = sink::KafkaSink::new(config.kafka, sink_liveness).unwrap(); + + let partition = PartitionLimiter::new(config.per_second_limit, config.burst_limit); + let sink = sink::KafkaSink::new(config.kafka, sink_liveness, partition).unwrap(); + router::router( crate::time::SystemTime {}, liveness, diff --git a/capture/src/sink.rs b/capture/src/sink.rs index a7be4588e0a20..9d915b0e15b2b 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -14,6 +14,7 @@ use crate::api::CaptureError; use crate::config::KafkaConfig; use crate::event::ProcessedEvent; use crate::health::HealthHandle; +use crate::partition_limits::PartitionLimiter; use crate::prometheus::report_dropped_events; #[async_trait] @@ -111,10 +112,15 @@ impl rdkafka::ClientContext for KafkaContext { pub struct KafkaSink { producer: FutureProducer, topic: String, + partition: PartitionLimiter, } impl KafkaSink { - pub fn new(config: KafkaConfig, liveness: HealthHandle) -> anyhow::Result { + pub fn new( + config: KafkaConfig, + liveness: HealthHandle, + partition: PartitionLimiter, + ) -> anyhow::Result { info!("connecting to Kafka brokers at {}...", config.kafka_hosts); let mut client_config = ClientConfig::new(); @@ -147,6 +153,7 @@ impl KafkaSink { Ok(KafkaSink { producer, + partition, topic: config.kafka_topic, }) } @@ -157,6 +164,7 @@ impl KafkaSink { producer: FutureProducer, topic: String, event: ProcessedEvent, + limited: bool, ) -> Result<(), CaptureError> { let payload = serde_json::to_string(&event).map_err(|e| { tracing::error!("failed to serialize event: {}", e); @@ -164,12 +172,13 @@ impl KafkaSink { })?; let key = event.key(); + let partition_key = if limited { None } else { Some(key.as_str()) }; match producer.send_result(FutureRecord { topic: topic.as_str(), payload: Some(&payload), partition: None, - key: Some(&key), + key: partition_key, timestamp: None, headers: None, }) { @@ -194,10 +203,12 @@ impl KafkaSink { impl EventSink for KafkaSink { #[instrument(skip_all)] async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { - Self::kafka_send(self.producer.clone(), self.topic.clone(), event).await?; + let limited = self.partition.is_limited(&event.key()); + Self::kafka_send(self.producer.clone(), self.topic.clone(), event, limited).await?; histogram!("capture_event_batch_size", 1.0); counter!("capture_events_ingested_total", 1); + Ok(()) } @@ -209,7 +220,8 @@ impl EventSink for KafkaSink { let producer = self.producer.clone(); let topic = self.topic.clone(); - set.spawn(Self::kafka_send(producer, topic, event)); + let limited = self.partition.is_limited(&event.key()); + set.spawn(Self::kafka_send(producer, topic, event, limited)); } // Await on all the produce promises From 0b1b0a2ab534a20532581df794c300d9755fb801 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 14 Nov 2023 14:55:49 +0100 Subject: [PATCH 050/249] tracing: add batch_size and token to root span (#49) --- capture/src/capture.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 83466eb7653a5..4177ad13f35be 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -26,7 +26,7 @@ use crate::{ utils::uuid_v7, }; -#[instrument(skip_all)] +#[instrument(skip_all, fields(token, batch_size))] pub async fn event( state: State, InsecureClientIp(ip): InsecureClientIp, @@ -48,6 +48,8 @@ pub async fn event( _ => RawEvent::from_bytes(&meta, body), }?; + tracing::Span::current().record("batch_size", events.len()); + if events.is_empty() { return Err(CaptureError::EmptyBatch); } @@ -57,6 +59,8 @@ pub async fn event( err })?; + tracing::Span::current().record("token", &token); + counter!("capture_events_received_total", events.len() as u64); let sent_at = meta.sent_at.and_then(|value| { From 3e70e7106b10e6eff94c842e0f4f3ba0dde1034f Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 20 Nov 2023 12:54:03 +0100 Subject: [PATCH 051/249] route GETs to the legacy endpoint too (#53) --- capture/src/router.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/capture/src/router.rs b/capture/src/router.rs index bae787c113853..6f2f044f88c67 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -58,8 +58,18 @@ pub fn router< .route("/", get(index)) .route("/_readiness", get(index)) .route("/_liveness", get(move || ready(liveness.get_status()))) - .route("/i/v0/e", post(capture::event).options(capture::options)) - .route("/i/v0/e/", post(capture::event).options(capture::options)) + .route( + "/i/v0/e", + post(capture::event) + .get(capture::event) + .options(capture::options), + ) + .route( + "/i/v0/e/", + post(capture::event) + .get(capture::event) + .options(capture::options), + ) .layer(TraceLayer::new_for_http()) .layer(cors) .layer(axum::middleware::from_fn(track_metrics)) From b68bd7e338c25ce0e6ff373e3edc41892cbd75bd Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 21 Nov 2023 15:08:32 +0100 Subject: [PATCH 052/249] fix: increment over_quota by batch size instead of 1 (#56) --- capture/src/capture.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 4177ad13f35be..39596da57a891 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -88,7 +88,7 @@ pub async fn event( .await; if billing_limited { - report_dropped_events("over_quota", 1); + report_dropped_events("over_quota", events.len() as u64); // for v0 we want to just return ok 🙃 // this is because the clients are pretty dumb and will just retry over and over and From f9fe9f11d83ca154fd87281cb9ec7a26c19acd78 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 21 Nov 2023 14:12:48 +0000 Subject: [PATCH 053/249] add more span attributes (#55) * add more spans * adjust * adjust again * fmt * bleh --- capture/src/capture.rs | 44 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 39596da57a891..d1d302ece64e5 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -16,7 +16,7 @@ use time::OffsetDateTime; use tracing::instrument; use crate::billing_limits::QuotaResource; -use crate::event::ProcessingContext; +use crate::event::{Compression, ProcessingContext}; use crate::prometheus::report_dropped_events; use crate::token::validate_token; use crate::{ @@ -26,7 +26,18 @@ use crate::{ utils::uuid_v7, }; -#[instrument(skip_all, fields(token, batch_size))] +#[instrument( + skip_all, + fields( + token, + batch_size, + user_agent, + content_encoding, + content_type, + version, + compression + ) +)] pub async fn event( state: State, InsecureClientIp(ip): InsecureClientIp, @@ -34,21 +45,48 @@ pub async fn event( headers: HeaderMap, body: Bytes, ) -> Result, CaptureError> { + // content-type + // user-agent + + let user_agent = headers + .get("user_agent") + .map_or("unknown", |v| v.to_str().unwrap_or("unknown")); + let content_encoding = headers + .get("content_encoding") + .map_or("unknown", |v| v.to_str().unwrap_or("unknown")); + + tracing::Span::current().record("user_agent", user_agent); + tracing::Span::current().record("content_encoding", content_encoding); + let events = match headers .get("content-type") .map_or("", |v| v.to_str().unwrap_or("")) { "application/x-www-form-urlencoded" => { + tracing::Span::current().record("content_type", "application/x-www-form-urlencoded"); + let input: EventFormData = serde_urlencoded::from_bytes(body.deref()).unwrap(); let payload = base64::engine::general_purpose::STANDARD .decode(input.data) .unwrap(); RawEvent::from_bytes(&meta, payload.into()) } - _ => RawEvent::from_bytes(&meta, body), + ct => { + tracing::Span::current().record("content_type", ct); + + RawEvent::from_bytes(&meta, body) + } }?; + let comp = match meta.compression { + None => String::from("unknown"), + Some(Compression::Gzip) => String::from("gzip"), + Some(Compression::Unsupported) => String::from("unsupported"), + }; + tracing::Span::current().record("batch_size", events.len()); + tracing::Span::current().record("version", meta.lib_version.clone()); + tracing::Span::current().record("compression", comp.as_str()); if events.is_empty() { return Err(CaptureError::EmptyBatch); From 6cee0a57c1522f0b2dc0e1dd3d83208a957ea3dc Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 21 Nov 2023 14:31:41 +0000 Subject: [PATCH 054/249] fix new span setup (#57) * fix spans * fmt * add method --- capture/src/capture.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index d1d302ece64e5..1d889d371b3ae 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -7,7 +7,7 @@ use bytes::Bytes; use axum::Json; // TODO: stream this instead use axum::extract::{Query, State}; -use axum::http::HeaderMap; +use axum::http::{HeaderMap, Method}; use axum_client_ip::InsecureClientIp; use base64::Engine; use metrics::counter; @@ -43,20 +43,30 @@ pub async fn event( InsecureClientIp(ip): InsecureClientIp, meta: Query, headers: HeaderMap, + method: Method, body: Bytes, ) -> Result, CaptureError> { // content-type // user-agent let user_agent = headers - .get("user_agent") + .get("user-agent") .map_or("unknown", |v| v.to_str().unwrap_or("unknown")); let content_encoding = headers - .get("content_encoding") + .get("content-encoding") .map_or("unknown", |v| v.to_str().unwrap_or("unknown")); + let comp = match meta.compression { + None => String::from("unknown"), + Some(Compression::Gzip) => String::from("gzip"), + Some(Compression::Unsupported) => String::from("unsupported"), + }; + tracing::Span::current().record("user_agent", user_agent); tracing::Span::current().record("content_encoding", content_encoding); + tracing::Span::current().record("version", meta.lib_version.clone()); + tracing::Span::current().record("compression", comp.as_str()); + tracing::Span::current().record("method", method.as_str()); let events = match headers .get("content-type") @@ -78,15 +88,7 @@ pub async fn event( } }?; - let comp = match meta.compression { - None => String::from("unknown"), - Some(Compression::Gzip) => String::from("gzip"), - Some(Compression::Unsupported) => String::from("unsupported"), - }; - tracing::Span::current().record("batch_size", events.len()); - tracing::Span::current().record("version", meta.lib_version.clone()); - tracing::Span::current().record("compression", comp.as_str()); if events.is_empty() { return Err(CaptureError::EmptyBatch); From f8a07fbfa3dead0b73cad250ffa4b2db0440ae1d Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 21 Nov 2023 16:36:02 +0100 Subject: [PATCH 055/249] account for process_events errors in capture_events_dropped_total and log them (#58) --- capture/src/capture.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 1d889d371b3ae..6935ee3ff6e19 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -143,7 +143,11 @@ pub async fn event( tracing::debug!(context=?context, events=?events, "decoded request"); - process_events(state.sink.clone(), &events, &context).await?; + if let Err(err) = process_events(state.sink.clone(), &events, &context).await { + report_dropped_events("process_events_error", events.len() as u64); + tracing::log::warn!("rejected invalid payload: {}", err); + return Err(err); + } Ok(Json(CaptureResponse { status: CaptureResponseCode::Ok, From 7228307d2319d6c8d63362510795313d1c71463a Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 27 Nov 2023 13:55:08 +0100 Subject: [PATCH 056/249] properly process Kafka produce ACKs to propagate errors (#59) --- Cargo.lock | 8 +- Cargo.toml | 4 +- capture-server/tests/common.rs | 15 ++- capture/src/capture.rs | 5 +- capture/src/config.rs | 2 + capture/src/server.rs | 3 +- capture/src/sink.rs | 202 +++++++++++++++++++++++++++++---- 7 files changed, 207 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e3b8737e2daa..6bf02dfc43650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1531,9 +1531,9 @@ dependencies = [ [[package]] name = "rdkafka" -version = "0.34.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053adfa02fab06e86c01d586cc68aa47ee0ff4489a59469081dc12cbcde578bf" +checksum = "d54f02a5a40220f8a2dfa47ddb38ba9064475a5807a69504b6f91711df2eea63" dependencies = [ "futures-channel", "futures-util", @@ -1549,9 +1549,9 @@ dependencies = [ [[package]] name = "rdkafka-sys" -version = "4.6.0+2.2.0" +version = "4.7.0+2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad63c279fca41a27c231c450a2d2ad18288032e9cbb159ad16c9d96eba35aaaf" +checksum = "55e0d2f9ba6253f6ec72385e453294f8618e9e15c2c6aba2a5c01ccf9622d615" dependencies = [ "cmake", "libc", diff --git a/Cargo.toml b/Cargo.toml index 5a0d5015ee8b5..983cbf2b7d696 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,6 @@ [workspace] +resolver = "2" + members = [ "capture", "capture-server" @@ -25,7 +27,7 @@ uuid = { version = "1.3.3", features = ["serde"] } async-trait = "0.1.68" serde_urlencoded = "0.7.1" rand = "0.8.5" -rdkafka = { version = "0.34", features = ["cmake-build", "ssl"] } +rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl"] } metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" thiserror = "1.0.48" diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index c0ee9ba9f843a..d74b5bf9eed2b 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -18,7 +18,8 @@ use rdkafka::consumer::{BaseConsumer, Consumer}; use rdkafka::util::Timeout; use rdkafka::{Message, TopicPartitionList}; use tokio::sync::Notify; -use tracing::debug; +use tokio::time::timeout; +use tracing::{debug, warn}; use capture::config::{Config, KafkaConfig}; use capture::server::serve; @@ -32,6 +33,7 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { kafka: KafkaConfig { kafka_producer_linger_ms: 0, // Send messages as soon as possible kafka_producer_queue_mib: 10, + kafka_message_timeout_ms: 10000, // 10s, ACKs can be slow on low volumes, should be tuned kafka_compression_codec: "none".to_string(), kafka_hosts: "kafka:9092".to_string(), kafka_topic: "events_plugin_ingestion".to_string(), @@ -174,9 +176,14 @@ impl EphemeralTopic { impl Drop for EphemeralTopic { fn drop(&mut self) { debug!("dropping EphemeralTopic {}...", self.topic_name); - _ = self.consumer.unassign(); - futures::executor::block_on(delete_topic(self.topic_name.clone())); - debug!("dropped topic"); + self.consumer.unsubscribe(); + match futures::executor::block_on(timeout( + Duration::from_secs(10), + delete_topic(self.topic_name.clone()), + )) { + Ok(_) => debug!("dropped topic"), + Err(err) => warn!("failed to drop topic: {}", err), + } } } diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 6935ee3ff6e19..da4a72693f51f 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -228,11 +228,10 @@ pub async fn process_events<'a>( tracing::debug!(events=?events, "processed {} events", events.len()); if events.len() == 1 { - sink.send(events[0].clone()).await?; + sink.send(events[0].clone()).await } else { - sink.send_batch(events).await?; + sink.send_batch(events).await } - Ok(()) } #[cfg(test)] diff --git a/capture/src/config.rs b/capture/src/config.rs index 8a471b3d288d6..d69d4a96262f9 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -35,6 +35,8 @@ pub struct KafkaConfig { pub kafka_producer_linger_ms: u32, // Maximum time between producer batches during low traffic #[envconfig(default = "400")] pub kafka_producer_queue_mib: u32, // Size of the in-memory producer queue in mebibytes + #[envconfig(default = "20000")] + pub kafka_message_timeout_ms: u32, // Time before we stop retrying producing a message: 20 seconds #[envconfig(default = "none")] pub kafka_compression_codec: String, // none, gzip, snappy, lz4, zstd pub kafka_hosts: String, diff --git a/capture/src/server.rs b/capture/src/server.rs index c84bd20c94ca6..ad9150907df98 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -45,7 +45,8 @@ where .await; let partition = PartitionLimiter::new(config.per_second_limit, config.burst_limit); - let sink = sink::KafkaSink::new(config.kafka, sink_liveness, partition).unwrap(); + let sink = sink::KafkaSink::new(config.kafka, sink_liveness, partition) + .expect("failed to start Kafka sink"); router::router( crate::time::SystemTime {}, diff --git a/capture/src/sink.rs b/capture/src/sink.rs index 9d915b0e15b2b..c1d291a275628 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -3,12 +3,13 @@ use std::time::Duration; use async_trait::async_trait; use metrics::{absolute_counter, counter, gauge, histogram}; use rdkafka::config::ClientConfig; -use rdkafka::error::RDKafkaErrorCode; +use rdkafka::error::{KafkaError, RDKafkaErrorCode}; use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; -use rdkafka::producer::Producer; +use rdkafka::producer::{DeliveryFuture, Producer}; use rdkafka::util::Timeout; use tokio::task::JoinSet; -use tracing::{debug, info, instrument}; +use tracing::instrument; +use tracing::log::{debug, error, info}; use crate::api::CaptureError; use crate::config::KafkaConfig; @@ -128,6 +129,10 @@ impl KafkaSink { .set("bootstrap.servers", &config.kafka_hosts) .set("statistics.interval.ms", "10000") .set("linger.ms", config.kafka_producer_linger_ms.to_string()) + .set( + "message.timeout.ms", + config.kafka_message_timeout_ms.to_string(), + ) .set("compression.codec", config.kafka_compression_codec) .set( "queue.buffering.max.kbytes", @@ -157,17 +162,20 @@ impl KafkaSink { topic: config.kafka_topic, }) } -} -impl KafkaSink { + pub fn flush(&self) -> Result<(), KafkaError> { + // TODO: hook it up on shutdown + self.producer.flush(Duration::new(30, 0)) + } + async fn kafka_send( producer: FutureProducer, topic: String, event: ProcessedEvent, limited: bool, - ) -> Result<(), CaptureError> { + ) -> Result { let payload = serde_json::to_string(&event).map_err(|e| { - tracing::error!("failed to serialize event: {}", e); + error!("failed to serialize event: {}", e); CaptureError::NonRetryableSinkError })?; @@ -182,7 +190,7 @@ impl KafkaSink { timestamp: None, headers: None, }) { - Ok(_) => Ok(()), + Ok(ack) => Ok(ack), Err((e, _)) => match e.rdkafka_error_code() { Some(RDKafkaErrorCode::InvalidMessageSize) => { report_dropped_events("kafka_message_size", 1); @@ -191,12 +199,38 @@ impl KafkaSink { _ => { // TODO(maybe someday): Don't drop them but write them somewhere and try again report_dropped_events("kafka_write_error", 1); - tracing::error!("failed to produce event: {}", e); + error!("failed to produce event: {}", e); Err(CaptureError::RetryableSinkError) } }, } } + + async fn process_ack(delivery: DeliveryFuture) -> Result<(), CaptureError> { + match delivery.await { + Err(_) => { + // Cancelled due to timeout while retrying + counter!("capture_kafka_produce_errors_total", 1); + error!("failed to produce to Kafka before write timeout"); + Err(CaptureError::RetryableSinkError) + } + Ok(Err((KafkaError::MessageProduction(RDKafkaErrorCode::MessageSizeTooLarge), _))) => { + // Rejected by broker due to message size + report_dropped_events("kafka_message_size", 1); + Err(CaptureError::EventTooBig) + } + Ok(Err((err, _))) => { + // Unretriable produce error + counter!("capture_kafka_produce_errors_total", 1); + error!("failed to produce to Kafka: {}", err); + Err(CaptureError::RetryableSinkError) + } + Ok(Ok(_)) => { + counter!("capture_events_ingested_total", 1); + Ok(()) + } + } + } } #[async_trait] @@ -204,12 +238,10 @@ impl EventSink for KafkaSink { #[instrument(skip_all)] async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { let limited = self.partition.is_limited(&event.key()); - Self::kafka_send(self.producer.clone(), self.topic.clone(), event, limited).await?; - + let ack = + Self::kafka_send(self.producer.clone(), self.topic.clone(), event, limited).await?; histogram!("capture_event_batch_size", 1.0); - counter!("capture_events_ingested_total", 1); - - Ok(()) + Self::process_ack(ack).await } #[instrument(skip_all)] @@ -219,16 +251,148 @@ impl EventSink for KafkaSink { for event in events { let producer = self.producer.clone(); let topic = self.topic.clone(); - let limited = self.partition.is_limited(&event.key()); - set.spawn(Self::kafka_send(producer, topic, event, limited)); + + // We await kafka_send to get events in the producer queue sequentially + let ack = Self::kafka_send(producer, topic, event, limited).await?; + + // Then stash the returned DeliveryFuture, waiting concurrently for the write ACKs from brokers. + set.spawn(Self::process_ack(ack)); } - // Await on all the produce promises - while (set.join_next().await).is_some() {} + // Await on all the produce promises, fail batch on first failure + while let Some(res) = set.join_next().await { + match res { + Ok(Ok(_)) => {} + Ok(Err(err)) => { + set.abort_all(); + return Err(err); + } + Err(err) => { + set.abort_all(); + error!("join error while waiting on Kafka ACK: {:?}", err); + return Err(CaptureError::RetryableSinkError); + } + } + } histogram!("capture_event_batch_size", batch_size as f64); - counter!("capture_events_ingested_total", batch_size as u64); Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::api::CaptureError; + use crate::config; + use crate::event::ProcessedEvent; + use crate::health::HealthRegistry; + use crate::partition_limits::PartitionLimiter; + use crate::sink::{EventSink, KafkaSink}; + use crate::utils::uuid_v7; + use rdkafka::mocking::MockCluster; + use rdkafka::producer::DefaultProducerContext; + use rdkafka::types::{RDKafkaApiKey, RDKafkaRespErr}; + use std::num::NonZeroU32; + use time::Duration; + + async fn start_on_mocked_sink() -> (MockCluster<'static, DefaultProducerContext>, KafkaSink) { + let registry = HealthRegistry::new("liveness"); + let handle = registry + .register("one".to_string(), Duration::seconds(30)) + .await; + let limiter = + PartitionLimiter::new(NonZeroU32::new(10).unwrap(), NonZeroU32::new(10).unwrap()); + let cluster = MockCluster::new(1).expect("failed to create mock brokers"); + let config = config::KafkaConfig { + kafka_producer_linger_ms: 0, + kafka_producer_queue_mib: 50, + kafka_message_timeout_ms: 500, + kafka_compression_codec: "none".to_string(), + kafka_hosts: cluster.bootstrap_servers(), + kafka_topic: "events_plugin_ingestion".to_string(), + kafka_tls: false, + }; + let sink = KafkaSink::new(config, handle, limiter).expect("failed to create sink"); + (cluster, sink) + } + + #[tokio::test] + async fn kafka_sink_error_handling() { + // Uses a mocked Kafka broker that allows injecting write errors, to check error handling. + // We test different cases in a single test to amortize the startup cost of the producer. + + let (cluster, sink) = start_on_mocked_sink().await; + let event: ProcessedEvent = ProcessedEvent { + uuid: uuid_v7(), + distinct_id: "id1".to_string(), + ip: "".to_string(), + data: "".to_string(), + now: "".to_string(), + sent_at: None, + token: "token1".to_string(), + }; + + // Wait for producer to be healthy, to keep kafka_message_timeout_ms short and tests faster + for _ in 0..20 { + if sink.send(event.clone()).await.is_ok() { + break; + } + } + + // Send events to confirm happy path + sink.send(event.clone()) + .await + .expect("failed to send one initial event"); + sink.send_batch(vec![event.clone(), event.clone()]) + .await + .expect("failed to send initial event batch"); + + // Simulate unretriable errors + cluster.clear_request_errors(RDKafkaApiKey::Produce); + let err = [RDKafkaRespErr::RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE; 1]; + cluster.request_errors(RDKafkaApiKey::Produce, &err); + match sink.send(event.clone()).await { + Err(CaptureError::EventTooBig) => {} // Expected + Err(err) => panic!("wrong error code {}", err), + Ok(()) => panic!("should have errored"), + }; + cluster.clear_request_errors(RDKafkaApiKey::Produce); + let err = [RDKafkaRespErr::RD_KAFKA_RESP_ERR_INVALID_PARTITIONS; 1]; + cluster.request_errors(RDKafkaApiKey::Produce, &err); + match sink.send_batch(vec![event.clone(), event.clone()]).await { + Err(CaptureError::RetryableSinkError) => {} // Expected + Err(err) => panic!("wrong error code {}", err), + Ok(()) => panic!("should have errored"), + }; + + // Simulate transient errors, messages should go through OK + cluster.clear_request_errors(RDKafkaApiKey::Produce); + let err = [RDKafkaRespErr::RD_KAFKA_RESP_ERR_BROKER_NOT_AVAILABLE; 2]; + cluster.request_errors(RDKafkaApiKey::Produce, &err); + sink.send(event.clone()) + .await + .expect("failed to send one event after recovery"); + cluster.clear_request_errors(RDKafkaApiKey::Produce); + let err = [RDKafkaRespErr::RD_KAFKA_RESP_ERR_BROKER_NOT_AVAILABLE; 2]; + cluster.request_errors(RDKafkaApiKey::Produce, &err); + sink.send_batch(vec![event.clone(), event.clone()]) + .await + .expect("failed to send event batch after recovery"); + + // Timeout on a sustained transient error + cluster.clear_request_errors(RDKafkaApiKey::Produce); + let err = [RDKafkaRespErr::RD_KAFKA_RESP_ERR_BROKER_NOT_AVAILABLE; 50]; + cluster.request_errors(RDKafkaApiKey::Produce, &err); + match sink.send(event.clone()).await { + Err(CaptureError::RetryableSinkError) => {} // Expected + Err(err) => panic!("wrong error code {}", err), + Ok(()) => panic!("should have errored"), + }; + match sink.send_batch(vec![event.clone(), event.clone()]).await { + Err(CaptureError::RetryableSinkError) => {} // Expected + Err(err) => panic!("wrong error code {}", err), + Ok(()) => panic!("should have errored"), + }; + } +} From 7056e808d91747029cc28d0f097cb7ed523ff817 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 27 Nov 2023 14:05:12 +0100 Subject: [PATCH 057/249] feat(overflow): add overflow_forced_keys envvar (#54) --- capture-server/tests/common.rs | 1 + capture/src/config.rs | 2 ++ capture/src/partition_limits.rs | 55 ++++++++++++++++++++++++++++----- capture/src/server.rs | 6 +++- capture/src/sink.rs | 7 +++-- 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index d74b5bf9eed2b..b71fdf62c9a35 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -30,6 +30,7 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { redis_url: "redis://localhost:6379/".to_string(), burst_limit: NonZeroU32::new(5).unwrap(), per_second_limit: NonZeroU32::new(10).unwrap(), + overflow_forced_keys: None, kafka: KafkaConfig { kafka_producer_linger_ms: 0, // Send messages as soon as possible kafka_producer_queue_mib: 10, diff --git a/capture/src/config.rs b/capture/src/config.rs index d69d4a96262f9..69a085dd927b0 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -19,6 +19,8 @@ pub struct Config { #[envconfig(default = "1000")] pub burst_limit: NonZeroU32, + pub overflow_forced_keys: Option, // Coma-delimited keys + #[envconfig(nested = true)] pub kafka: KafkaConfig, diff --git a/capture/src/partition_limits.rs b/capture/src/partition_limits.rs index fe63ec157141c..386665780ad1c 100644 --- a/capture/src/partition_limits.rs +++ b/capture/src/partition_limits.rs @@ -6,7 +6,9 @@ /// before it causes a negative impact. In this case, instead of passing the error to the customer /// with a 429, we relax our ordering constraints and temporarily override the key, meaning the /// customers data will be spread across all partitions. -use std::{num::NonZeroU32, sync::Arc}; +use std::collections::HashSet; +use std::num::NonZeroU32; +use std::sync::Arc; use governor::{clock, state::keyed::DefaultKeyedStateStore, Quota, RateLimiter}; @@ -14,18 +16,27 @@ use governor::{clock, state::keyed::DefaultKeyedStateStore, Quota, RateLimiter}; #[derive(Clone)] pub struct PartitionLimiter { limiter: Arc, clock::DefaultClock>>, + forced_keys: HashSet, } impl PartitionLimiter { - pub fn new(per_second: NonZeroU32, burst: NonZeroU32) -> Self { + pub fn new(per_second: NonZeroU32, burst: NonZeroU32, forced_keys: Option) -> Self { let quota = Quota::per_second(per_second).allow_burst(burst); let limiter = Arc::new(governor::RateLimiter::dashmap(quota)); - PartitionLimiter { limiter } + let forced_keys: HashSet = match forced_keys { + None => HashSet::new(), + Some(values) => values.split(',').map(String::from).collect(), + }; + + PartitionLimiter { + limiter, + forced_keys, + } } pub fn is_limited(&self, key: &String) -> bool { - self.limiter.check_key(key).is_err() + self.forced_keys.contains(key) || self.limiter.check_key(key).is_err() } } @@ -36,8 +47,11 @@ mod tests { #[tokio::test] async fn low_limits() { - let limiter = - PartitionLimiter::new(NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap()); + let limiter = PartitionLimiter::new( + NonZeroU32::new(1).unwrap(), + NonZeroU32::new(1).unwrap(), + None, + ); let token = String::from("test"); assert!(!limiter.is_limited(&token)); @@ -46,8 +60,11 @@ mod tests { #[tokio::test] async fn bursting() { - let limiter = - PartitionLimiter::new(NonZeroU32::new(1).unwrap(), NonZeroU32::new(3).unwrap()); + let limiter = PartitionLimiter::new( + NonZeroU32::new(1).unwrap(), + NonZeroU32::new(3).unwrap(), + None, + ); let token = String::from("test"); assert!(!limiter.is_limited(&token)); @@ -55,4 +72,26 @@ mod tests { assert!(!limiter.is_limited(&token)); assert!(limiter.is_limited(&token)); } + + #[tokio::test] + async fn forced_key() { + let key_one = String::from("one"); + let key_two = String::from("two"); + let key_three = String::from("three"); + let forced_keys = Some(String::from("one,three")); + + let limiter = PartitionLimiter::new( + NonZeroU32::new(1).unwrap(), + NonZeroU32::new(1).unwrap(), + forced_keys, + ); + + // One and three are limited from the start, two is not + assert!(limiter.is_limited(&key_one)); + assert!(!limiter.is_limited(&key_two)); + assert!(limiter.is_limited(&key_three)); + + // Two is limited on the second event + assert!(limiter.is_limited(&key_two)); + } } diff --git a/capture/src/server.rs b/capture/src/server.rs index ad9150907df98..32bafa83b252c 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -44,7 +44,11 @@ where .register("rdkafka".to_string(), Duration::seconds(30)) .await; - let partition = PartitionLimiter::new(config.per_second_limit, config.burst_limit); + let partition = PartitionLimiter::new( + config.per_second_limit, + config.burst_limit, + config.overflow_forced_keys, + ); let sink = sink::KafkaSink::new(config.kafka, sink_liveness, partition) .expect("failed to start Kafka sink"); diff --git a/capture/src/sink.rs b/capture/src/sink.rs index c1d291a275628..93ba6c57e0989 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -301,8 +301,11 @@ mod tests { let handle = registry .register("one".to_string(), Duration::seconds(30)) .await; - let limiter = - PartitionLimiter::new(NonZeroU32::new(10).unwrap(), NonZeroU32::new(10).unwrap()); + let limiter = PartitionLimiter::new( + NonZeroU32::new(10).unwrap(), + NonZeroU32::new(10).unwrap(), + None, + ); let cluster = MockCluster::new(1).expect("failed to create mock brokers"); let config = config::KafkaConfig { kafka_producer_linger_ms: 0, From 03e47f497063fddef50a48db5b7857ecd2695753 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 27 Nov 2023 13:10:15 +0000 Subject: [PATCH 058/249] Initial commit --- LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000..d1e439cba370e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 PostHog + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000..cf48fa8902586 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# rusty-hook +A reliable and performant webhook system for PostHog From 80f5270df14f1da01382ddf9bbeef5bb74f1e3a9 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 27 Nov 2023 13:20:47 +0000 Subject: [PATCH 059/249] Structure --- .gitignore | 1 + Cargo.lock | 15 +++++++++++++++ Cargo.toml | 11 +++++++++++ hook-common/Cargo.toml | 8 ++++++++ hook-common/src/lib.rs | 14 ++++++++++++++ hook-consumer/Cargo.toml | 8 ++++++++ hook-consumer/src/main.rs | 3 +++ hook-producer/Cargo.toml | 8 ++++++++ hook-producer/src/main.rs | 3 +++ 9 files changed, 71 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 hook-common/Cargo.toml create mode 100644 hook-common/src/lib.rs create mode 100644 hook-consumer/Cargo.toml create mode 100644 hook-consumer/src/main.rs create mode 100644 hook-producer/Cargo.toml create mode 100644 hook-producer/src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000..ea8c4bf7f35f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000000..456c3857724f0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,15 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "hook-common" +version = "0.1.0" + +[[package]] +name = "hook-consumer" +version = "0.1.0" + +[[package]] +name = "hook-producer" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000..d880e2cd85940 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +resolver = "2" + +members = [ + "hook-common", + "hook-producer", + "hook-consumer", +] + +[workspace.dependencies] +sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json" ] } diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml new file mode 100644 index 0000000000000..1d1418563ca5e --- /dev/null +++ b/hook-common/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hook-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs new file mode 100644 index 0000000000000..7d12d9af8195b --- /dev/null +++ b/hook-common/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/hook-consumer/Cargo.toml b/hook-consumer/Cargo.toml new file mode 100644 index 0000000000000..49c2d9f84b17d --- /dev/null +++ b/hook-consumer/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hook-consumer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs new file mode 100644 index 0000000000000..e7a11a969c037 --- /dev/null +++ b/hook-consumer/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/hook-producer/Cargo.toml b/hook-producer/Cargo.toml new file mode 100644 index 0000000000000..96fbb4d7528fe --- /dev/null +++ b/hook-producer/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hook-producer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs new file mode 100644 index 0000000000000..e7a11a969c037 --- /dev/null +++ b/hook-producer/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 2ca64c086c96c997688dc9abca8f7e2fbb39cf31 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 27 Nov 2023 14:44:09 +0000 Subject: [PATCH 060/249] Add rust workflow --- .github/workflows/rust.yml | 93 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000000000..6fc44a98c29ea --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,93 @@ +name: Rust + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: buildjet-4vcpu-ubuntu-2204 + + steps: + - uses: actions/checkout@v3 + + - name: Install rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo build + run: cargo build --all --locked --release + + test: + runs-on: buildjet-4vcpu-ubuntu-2204 + + steps: + - uses: actions/checkout@v3 + + - name: Install rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${ runner.os }-cargo-debug-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo test + run: cargo test --all-features + + clippy: + runs-on: buildjet-4vcpu-ubuntu-2204 + + steps: + - uses: actions/checkout@v3 + + - name: Install latest rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy + + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} + + - name: Run clippy + run: cargo clippy -- -D warnings + + format: + runs-on: buildjet-4vcpu-ubuntu-2204 + + steps: + - uses: actions/checkout@v3 + + - name: Install latest rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: rustfmt + + - name: Format + run: cargo fmt -- --check From cd4d639faddf3b05b82fc79c2fbd765dc4ee2ecc Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 28 Nov 2023 15:24:07 +0100 Subject: [PATCH 061/249] tracing: add ack_wait spans (#60) --- capture/src/sink.rs | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/capture/src/sink.rs b/capture/src/sink.rs index 93ba6c57e0989..13397dcf83465 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -8,8 +8,8 @@ use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; use rdkafka::producer::{DeliveryFuture, Producer}; use rdkafka::util::Timeout; use tokio::task::JoinSet; -use tracing::instrument; use tracing::log::{debug, error, info}; +use tracing::{info_span, instrument, Instrument}; use crate::api::CaptureError; use crate::config::KafkaConfig; @@ -241,7 +241,9 @@ impl EventSink for KafkaSink { let ack = Self::kafka_send(self.producer.clone(), self.topic.clone(), event, limited).await?; histogram!("capture_event_batch_size", 1.0); - Self::process_ack(ack).await + Self::process_ack(ack) + .instrument(info_span!("ack_wait_one")) + .await } #[instrument(skip_all)] @@ -261,20 +263,25 @@ impl EventSink for KafkaSink { } // Await on all the produce promises, fail batch on first failure - while let Some(res) = set.join_next().await { - match res { - Ok(Ok(_)) => {} - Ok(Err(err)) => { - set.abort_all(); - return Err(err); - } - Err(err) => { - set.abort_all(); - error!("join error while waiting on Kafka ACK: {:?}", err); - return Err(CaptureError::RetryableSinkError); + async move { + while let Some(res) = set.join_next().await { + match res { + Ok(Ok(_)) => {} + Ok(Err(err)) => { + set.abort_all(); + return Err(err); + } + Err(err) => { + set.abort_all(); + error!("join error while waiting on Kafka ACK: {:?}", err); + return Err(CaptureError::RetryableSinkError); + } } } + Ok(()) } + .instrument(info_span!("ack_wait_many")) + .await?; histogram!("capture_event_batch_size", batch_size as f64); Ok(()) From cc43e214729cefe5d4b58482d506bbef1ccf8691 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 28 Nov 2023 17:44:08 +0100 Subject: [PATCH 062/249] feat: trim distinct_id to 200 chars (#61) --- capture-server/tests/events.rs | 41 ++++++++++++++++++++++++++++++++++ capture/src/capture.rs | 8 ++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/capture-server/tests/events.rs b/capture-server/tests/events.rs index 27db9a7c897de..b38ac5a1a63fb 100644 --- a/capture-server/tests/events.rs +++ b/capture-server/tests/events.rs @@ -166,3 +166,44 @@ async fn it_does_not_partition_limit_different_ids() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn it_trims_distinct_id() -> Result<()> { + setup_tracing(); + let token = random_string("token", 16); + let distinct_id1 = random_string("id", 200 - 3); + let distinct_id2 = random_string("id", 222); + let (trimmed_distinct_id2, _) = distinct_id2.split_at(200); // works because ascii chars + + let topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topic(&topic); + + let event = json!([{ + "token": token, + "event": "event1", + "distinct_id": distinct_id1 + },{ + "token": token, + "event": "event2", + "distinct_id": distinct_id2 + }]); + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + assert_json_include!( + actual: topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": distinct_id1 + }) + ); + assert_json_include!( + actual: topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": trimmed_distinct_id2 + }) + ); + + Ok(()) +} diff --git a/capture/src/capture.rs b/capture/src/capture.rs index da4a72693f51f..8cc37e0737a00 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -172,6 +172,12 @@ pub fn process_single_event( _ => return Err(CaptureError::MissingDistinctId), }, }; + // Limit the size of distinct_id to 200 chars + let distinct_id: String = match distinct_id.len() { + 0..=200 => distinct_id.to_owned(), + _ => distinct_id.chars().take(200).collect(), + }; + if event.event.is_empty() { return Err(CaptureError::MissingEventName); } @@ -183,7 +189,7 @@ pub fn process_single_event( Ok(ProcessedEvent { uuid: event.uuid.unwrap_or_else(uuid_v7), - distinct_id: distinct_id.to_string(), + distinct_id, ip: context.client_ip.clone(), data, now: context.now.clone(), From 681fcafe74d3ffebdd35b5b440f93cc5ca7b63b9 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 28 Nov 2023 17:06:36 +0000 Subject: [PATCH 063/249] chore: add dispatch trigger (#62) --- .github/workflows/docker.yml | 1 + .github/workflows/rust.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 98160d19edce4..1b5f29bd90da3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,6 +1,7 @@ name: Build docker image on: + workflow_dispatch: push: branches: - 'main' diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 10636f11554e9..cadd2fb6ee3a6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,6 +1,7 @@ name: Rust on: + workflow_dispatch: push: branches: [ main ] pull_request: From 132ceeec9d7cffcafaeed46a1f12ad09374542c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 29 Nov 2023 18:52:12 +0100 Subject: [PATCH 064/249] feat: Initial PgQueue implementation --- .github/workflows/rust.yml | 22 +- Cargo.lock | 1865 +++++++++++++++++ README.md | 19 + docker-compose.yml | 15 + hook-common/Cargo.toml | 8 + hook-common/README.md | 2 + hook-common/src/lib.rs | 15 +- hook-common/src/pgqueue.rs | 215 ++ migrations/20231129172339_job_queue_table.sql | 16 + 9 files changed, 2161 insertions(+), 16 deletions(-) create mode 100644 docker-compose.yml create mode 100644 hook-common/README.md create mode 100644 hook-common/src/pgqueue.rs create mode 100644 migrations/20231129172339_job_queue_table.sql diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6fc44a98c29ea..a06e9ee211676 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,7 +30,7 @@ jobs: key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} - name: Run cargo build - run: cargo build --all --locked --release + run: cargo build --all --locked --release test: runs-on: buildjet-4vcpu-ubuntu-2204 @@ -43,6 +43,24 @@ jobs: with: toolchain: stable + - name: Install rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - name: Stop/Start stack with Docker Compose + shell: bash + run: | + docker compose -f docker-compose.dev.yml down + docker compose -f docker-compose.dev.yml up -d + + - name: Run migrations + shell: bash + run: | + cargo install sqlx-cli --no-default-features --features native-tls,postgres + DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database sqlx database create + DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database sqlx migrate run + - uses: actions/cache@v3 with: path: | @@ -76,7 +94,7 @@ jobs: - name: Run clippy run: cargo clippy -- -D warnings - + format: runs-on: buildjet-4vcpu-ubuntu-2204 diff --git a/Cargo.lock b/Cargo.lock index 456c3857724f0..16de92647176f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,9 +2,542 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-write-file" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae364a6c1301604bbc6dfbf8c385c47ff82301dd01eef506195a029196d8d04" +dependencies = [ + "nix", + "rand", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "hook-common" version = "0.1.0" +dependencies = [ + "chrono", + "serde", + "serde_derive", + "sqlx", + "thiserror", + "tokio", +] [[package]] name = "hook-consumer" @@ -13,3 +546,1335 @@ version = "0.1.0" [[package]] name = "hook-producer" version = "0.1.0" + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "rsa" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af6c4b23d99685a1408194da11270ef8e9809aff951cc70ec9b17350b087e474" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +dependencies = [ + "atomic-write-file", + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", + "uuid", +] + +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "zerocopy" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/README.md b/README.md index cf48fa8902586..2ce36b3908cc8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,21 @@ # rusty-hook A reliable and performant webhook system for PostHog + +## Testing + +1. Start a PostgreSQL instance: +```bash +docker compose -f docker-compose.yml up -d +``` + +2. Prepare test database: +```bash +export DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database +sqlx database create +sqlx migrate run +``` + +3. Test: +```bash +cargo test +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000..35b7a498d44b4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + db: + image: docker.io/library/postgres:16-alpine + restart: on-failure + environment: + POSTGRES_USER: posthog + POSTGRES_DB: posthog + POSTGRES_PASSWORD: posthog + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U posthog'] + interval: 5s + timeout: 5s + ports: + - '15432:5432' + command: postgres -c max_connections=1000 -c idle_in_transaction_session_timeout=300000 diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 1d1418563ca5e..673d8877f726a 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -6,3 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = { version = "0.4" } +serde = { version = "1.0" } +serde_derive = { version = "1.0" } +sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json", "chrono" ] } +thiserror = { version = "1.0" } + +[dev-dependencies] +tokio = { version = "1.34", features = ["macros"] } # We need a runtime for async tests diff --git a/hook-common/README.md b/hook-common/README.md new file mode 100644 index 0000000000000..d277a6c8600c9 --- /dev/null +++ b/hook-common/README.md @@ -0,0 +1,2 @@ +# hook-common +Library of common utilities used by rusty-hook. diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs index 7d12d9af8195b..d1dadf32ba63e 100644 --- a/hook-common/src/lib.rs +++ b/hook-common/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod pgqueue; diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs new file mode 100644 index 0000000000000..f06f50d10335d --- /dev/null +++ b/hook-common/src/pgqueue.rs @@ -0,0 +1,215 @@ +use std::str::FromStr; + +use chrono::prelude::*; +use serde::{de::DeserializeOwned, Serialize}; +use sqlx::postgres::{PgPool, PgPoolOptions}; +use thiserror::Error; + +/// Enumeration of errors for operations with PgQueue. +/// Errors can originate from sqlx and are wrapped by us to provide additional context. +#[derive(Error, Debug)] +pub enum PgQueueError { + #[error("connection failed with: {error}")] + ConnectionError { + error: sqlx::Error + }, + #[error("{command} query failed with: {error}")] + QueryError { + command: String, + error: sqlx::Error + }, + #[error("{0} is not a valid JobStatus")] + ParseJobStatusError(String), +} + +/// Enumeration of possible statuses for a Job. +/// Available: A job that is waiting in the queue to be picked up by a worker. +/// Completed: A job that was successfully completed by a worker. +/// Failed: A job that was unsuccessfully completed by a worker. +/// Running: A job that was picked up by a worker and it's currentlly being run. +#[derive(Debug, PartialEq, sqlx::Type)] +#[sqlx(type_name = "job_status")] +#[sqlx(rename_all = "lowercase")] +pub enum JobStatus { + Available, + Completed, + Failed, + Running, +} + +/// Allow casting JobStatus from strings. +impl FromStr for JobStatus { + type Err = PgQueueError; + + fn from_str(s: &str) -> Result { + match s { + "available" => Ok(JobStatus::Available), + "completed" => Ok(JobStatus::Completed), + "failed" => Ok(JobStatus::Failed), + "running" => Ok(JobStatus::Running), + invalid => Err(PgQueueError::ParseJobStatusError(invalid.to_owned())), + } + } +} + +/// JobParameters are stored and read to and from a JSONB field, so we accept anything that fits `sqlx::types::Json`. +pub type JobParameters = sqlx::types::Json; + +/// A Job to be executed by a worker dequeueing a PgQueue. +#[derive(sqlx::FromRow)] +pub struct Job { + pub id: i64, + pub attempt: i32, + pub finished_at: Option>, + pub created_at: DateTime, + pub started_at: Option>, + pub status: JobStatus, + pub parameters: sqlx::types::Json, +} + +/// A NewJob to be enqueued into a PgQueue. +pub struct NewJob { + pub attempt: i32, + pub finished_at: Option>, + pub started_at: Option>, + pub status: JobStatus, + pub parameters: sqlx::types::Json, +} + +impl NewJob { + pub fn new(parameters: J) -> Self { + Self { + attempt: 0, + parameters: sqlx::types::Json(parameters), + finished_at: None, + started_at: None, + status: JobStatus::Available, + } + } +} + +/// A queue implemented on top of a PostgreSQL table. +pub struct PgQueue { + table: String, + pool: PgPool, +} + +pub type PgQueueResult = std::result::Result; + +impl PgQueue { + /// Initialize a new PgQueue backed by table in PostgreSQL. + pub async fn new(table: &str, url: &str) -> PgQueueResult { + let table = table.to_owned(); + let pool = PgPoolOptions::new() + .connect(url) + .await + .map_err(|error| PgQueueError::ConnectionError {error})?; + + Ok(Self { + table, + pool, + }) + } + + /// Dequeue a Job from this PgQueue. + pub async fn dequeue(&self) -> PgQueueResult> { + let base_query = format!( + r#" +WITH available_in_queue AS ( + SELECT + id + FROM + "{0}" + WHERE + status = 'available' + ORDER BY + id + LIMIT 1 + FOR UPDATE SKIP LOCKED +) +UPDATE + "{0}" +SET + started_at = NOW(), + status = 'running'::job_status, + attempt = "{0}".attempt + 1 +FROM + available_in_queue +WHERE + "{0}".id = available_in_queue.id +RETURNING + "{0}".* + "#, &self.table); + + let item: Job = sqlx::query_as(&base_query) + .bind(&self.table) + .bind(&self.table) + .bind(&self.table) + .fetch_one(&self.pool) + .await + .map_err(|error| PgQueueError::QueryError { command: "UPDATE".to_owned(), error})?; + + Ok(item) + } + + /// Enqueue a Job into this PgQueue. + /// We take ownership of NewJob to enforce a specific NewJob is only enqueued once. + pub async fn enqueue(&self, job: NewJob) -> PgQueueResult<()> { + // TODO: Escaping. I think sqlx doesn't support identifiers. + let base_query = format!( + r#" +INSERT INTO {} + (attempt, created_at, finished_at, started_at, status, parameters) +VALUES + ($1, NOW(), $2, $3, $4::job_status, $5) + "#, &self.table); + + sqlx::query(&base_query) + .bind(job.attempt) + .bind(job.finished_at) + .bind(job.started_at) + .bind(job.status) + .bind(&job.parameters) + .execute(&self.pool) + .await + .map_err(|error| PgQueueError::QueryError { command: "INSERT".to_owned(), error})?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Serialize, Deserialize)] + struct JobParameters { + method: String, + body: String, + url: String, + } + + #[tokio::test] + async fn test_can_enqueue_and_dequeue_job() { + let job_parameters = JobParameters { + method: "POST".to_string(), + body: "{\"event\":\"event-name\"}".to_string(), + url: "https://localhost".to_string(), + }; + let new_job = NewJob::new(job_parameters); + + let queue = PgQueue::new("job_queue", "postgres://posthog:posthog@localhost:15432/test_database").await.unwrap(); + + queue.enqueue(new_job).await.unwrap(); + + let job: Job = queue.dequeue().await.unwrap(); + + assert_eq!(job.attempt, 1); + assert_eq!(job.parameters.method, "POST".to_string()); + assert_eq!(job.parameters.body, "{\"event\":\"event-name\"}".to_string()); + assert_eq!(job.parameters.url, "https://localhost".to_string()); + assert_eq!(job.finished_at, None); + assert_eq!(job.status, JobStatus::Running); + } +} diff --git a/migrations/20231129172339_job_queue_table.sql b/migrations/20231129172339_job_queue_table.sql new file mode 100644 index 0000000000000..078fdd9ef58b6 --- /dev/null +++ b/migrations/20231129172339_job_queue_table.sql @@ -0,0 +1,16 @@ +CREATE TYPE job_status AS ENUM( + 'available', + 'completed', + 'failed', + 'running' +); + +CREATE TABLE job_queue( + id BIGSERIAL PRIMARY KEY, + attempt INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + finished_at TIMESTAMPTZ DEFAULT NULL, + started_at TIMESTAMPTZ DEFAULT NULL, + status job_status NOT NULL DEFAULT 'available'::job_status, + parameters JSONB +); From 388b5444afd67d18b8cb66a84278f7725c072d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 30 Nov 2023 12:18:17 +0100 Subject: [PATCH 065/249] feat: Support for attempted_by --- hook-common/src/pgqueue.rs | 52 ++++++++++++++----- migrations/20231129172339_job_queue_table.sql | 2 + 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index f06f50d10335d..e2c206ed6457d 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -1,3 +1,7 @@ +//! # PgQueue +//! +//! A job queue implementation backed by a PostgreSQL table. + use std::str::FromStr; use chrono::prelude::*; @@ -6,7 +10,7 @@ use sqlx::postgres::{PgPool, PgPoolOptions}; use thiserror::Error; /// Enumeration of errors for operations with PgQueue. -/// Errors can originate from sqlx and are wrapped by us to provide additional context. +/// Errors that can originate from sqlx and are wrapped by us to provide additional context. #[derive(Error, Debug)] pub enum PgQueueError { #[error("connection failed with: {error}")] @@ -23,17 +27,17 @@ pub enum PgQueueError { } /// Enumeration of possible statuses for a Job. -/// Available: A job that is waiting in the queue to be picked up by a worker. -/// Completed: A job that was successfully completed by a worker. -/// Failed: A job that was unsuccessfully completed by a worker. -/// Running: A job that was picked up by a worker and it's currentlly being run. #[derive(Debug, PartialEq, sqlx::Type)] #[sqlx(type_name = "job_status")] #[sqlx(rename_all = "lowercase")] pub enum JobStatus { + /// A job that is waiting in the queue to be picked up by a worker. Available, + /// A job that was successfully completed by a worker. Completed, + /// A job that was unsuccessfully completed by a worker. Failed, + /// A job that was picked up by a worker and it's currentlly being run. Running, } @@ -58,12 +62,23 @@ pub type JobParameters = sqlx::types::Json; /// A Job to be executed by a worker dequeueing a PgQueue. #[derive(sqlx::FromRow)] pub struct Job { + /// A unique id identifying a job. pub id: i64, + /// A number corresponding to the current job attempt. pub attempt: i32, + /// A datetime corresponding to when the current job attempt started. + pub attempted_at: Option>, + /// A vector of identifiers that have attempted this job. E.g. thread ids, pod names, etc... + pub attempted_by: Vec, + /// A datetime corresponding to when the job was finished (either successfully or unsuccessfully). pub finished_at: Option>, + /// A datetime corresponding to when the job was created. pub created_at: DateTime, + /// A datetime corresponding to when the first job attempt was started. pub started_at: Option>, + /// The current status of the job. pub status: JobStatus, + /// Arbitrary job parameters stored as JSON. pub parameters: sqlx::types::Json, } @@ -90,16 +105,21 @@ impl NewJob { /// A queue implemented on top of a PostgreSQL table. pub struct PgQueue { + /// The identifier of the PostgreSQL table this queue runs on. table: String, + /// A connection pool used to connect to the PostgreSQL database. pool: PgPool, + /// The identifier of the worker listening on this queue. + worker: String, } pub type PgQueueResult = std::result::Result; impl PgQueue { /// Initialize a new PgQueue backed by table in PostgreSQL. - pub async fn new(table: &str, url: &str) -> PgQueueResult { + pub async fn new(table: &str, url: &str, worker: &str) -> PgQueueResult { let table = table.to_owned(); + let worker = worker.to_owned(); let pool = PgPoolOptions::new() .connect(url) .await @@ -108,6 +128,7 @@ impl PgQueue { Ok(Self { table, pool, + worker, }) } @@ -132,7 +153,8 @@ UPDATE SET started_at = NOW(), status = 'running'::job_status, - attempt = "{0}".attempt + 1 + attempt = "{0}".attempt + 1, + attempted_by = array_append("{0}".attempted_by, $1::text) FROM available_in_queue WHERE @@ -142,9 +164,7 @@ RETURNING "#, &self.table); let item: Job = sqlx::query_as(&base_query) - .bind(&self.table) - .bind(&self.table) - .bind(&self.table) + .bind(&self.worker) .fetch_one(&self.pool) .await .map_err(|error| PgQueueError::QueryError { command: "UPDATE".to_owned(), error})?; @@ -199,17 +219,21 @@ mod tests { }; let new_job = NewJob::new(job_parameters); - let queue = PgQueue::new("job_queue", "postgres://posthog:posthog@localhost:15432/test_database").await.unwrap(); + let worker_id = std::process::id().to_string(); + let queue = PgQueue::new("job_queue", "postgres://posthog:posthog@localhost:15432/test_database", &worker_id) + .await + .expect("failed to connect to local test postgresql database"); - queue.enqueue(new_job).await.unwrap(); + queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: Job = queue.dequeue().await.unwrap(); + let job: Job = queue.dequeue().await.expect("failed to dequeue job"); assert_eq!(job.attempt, 1); assert_eq!(job.parameters.method, "POST".to_string()); assert_eq!(job.parameters.body, "{\"event\":\"event-name\"}".to_string()); assert_eq!(job.parameters.url, "https://localhost".to_string()); - assert_eq!(job.finished_at, None); + assert!(job.finished_at.is_none()); assert_eq!(job.status, JobStatus::Running); + assert!(job.attempted_by.contains(&worker_id)); } } diff --git a/migrations/20231129172339_job_queue_table.sql b/migrations/20231129172339_job_queue_table.sql index 078fdd9ef58b6..f2682eea2fef4 100644 --- a/migrations/20231129172339_job_queue_table.sql +++ b/migrations/20231129172339_job_queue_table.sql @@ -8,6 +8,8 @@ CREATE TYPE job_status AS ENUM( CREATE TABLE job_queue( id BIGSERIAL PRIMARY KEY, attempt INT NOT NULL DEFAULT 0, + attempted_at TIMESTAMPTZ DEFAULT NULL, + attempted_by TEXT[] DEFAULT ARRAY[]::TEXT[], created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), finished_at TIMESTAMPTZ DEFAULT NULL, started_at TIMESTAMPTZ DEFAULT NULL, From bfeab3642dab184f4c6a717071c75d85d61ab790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 30 Nov 2023 12:20:38 +0100 Subject: [PATCH 066/249] fix: Wait for Postgres to be up in CI --- .github/workflows/rust.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a06e9ee211676..ee0e5b3ebc785 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -38,11 +38,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Install rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - - name: Install rust uses: dtolnay/rust-toolchain@master with: @@ -51,8 +46,8 @@ jobs: - name: Stop/Start stack with Docker Compose shell: bash run: | - docker compose -f docker-compose.dev.yml down - docker compose -f docker-compose.dev.yml up -d + docker compose -f docker-compose.yml down + docker compose -f docker-compose.yml up -d --wait - name: Run migrations shell: bash From 8b2325bddd2d907dc0e81bdf11c1c7dcf94a56c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 30 Nov 2023 12:23:05 +0100 Subject: [PATCH 067/249] fix: Formatting --- hook-common/src/pgqueue.rs | 55 +++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index e2c206ed6457d..18d324b733a57 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -14,14 +14,9 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum PgQueueError { #[error("connection failed with: {error}")] - ConnectionError { - error: sqlx::Error - }, + ConnectionError { error: sqlx::Error }, #[error("{command} query failed with: {error}")] - QueryError { - command: String, - error: sqlx::Error - }, + QueryError { command: String, error: sqlx::Error }, #[error("{0} is not a valid JobStatus")] ParseJobStatusError(String), } @@ -95,7 +90,7 @@ impl NewJob { pub fn new(parameters: J) -> Self { Self { attempt: 0, - parameters: sqlx::types::Json(parameters), + parameters: sqlx::types::Json(parameters), finished_at: None, started_at: None, status: JobStatus::Available, @@ -123,7 +118,7 @@ impl PgQueue { let pool = PgPoolOptions::new() .connect(url) .await - .map_err(|error| PgQueueError::ConnectionError {error})?; + .map_err(|error| PgQueueError::ConnectionError { error })?; Ok(Self { table, @@ -133,7 +128,9 @@ impl PgQueue { } /// Dequeue a Job from this PgQueue. - pub async fn dequeue(&self) -> PgQueueResult> { + pub async fn dequeue( + &self, + ) -> PgQueueResult> { let base_query = format!( r#" WITH available_in_queue AS ( @@ -161,20 +158,28 @@ WHERE "{0}".id = available_in_queue.id RETURNING "{0}".* - "#, &self.table); + "#, + &self.table + ); let item: Job = sqlx::query_as(&base_query) .bind(&self.worker) .fetch_one(&self.pool) .await - .map_err(|error| PgQueueError::QueryError { command: "UPDATE".to_owned(), error})?; + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; Ok(item) } /// Enqueue a Job into this PgQueue. /// We take ownership of NewJob to enforce a specific NewJob is only enqueued once. - pub async fn enqueue(&self, job: NewJob) -> PgQueueResult<()> { + pub async fn enqueue( + &self, + job: NewJob, + ) -> PgQueueResult<()> { // TODO: Escaping. I think sqlx doesn't support identifiers. let base_query = format!( r#" @@ -182,7 +187,9 @@ INSERT INTO {} (attempt, created_at, finished_at, started_at, status, parameters) VALUES ($1, NOW(), $2, $3, $4::job_status, $5) - "#, &self.table); + "#, + &self.table + ); sqlx::query(&base_query) .bind(job.attempt) @@ -192,7 +199,10 @@ VALUES .bind(&job.parameters) .execute(&self.pool) .await - .map_err(|error| PgQueueError::QueryError { command: "INSERT".to_owned(), error})?; + .map_err(|error| PgQueueError::QueryError { + command: "INSERT".to_owned(), + error, + })?; Ok(()) } @@ -220,9 +230,13 @@ mod tests { let new_job = NewJob::new(job_parameters); let worker_id = std::process::id().to_string(); - let queue = PgQueue::new("job_queue", "postgres://posthog:posthog@localhost:15432/test_database", &worker_id) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new( + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + &worker_id, + ) + .await + .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); @@ -230,7 +244,10 @@ mod tests { assert_eq!(job.attempt, 1); assert_eq!(job.parameters.method, "POST".to_string()); - assert_eq!(job.parameters.body, "{\"event\":\"event-name\"}".to_string()); + assert_eq!( + job.parameters.body, + "{\"event\":\"event-name\"}".to_string() + ); assert_eq!(job.parameters.url, "https://localhost".to_string()); assert!(job.finished_at.is_none()); assert_eq!(job.status, JobStatus::Running); From ec6ba09b820a9b6515acfd719694a5dad696ce3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 30 Nov 2023 12:27:47 +0100 Subject: [PATCH 068/249] fix: README recommends --waiting for docker compose to be up --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ce36b3908cc8..8eb2e2c347ca0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A reliable and performant webhook system for PostHog 1. Start a PostgreSQL instance: ```bash -docker compose -f docker-compose.yml up -d +docker compose -f docker-compose.yml up -d --wait ``` 2. Prepare test database: From c6ab642c284d26c504fc66dbd5ec58d30eae5de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 30 Nov 2023 12:31:02 +0100 Subject: [PATCH 069/249] chore: Add comment on SKIP LOCKED clause --- hook-common/src/pgqueue.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 18d324b733a57..8bb40e18af717 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -131,6 +131,8 @@ impl PgQueue { pub async fn dequeue( &self, ) -> PgQueueResult> { + // The query that follows uses a FOR UPDATE SKIP LOCKED clause. + // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. let base_query = format!( r#" WITH available_in_queue AS ( From da952a662956493b27380f01e2f8bde6ee487811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 1 Dec 2023 00:43:31 +0100 Subject: [PATCH 070/249] fix: Use the type alias I defined --- hook-common/src/pgqueue.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 8bb40e18af717..53398bfe27944 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -74,7 +74,7 @@ pub struct Job { /// The current status of the job. pub status: JobStatus, /// Arbitrary job parameters stored as JSON. - pub parameters: sqlx::types::Json, + pub parameters: JobParameters, } /// A NewJob to be enqueued into a PgQueue. @@ -83,7 +83,7 @@ pub struct NewJob { pub finished_at: Option>, pub started_at: Option>, pub status: JobStatus, - pub parameters: sqlx::types::Json, + pub parameters: JobParameters, } impl NewJob { From 5d0838f175bca4e4e6208dbce1d1150a220b3212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 1 Dec 2023 12:47:20 +0100 Subject: [PATCH 071/249] feat: Support for retrying --- hook-common/src/pgqueue.rs | 365 ++++++++++++++++-- migrations/20231129172339_job_queue_table.sql | 9 +- 2 files changed, 338 insertions(+), 36 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 53398bfe27944..a85b323521346 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -2,9 +2,10 @@ //! //! A job queue implementation backed by a PostgreSQL table. +use std::default::Default; use std::str::FromStr; -use chrono::prelude::*; +use chrono::{prelude::*, Duration}; use serde::{de::DeserializeOwned, Serialize}; use sqlx::postgres::{PgPool, PgPoolOptions}; use thiserror::Error; @@ -19,6 +20,8 @@ pub enum PgQueueError { QueryError { command: String, error: sqlx::Error }, #[error("{0} is not a valid JobStatus")] ParseJobStatusError(String), + #[error("{0} Job has reached max attempts and cannot be retried further")] + MaxAttemptsReachedError(String), } /// Enumeration of possible statuses for a Job. @@ -28,8 +31,12 @@ pub enum PgQueueError { pub enum JobStatus { /// A job that is waiting in the queue to be picked up by a worker. Available, + /// A job that was cancelled by a worker. + Cancelled, /// A job that was successfully completed by a worker. Completed, + /// A job that has + Discarded, /// A job that was unsuccessfully completed by a worker. Failed, /// A job that was picked up by a worker and it's currentlly being run. @@ -61,49 +68,123 @@ pub struct Job { pub id: i64, /// A number corresponding to the current job attempt. pub attempt: i32, - /// A datetime corresponding to when the current job attempt started. - pub attempted_at: Option>, + /// A datetime corresponding to when the job was attempted. + pub attempted_at: DateTime, /// A vector of identifiers that have attempted this job. E.g. thread ids, pod names, etc... pub attempted_by: Vec, - /// A datetime corresponding to when the job was finished (either successfully or unsuccessfully). - pub finished_at: Option>, /// A datetime corresponding to when the job was created. pub created_at: DateTime, - /// A datetime corresponding to when the first job attempt was started. - pub started_at: Option>, - /// The current status of the job. - pub status: JobStatus, + /// The current job's number of max attempts. + pub max_attempts: i32, /// Arbitrary job parameters stored as JSON. pub parameters: JobParameters, + /// The current status of the job. + pub status: JobStatus, + /// The target of the job. E.g. an endpoint or service we are trying to reach. + pub target: String, +} + +impl Job { + pub fn retry(self, error: E) -> Result, PgQueueError> { + if self.attempt == self.max_attempts { + Err(PgQueueError::MaxAttemptsReachedError(self.target)) + } else { + Ok(RetryableJob { + id: self.id, + attempt: self.attempt, + max_attempts: self.max_attempts, + error: sqlx::types::Json(error), + }) + } + } + + pub fn complete(self) -> CompletedJob { + CompletedJob { id: self.id } + } + + pub fn fail(self, error: E) -> FailedJob { + FailedJob { + id: self.id, + error: sqlx::types::Json(error), + } + } +} + +pub struct RetryPolicy { + backoff_coefficient: i32, + initial_interval: Duration, + maximum_interval: Option, +} + +impl RetryPolicy { + pub fn time_until_next_retry(&self, job: &RetryableJob) -> Duration { + let candidate_interval = + self.initial_interval * self.backoff_coefficient.pow(job.attempt as u32); + + if let Some(max_interval) = self.maximum_interval { + std::cmp::min(candidate_interval, max_interval) + } else { + candidate_interval + } + } +} + +impl Default for RetryPolicy { + fn default() -> Self { + Self { + backoff_coefficient: 2, + initial_interval: Duration::seconds(1), + maximum_interval: None, + } + } +} + +pub struct RetryableJob { + pub id: i64, + pub attempt: i32, + pub max_attempts: i32, + pub error: sqlx::types::Json, +} + +pub struct CompletedJob { + pub id: i64, +} + +pub struct FailedJob { + pub id: i64, + pub error: sqlx::types::Json, } /// A NewJob to be enqueued into a PgQueue. pub struct NewJob { - pub attempt: i32, - pub finished_at: Option>, - pub started_at: Option>, - pub status: JobStatus, + /// The maximum amount of attempts this NewJob has to complete. + pub max_attempts: i32, + /// The JSON-deserializable parameters for this NewJob. pub parameters: JobParameters, + /// The target of the NewJob. E.g. an endpoint or service we are trying to reach. + pub target: String, } impl NewJob { - pub fn new(parameters: J) -> Self { + pub fn new(max_attempts: i32, parameters: J, target: &str) -> Self { Self { - attempt: 0, + max_attempts, parameters: sqlx::types::Json(parameters), - finished_at: None, - started_at: None, - status: JobStatus::Available, + target: target.to_owned(), } } } /// A queue implemented on top of a PostgreSQL table. pub struct PgQueue { - /// The identifier of the PostgreSQL table this queue runs on. - table: String, + /// A name to identify this PgQueue as multiple may share a table. + name: String, /// A connection pool used to connect to the PostgreSQL database. pool: PgPool, + /// The retry policy to use to enqueue any retryable jobs. + retry_policy: RetryPolicy, + /// The identifier of the PostgreSQL table this queue runs on. + table: String, /// The identifier of the worker listening on this queue. worker: String, } @@ -112,7 +193,14 @@ pub type PgQueueResult = std::result::Result; impl PgQueue { /// Initialize a new PgQueue backed by table in PostgreSQL. - pub async fn new(table: &str, url: &str, worker: &str) -> PgQueueResult { + pub async fn new( + name: &str, + table: &str, + retry_policy: RetryPolicy, + url: &str, + worker: &str, + ) -> PgQueueResult { + let name = name.to_owned(); let table = table.to_owned(); let worker = worker.to_owned(); let pool = PgPoolOptions::new() @@ -121,13 +209,15 @@ impl PgQueue { .map_err(|error| PgQueueError::ConnectionError { error })?; Ok(Self { + name, table, pool, worker, + retry_policy, }) } - /// Dequeue a Job from this PgQueue. + /// Dequeue a Job from this PgQueue to work on it. pub async fn dequeue( &self, ) -> PgQueueResult> { @@ -142,6 +232,8 @@ WITH available_in_queue AS ( "{0}" WHERE status = 'available' + AND scheduled_at <= NOW() + AND queue = $1 ORDER BY id LIMIT 1 @@ -150,10 +242,10 @@ WITH available_in_queue AS ( UPDATE "{0}" SET - started_at = NOW(), + attempted_at = NOW(), status = 'running'::job_status, attempt = "{0}".attempt + 1, - attempted_by = array_append("{0}".attempted_by, $1::text) + attempted_by = array_append("{0}".attempted_by, $2::text) FROM available_in_queue WHERE @@ -165,6 +257,7 @@ RETURNING ); let item: Job = sqlx::query_as(&base_query) + .bind(&self.name) .bind(&self.worker) .fetch_one(&self.pool) .await @@ -186,19 +279,18 @@ RETURNING let base_query = format!( r#" INSERT INTO {} - (attempt, created_at, finished_at, started_at, status, parameters) + (attempt, created_at, scheduled_at, max_attempts, parameters, queue, status, target) VALUES - ($1, NOW(), $2, $3, $4::job_status, $5) + (0, NOW(), NOW(), $1, $2, $3, 'available'::job_status, $4) "#, &self.table ); sqlx::query(&base_query) - .bind(job.attempt) - .bind(job.finished_at) - .bind(job.started_at) - .bind(job.status) + .bind(job.max_attempts) .bind(&job.parameters) + .bind(&self.name) + .bind(&job.target) .execute(&self.pool) .await .map_err(|error| PgQueueError::QueryError { @@ -208,6 +300,120 @@ VALUES Ok(()) } + + /// Enqueue a Job back into this PgQueue marked as completed. + /// We take ownership of Job to enforce a specific Job is only enqueued once. + pub async fn enqueue_completed(&self, job: CompletedJob) -> PgQueueResult<()> { + // TODO: Escaping. I think sqlx doesn't support identifiers. + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + completed_at = NOW(), + status = 'completed'::job_status +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&self.name) + .bind(job.id) + .execute(&self.pool) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + Ok(()) + } + + /// Enqueue a Job back into this PgQueue to be retried at a later time. + /// We take ownership of Job to enforce a specific Job is only enqueued once. + pub async fn enqueue_retryable( + &self, + job: RetryableJob, + ) -> PgQueueResult<()> { + // TODO: Escaping. I think sqlx doesn't support identifiers. + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'available'::job_status, + scheduled_at = NOW() + $3, + errors = array_append("{0}".errors, $4) +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&self.name) + .bind(job.id) + .bind(self.retry_policy.time_until_next_retry(&job)) + .bind(&job.error) + .execute(&self.pool) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + Ok(()) + } + + /// Enqueue a Job back into this PgQueue marked as failed. + /// Jobs marked as failed will remain in the queue for tracking purposes but will not be dequeued. + /// We take ownership of FailedJob to enforce a specific FailedJob is only enqueued once. + pub async fn enqueue_failed( + &self, + job: FailedJob, + ) -> PgQueueResult<()> { + // TODO: Escaping. I think sqlx doesn't support identifiers. + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + completed_at = NOW(), + status = 'failed'::job_status + errors = array_append("{0}".errors, $3) +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&self.name) + .bind(job.id) + .bind(&job.error) + .execute(&self.pool) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + Ok(()) + } } #[cfg(test)] @@ -223,17 +429,21 @@ mod tests { } #[tokio::test] - async fn test_can_enqueue_and_dequeue_job() { + async fn test_can_dequeue_job() { let job_parameters = JobParameters { method: "POST".to_string(), body: "{\"event\":\"event-name\"}".to_string(), url: "https://localhost".to_string(), }; - let new_job = NewJob::new(job_parameters); + let job_target = "https://myhost/endpoint"; + let new_job = NewJob::new(1, job_parameters, job_target); let worker_id = std::process::id().to_string(); + let retry_policy = RetryPolicy::default(); let queue = PgQueue::new( + "test_queue_1", "job_queue", + retry_policy, "postgres://posthog:posthog@localhost:15432/test_database", &worker_id, ) @@ -245,14 +455,101 @@ mod tests { let job: Job = queue.dequeue().await.expect("failed to dequeue job"); assert_eq!(job.attempt, 1); + assert!(job.attempted_by.contains(&worker_id)); + assert_eq!(job.attempted_by.len(), 1); + assert_eq!(job.max_attempts, 1); assert_eq!(job.parameters.method, "POST".to_string()); assert_eq!( job.parameters.body, "{\"event\":\"event-name\"}".to_string() ); assert_eq!(job.parameters.url, "https://localhost".to_string()); - assert!(job.finished_at.is_none()); assert_eq!(job.status, JobStatus::Running); - assert!(job.attempted_by.contains(&worker_id)); + assert_eq!(job.target, job_target.to_string()); + } + + #[tokio::test] + async fn test_can_retry_job_with_remaining_attempts() { + let job_parameters = JobParameters { + method: "POST".to_string(), + body: "{\"event\":\"event-name\"}".to_string(), + url: "https://localhost".to_string(), + }; + let job_target = "https://myhost/endpoint"; + let new_job = NewJob::new(2, job_parameters, job_target); + + let worker_id = std::process::id().to_string(); + let retry_policy = RetryPolicy { + backoff_coefficient: 0, + initial_interval: Duration::seconds(0), + maximum_interval: None, + }; + let queue = PgQueue::new( + "test_queue_2", + "job_queue", + retry_policy, + "postgres://posthog:posthog@localhost:15432/test_database", + &worker_id, + ) + .await + .expect("failed to connect to local test postgresql database"); + + queue.enqueue(new_job).await.expect("failed to enqueue job"); + let job: Job = queue.dequeue().await.expect("failed to dequeue job"); + let retryable_job = job + .retry("a very reasonable failure reason") + .expect("failed to retry job"); + + queue + .enqueue_retryable(retryable_job) + .await + .expect("failed to enqueue retryable job"); + let retried_job: Job = queue.dequeue().await.expect("failed to dequeue job"); + + assert_eq!(retried_job.attempt, 2); + assert!(retried_job.attempted_by.contains(&worker_id)); + assert_eq!(retried_job.attempted_by.len(), 2); + assert_eq!(retried_job.max_attempts, 2); + assert_eq!(retried_job.parameters.method, "POST".to_string()); + assert_eq!( + retried_job.parameters.body, + "{\"event\":\"event-name\"}".to_string() + ); + assert_eq!(retried_job.parameters.url, "https://localhost".to_string()); + assert_eq!(retried_job.status, JobStatus::Running); + assert_eq!(retried_job.target, job_target.to_string()); + } + + #[tokio::test] + #[should_panic(expected = "failed to retry job")] + async fn test_cannot_retry_job_without_remaining_attempts() { + let job_parameters = JobParameters { + method: "POST".to_string(), + body: "{\"event\":\"event-name\"}".to_string(), + url: "https://localhost".to_string(), + }; + let job_target = "https://myhost/endpoint"; + let new_job = NewJob::new(1, job_parameters, job_target); + + let worker_id = std::process::id().to_string(); + let retry_policy = RetryPolicy { + backoff_coefficient: 0, + initial_interval: Duration::seconds(0), + maximum_interval: None, + }; + let queue = PgQueue::new( + "test_queue_3", + "job_queue", + retry_policy, + "postgres://posthog:posthog@localhost:15432/test_database", + &worker_id, + ) + .await + .expect("failed to connect to local test postgresql database"); + + queue.enqueue(new_job).await.expect("failed to enqueue job"); + let job: Job = queue.dequeue().await.expect("failed to dequeue job"); + job.retry("a very reasonable failure reason") + .expect("failed to retry job"); } } diff --git a/migrations/20231129172339_job_queue_table.sql b/migrations/20231129172339_job_queue_table.sql index f2682eea2fef4..4631f0b0a5d3f 100644 --- a/migrations/20231129172339_job_queue_table.sql +++ b/migrations/20231129172339_job_queue_table.sql @@ -10,9 +10,14 @@ CREATE TABLE job_queue( attempt INT NOT NULL DEFAULT 0, attempted_at TIMESTAMPTZ DEFAULT NULL, attempted_by TEXT[] DEFAULT ARRAY[]::TEXT[], + completed_at TIMESTAMPTZ DEFAULT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + errors jsonb[], + max_attempts INT NOT NULL DEFAULT 1, finished_at TIMESTAMPTZ DEFAULT NULL, - started_at TIMESTAMPTZ DEFAULT NULL, + parameters JSONB, + queue TEXT NOT NULL DEFAULT 'default'::text, + scheduled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), status job_status NOT NULL DEFAULT 'available'::job_status, - parameters JSONB + target TEXT NOT NULL ); From 67c28b0c6aa574198a3a7adddf02d4a373186cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 1 Dec 2023 14:49:58 +0100 Subject: [PATCH 072/249] chore: Add docstrings --- hook-common/src/pgqueue.rs | 71 ++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index a85b323521346..1aa2a8f080843 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -85,23 +85,36 @@ pub struct Job { } impl Job { + /// Consume Job to retry it. + /// This returns a RetryableJob that can be enqueued by PgQueue. + /// + /// # Arguments + /// + /// * `error`: Any JSON-serializable value to be stored as an error. pub fn retry(self, error: E) -> Result, PgQueueError> { - if self.attempt == self.max_attempts { + if self.attempt >= self.max_attempts { Err(PgQueueError::MaxAttemptsReachedError(self.target)) } else { Ok(RetryableJob { id: self.id, attempt: self.attempt, - max_attempts: self.max_attempts, error: sqlx::types::Json(error), }) } } + /// Consume Job to complete it. + /// This returns a CompletedJob that can be enqueued by PgQueue. pub fn complete(self) -> CompletedJob { CompletedJob { id: self.id } } + /// Consume Job to fail it. + /// This returns a FailedJob that can be enqueued by PgQueue. + /// + /// # Arguments + /// + /// * `error`: Any JSON-serializable value to be stored as an error. pub fn fail(self, error: E) -> FailedJob { FailedJob { id: self.id, @@ -110,25 +123,6 @@ impl Job { } } -pub struct RetryPolicy { - backoff_coefficient: i32, - initial_interval: Duration, - maximum_interval: Option, -} - -impl RetryPolicy { - pub fn time_until_next_retry(&self, job: &RetryableJob) -> Duration { - let candidate_interval = - self.initial_interval * self.backoff_coefficient.pow(job.attempt as u32); - - if let Some(max_interval) = self.maximum_interval { - std::cmp::min(candidate_interval, max_interval) - } else { - candidate_interval - } - } -} - impl Default for RetryPolicy { fn default() -> Self { Self { @@ -139,19 +133,28 @@ impl Default for RetryPolicy { } } +/// A Job that has failed but can still be enqueued into a PgQueue to be retried at a later point. +/// The time until retry will depend on the PgQueue's RetryPolicy. pub struct RetryableJob { + /// A unique id identifying a job. pub id: i64, + /// A number corresponding to the current job attempt. pub attempt: i32, - pub max_attempts: i32, + /// Any JSON-serializable value to be stored as an error. pub error: sqlx::types::Json, } +/// A Job that has completed to be enqueued into a PgQueue and marked as completed. pub struct CompletedJob { + /// A unique id identifying a job. pub id: i64, } +/// A Job that has failed to be enqueued into a PgQueue and marked as failed. pub struct FailedJob { + /// A unique id identifying a job. pub id: i64, + /// Any JSON-serializable value to be stored as an error. pub error: sqlx::types::Json, } @@ -175,6 +178,30 @@ impl NewJob { } } +/// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. +pub struct RetryPolicy { + /// Coeficient to multiply initial_interval with for every past attempt. + backoff_coefficient: i32, + /// The backoff interval for the first retry. + initial_interval: Duration, + /// The maximum possible backoff between retries. + maximum_interval: Option, +} + +impl RetryPolicy { + /// Calculate the time until the next retry for a given RetryableJob. + pub fn time_until_next_retry(&self, job: &RetryableJob) -> Duration { + let candidate_interval = + self.initial_interval * self.backoff_coefficient.pow(job.attempt as u32); + + if let Some(max_interval) = self.maximum_interval { + std::cmp::min(candidate_interval, max_interval) + } else { + candidate_interval + } + } +} + /// A queue implemented on top of a PostgreSQL table. pub struct PgQueue { /// A name to identify this PgQueue as multiple may share a table. From 8c5c7ec19d4f984a23fb539626761f483759717f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 1 Dec 2023 15:01:22 +0100 Subject: [PATCH 073/249] chore: Add basic requirements --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 8eb2e2c347ca0..b17e7ae6ffbe7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # rusty-hook A reliable and performant webhook system for PostHog +## Requirements + +1. [Rust](https://www.rust-lang.org/tools/install). +2. [sqlx-cli](https://crates.io/crates/sqlx-cli): To setup database and run migrations. +3. [Docker](https://docs.docker.com/engine/install/) or [podman](https://podman.io/docs/installation) (and [podman-compose](https://github.com/containers/podman-compose#installation)): To setup testing services. + ## Testing 1. Start a PostgreSQL instance: From d05f6ebccbfdf5e6c8837d0e468513c893664740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 1 Dec 2023 16:48:03 +0100 Subject: [PATCH 074/249] feat: Implement PgTransactionJob to hold a transaction open for the job --- hook-common/src/pgqueue.rs | 290 ++++++++++++++++++++++++++++++++++++- 1 file changed, 282 insertions(+), 8 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 1aa2a8f080843..3d1b233b6d804 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -18,6 +18,8 @@ pub enum PgQueueError { ConnectionError { error: sqlx::Error }, #[error("{command} query failed with: {error}")] QueryError { command: String, error: sqlx::Error }, + #[error("transaction {command} failed with: {error}")] + TransactionError { command: String, error: sqlx::Error }, #[error("{0} is not a valid JobStatus")] ParseJobStatusError(String), #[error("{0} Job has reached max attempts and cannot be retried further")] @@ -78,6 +80,8 @@ pub struct Job { pub max_attempts: i32, /// Arbitrary job parameters stored as JSON. pub parameters: JobParameters, + /// The queue this job belongs to. + pub queue: String, /// The current status of the job. pub status: JobStatus, /// The target of the job. E.g. an endpoint or service we are trying to reach. @@ -99,6 +103,7 @@ impl Job { id: self.id, attempt: self.attempt, error: sqlx::types::Json(error), + queue: self.queue, }) } } @@ -106,7 +111,10 @@ impl Job { /// Consume Job to complete it. /// This returns a CompletedJob that can be enqueued by PgQueue. pub fn complete(self) -> CompletedJob { - CompletedJob { id: self.id } + CompletedJob { + id: self.id, + queue: self.queue, + } } /// Consume Job to fail it. @@ -119,17 +127,152 @@ impl Job { FailedJob { id: self.id, error: sqlx::types::Json(error), + queue: self.queue, } } } -impl Default for RetryPolicy { - fn default() -> Self { - Self { - backoff_coefficient: 2, - initial_interval: Duration::seconds(1), - maximum_interval: None, - } +/// A Job within an open PostgreSQL transaction. +/// This implementation allows 'hiding' the job from any other workers running SKIP LOCKED queries. +pub struct PgTransactionJob<'c, J> { + pub job: Job, + pub table: String, + pub transaction: sqlx::Transaction<'c, sqlx::postgres::Postgres>, +} + +impl<'c, J> PgTransactionJob<'c, J> { + pub async fn retry( + mut self, + error: E, + retry_policy: &RetryPolicy, + ) -> Result, PgQueueError> { + let retryable_job = self.job.retry(error)?; + + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'available'::job_status, + scheduled_at = NOW() + $3, + errors = array_append("{0}".errors, $4) +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&retryable_job.queue) + .bind(retryable_job.id) + .bind(retry_policy.time_until_next_retry(&retryable_job)) + .bind(&retryable_job.error) + .execute(&mut *self.transaction) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + self.transaction + .commit() + .await + .map_err(|error| PgQueueError::TransactionError { + command: "COMMIT".to_owned(), + error, + })?; + + Ok(retryable_job) + } + + pub async fn complete(mut self) -> Result { + let completed_job = self.job.complete(); + + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'completed'::job_status, +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&completed_job.queue) + .bind(completed_job.id) + .execute(&mut *self.transaction) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + self.transaction + .commit() + .await + .map_err(|error| PgQueueError::TransactionError { + command: "COMMIT".to_owned(), + error, + })?; + + Ok(completed_job) + } + + pub async fn fail( + mut self, + error: E, + ) -> Result, PgQueueError> { + let failed_job = self.job.fail(error); + + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'failed'::job_status, +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&failed_job.queue) + .bind(failed_job.id) + .execute(&mut *self.transaction) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + self.transaction + .commit() + .await + .map_err(|error| PgQueueError::TransactionError { + command: "COMMIT".to_owned(), + error, + })?; + + Ok(failed_job) } } @@ -142,12 +285,16 @@ pub struct RetryableJob { pub attempt: i32, /// Any JSON-serializable value to be stored as an error. pub error: sqlx::types::Json, + /// A unique id identifying a job queue. + pub queue: String, } /// A Job that has completed to be enqueued into a PgQueue and marked as completed. pub struct CompletedJob { /// A unique id identifying a job. pub id: i64, + /// A unique id identifying a job queue. + pub queue: String, } /// A Job that has failed to be enqueued into a PgQueue and marked as failed. @@ -156,6 +303,8 @@ pub struct FailedJob { pub id: i64, /// Any JSON-serializable value to be stored as an error. pub error: sqlx::types::Json, + /// A unique id identifying a job queue. + pub queue: String, } /// A NewJob to be enqueued into a PgQueue. @@ -202,6 +351,16 @@ impl RetryPolicy { } } +impl Default for RetryPolicy { + fn default() -> Self { + Self { + backoff_coefficient: 2, + initial_interval: Duration::seconds(1), + maximum_interval: None, + } + } +} + /// A queue implemented on top of a PostgreSQL table. pub struct PgQueue { /// A name to identify this PgQueue as multiple may share a table. @@ -296,6 +455,73 @@ RETURNING Ok(item) } + /// Dequeue a Job from this PgQueue to work on it. + pub async fn dequeue_tx< + J: DeserializeOwned + std::marker::Send + std::marker::Unpin + 'static, + >( + &self, + ) -> PgQueueResult>> { + // The query that follows uses a FOR UPDATE SKIP LOCKED clause. + // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. + let mut tx = self.pool.begin().await.unwrap(); + + let base_query = format!( + r#" +WITH available_in_queue AS ( + SELECT + id + FROM + "{0}" + WHERE + status = 'available' + AND scheduled_at <= NOW() + AND queue = $1 + ORDER BY + id + LIMIT 1 + FOR UPDATE SKIP LOCKED +) +UPDATE + "{0}" +SET + attempted_at = NOW(), + status = 'running'::job_status, + attempt = "{0}".attempt + 1, + attempted_by = array_append("{0}".attempted_by, $2::text) +FROM + available_in_queue +WHERE + "{0}".id = available_in_queue.id +RETURNING + "{0}".* + "#, + &self.table + ); + + let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) + .bind(&self.name) + .bind(&self.worker) + .fetch_one(&mut *tx) + .await; + + let job: Job = match query_result { + Ok(j) => j, + Err(sqlx::Error::RowNotFound) => return Ok(None), + Err(e) => { + return Err(PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error: e, + }) + } + }; + + Ok(Some(PgTransactionJob { + job, + table: self.table.to_owned(), + transaction: tx, + })) + } + /// Enqueue a Job into this PgQueue. /// We take ownership of NewJob to enforce a specific NewJob is only enqueued once. pub async fn enqueue( @@ -495,6 +721,54 @@ mod tests { assert_eq!(job.target, job_target.to_string()); } + #[tokio::test] + async fn test_can_dequeue_tx_job() { + let job_parameters = JobParameters { + method: "POST".to_string(), + body: "{\"event\":\"event-name\"}".to_string(), + url: "https://localhost".to_string(), + }; + let job_target = "https://myhost/endpoint"; + let new_job = NewJob::new(1, job_parameters, job_target); + + let worker_id = std::process::id().to_string(); + let retry_policy = RetryPolicy::default(); + let queue = PgQueue::new( + "test_queue_tx_1", + "job_queue", + retry_policy, + "postgres://posthog:posthog@localhost:15432/test_database", + &worker_id, + ) + .await + .expect("failed to connect to local test postgresql database"); + + queue.enqueue(new_job).await.expect("failed to enqueue job"); + + let tx_job: PgTransactionJob<'_, JobParameters> = queue + .dequeue_tx() + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + let another_job: Option> = + queue.dequeue_tx().await.expect("failed to dequeue job"); + + assert!(another_job.is_none()); + + assert_eq!(tx_job.job.attempt, 1); + assert!(tx_job.job.attempted_by.contains(&worker_id)); + assert_eq!(tx_job.job.attempted_by.len(), 1); + assert_eq!(tx_job.job.max_attempts, 1); + assert_eq!(tx_job.job.parameters.method, "POST".to_string()); + assert_eq!( + tx_job.job.parameters.body, + "{\"event\":\"event-name\"}".to_string() + ); + assert_eq!(tx_job.job.parameters.url, "https://localhost".to_string()); + assert_eq!(tx_job.job.status, JobStatus::Running); + assert_eq!(tx_job.job.target, job_target.to_string()); + } + #[tokio::test] async fn test_can_retry_job_with_remaining_attempts() { let job_parameters = JobParameters { From 4bf65168952211982ec543646d54006a215fc1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 1 Dec 2023 18:09:52 +0100 Subject: [PATCH 075/249] refactor: Move queries to state handling methods in Job --- hook-common/src/pgqueue.rs | 505 +++++++++++++++++++++---------------- 1 file changed, 286 insertions(+), 219 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 3d1b233b6d804..451724debd364 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -132,19 +132,138 @@ impl Job { } } +/// A Job that can be updated in PostgreSQL. +pub struct PgJob { + pub job: Job, + pub table: String, + pub connection: sqlx::pool::PoolConnection, + pub retry_policy: RetryPolicy, +} + +impl PgJob { + pub async fn retry( + mut self, + error: E, + ) -> Result, PgQueueError> { + let retryable_job = self.job.retry(error)?; + + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'available'::job_status, + scheduled_at = NOW() + $3, + errors = array_append("{0}".errors, $4) +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&retryable_job.queue) + .bind(retryable_job.id) + .bind(self.retry_policy.time_until_next_retry(&retryable_job)) + .bind(&retryable_job.error) + .execute(&mut *self.connection) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + Ok(retryable_job) + } + + pub async fn complete(mut self) -> Result { + let completed_job = self.job.complete(); + + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'completed'::job_status, +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&completed_job.queue) + .bind(completed_job.id) + .execute(&mut *self.connection) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + Ok(completed_job) + } + + pub async fn fail( + mut self, + error: E, + ) -> Result, PgQueueError> { + let failed_job = self.job.fail(error); + + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'failed'::job_status, +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + + "#, + &self.table + ); + + sqlx::query(&base_query) + .bind(&failed_job.queue) + .bind(failed_job.id) + .execute(&mut *self.connection) + .await + .map_err(|error| PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; + + Ok(failed_job) + } +} + /// A Job within an open PostgreSQL transaction. /// This implementation allows 'hiding' the job from any other workers running SKIP LOCKED queries. pub struct PgTransactionJob<'c, J> { pub job: Job, pub table: String, pub transaction: sqlx::Transaction<'c, sqlx::postgres::Postgres>, + pub retry_policy: RetryPolicy, } impl<'c, J> PgTransactionJob<'c, J> { pub async fn retry( mut self, error: E, - retry_policy: &RetryPolicy, ) -> Result, PgQueueError> { let retryable_job = self.job.retry(error)?; @@ -170,7 +289,7 @@ RETURNING sqlx::query(&base_query) .bind(&retryable_job.queue) .bind(retryable_job.id) - .bind(retry_policy.time_until_next_retry(&retryable_job)) + .bind(self.retry_policy.time_until_next_retry(&retryable_job)) .bind(&retryable_job.error) .execute(&mut *self.transaction) .await @@ -327,6 +446,7 @@ impl NewJob { } } +#[derive(Copy, Clone)] /// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. pub struct RetryPolicy { /// Coeficient to multiply initial_interval with for every past attempt. @@ -367,7 +487,7 @@ pub struct PgQueue { name: String, /// A connection pool used to connect to the PostgreSQL database. pool: PgPool, - /// The retry policy to use to enqueue any retryable jobs. + /// The retry policy to be assigned to Jobs in this PgQueue. retry_policy: RetryPolicy, /// The identifier of the PostgreSQL table this queue runs on. table: String, @@ -382,9 +502,9 @@ impl PgQueue { pub async fn new( name: &str, table: &str, - retry_policy: RetryPolicy, url: &str, worker: &str, + retry_policy: RetryPolicy, ) -> PgQueueResult { let name = name.to_owned(); let table = table.to_owned(); @@ -396,17 +516,23 @@ impl PgQueue { Ok(Self { name, - table, pool, - worker, retry_policy, + table, + worker, }) } /// Dequeue a Job from this PgQueue to work on it. pub async fn dequeue( &self, - ) -> PgQueueResult> { + ) -> PgQueueResult>> { + let mut connection = self + .pool + .acquire() + .await + .map_err(|error| PgQueueError::ConnectionError { error })?; + // The query that follows uses a FOR UPDATE SKIP LOCKED clause. // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. let base_query = format!( @@ -442,17 +568,29 @@ RETURNING &self.table ); - let item: Job = sqlx::query_as(&base_query) + let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) .bind(&self.name) .bind(&self.worker) - .fetch_one(&self.pool) - .await - .map_err(|error| PgQueueError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; + .fetch_one(&mut *connection) + .await; + + let job: Job = match query_result { + Ok(j) => j, + Err(sqlx::Error::RowNotFound) => return Ok(None), + Err(e) => { + return Err(PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error: e, + }) + } + }; - Ok(item) + Ok(Some(PgJob { + job, + table: self.table.to_owned(), + connection, + retry_policy: self.retry_policy, + })) } /// Dequeue a Job from this PgQueue to work on it. @@ -461,10 +599,14 @@ RETURNING >( &self, ) -> PgQueueResult>> { + let mut tx = self + .pool + .begin() + .await + .map_err(|error| PgQueueError::ConnectionError { error })?; + // The query that follows uses a FOR UPDATE SKIP LOCKED clause. // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. - let mut tx = self.pool.begin().await.unwrap(); - let base_query = format!( r#" WITH available_in_queue AS ( @@ -519,6 +661,7 @@ RETURNING job, table: self.table.to_owned(), transaction: tx, + retry_policy: self.retry_policy, })) } @@ -553,120 +696,6 @@ VALUES Ok(()) } - - /// Enqueue a Job back into this PgQueue marked as completed. - /// We take ownership of Job to enforce a specific Job is only enqueued once. - pub async fn enqueue_completed(&self, job: CompletedJob) -> PgQueueResult<()> { - // TODO: Escaping. I think sqlx doesn't support identifiers. - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - completed_at = NOW(), - status = 'completed'::job_status -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&self.name) - .bind(job.id) - .execute(&self.pool) - .await - .map_err(|error| PgQueueError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; - - Ok(()) - } - - /// Enqueue a Job back into this PgQueue to be retried at a later time. - /// We take ownership of Job to enforce a specific Job is only enqueued once. - pub async fn enqueue_retryable( - &self, - job: RetryableJob, - ) -> PgQueueResult<()> { - // TODO: Escaping. I think sqlx doesn't support identifiers. - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - status = 'available'::job_status, - scheduled_at = NOW() + $3, - errors = array_append("{0}".errors, $4) -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&self.name) - .bind(job.id) - .bind(self.retry_policy.time_until_next_retry(&job)) - .bind(&job.error) - .execute(&self.pool) - .await - .map_err(|error| PgQueueError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; - - Ok(()) - } - - /// Enqueue a Job back into this PgQueue marked as failed. - /// Jobs marked as failed will remain in the queue for tracking purposes but will not be dequeued. - /// We take ownership of FailedJob to enforce a specific FailedJob is only enqueued once. - pub async fn enqueue_failed( - &self, - job: FailedJob, - ) -> PgQueueResult<()> { - // TODO: Escaping. I think sqlx doesn't support identifiers. - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - completed_at = NOW(), - status = 'failed'::job_status - errors = array_append("{0}".errors, $3) -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&self.name) - .bind(job.id) - .bind(&job.error) - .execute(&self.pool) - .await - .map_err(|error| PgQueueError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; - - Ok(()) - } } #[cfg(test)] @@ -674,71 +703,99 @@ mod tests { use super::*; use serde::Deserialize; - #[derive(Serialize, Deserialize)] + #[derive(Serialize, Deserialize, PartialEq, Debug)] struct JobParameters { method: String, body: String, url: String, } + impl Default for JobParameters { + fn default() -> Self { + Self { + method: "POST".to_string(), + body: "{\"event\":\"event-name\"}".to_string(), + url: "https://localhost".to_string(), + } + } + } + + /// Use process id as a worker id for tests. + fn worker_id() -> String { + std::process::id().to_string() + } + + /// Hardcoded test value for job target. + fn job_target() -> String { + "https://myhost/endpoint".to_owned() + } + #[tokio::test] async fn test_can_dequeue_job() { - let job_parameters = JobParameters { - method: "POST".to_string(), - body: "{\"event\":\"event-name\"}".to_string(), - url: "https://localhost".to_string(), - }; - let job_target = "https://myhost/endpoint"; - let new_job = NewJob::new(1, job_parameters, job_target); + let job_target = job_target(); + let job_parameters = JobParameters::default(); + let worker_id = worker_id(); + let new_job = NewJob::new(1, job_parameters, &job_target); - let worker_id = std::process::id().to_string(); - let retry_policy = RetryPolicy::default(); let queue = PgQueue::new( - "test_queue_1", + "test_can_dequeue_job", "job_queue", - retry_policy, "postgres://posthog:posthog@localhost:15432/test_database", &worker_id, + RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: Job = queue.dequeue().await.expect("failed to dequeue job"); + let pg_job: PgJob = queue + .dequeue() + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + + assert_eq!(pg_job.job.attempt, 1); + assert!(pg_job.job.attempted_by.contains(&worker_id)); + assert_eq!(pg_job.job.attempted_by.len(), 1); + assert_eq!(pg_job.job.max_attempts, 1); + assert_eq!(*pg_job.job.parameters.as_ref(), JobParameters::default()); + assert_eq!(pg_job.job.status, JobStatus::Running); + assert_eq!(pg_job.job.target, job_target); + } - assert_eq!(job.attempt, 1); - assert!(job.attempted_by.contains(&worker_id)); - assert_eq!(job.attempted_by.len(), 1); - assert_eq!(job.max_attempts, 1); - assert_eq!(job.parameters.method, "POST".to_string()); - assert_eq!( - job.parameters.body, - "{\"event\":\"event-name\"}".to_string() - ); - assert_eq!(job.parameters.url, "https://localhost".to_string()); - assert_eq!(job.status, JobStatus::Running); - assert_eq!(job.target, job_target.to_string()); + #[tokio::test] + async fn test_dequeue_returns_none_on_no_jobs() { + let worker_id = worker_id(); + let queue = PgQueue::new( + "test_dequeue_returns_none_on_no_jobs", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + &worker_id, + RetryPolicy::default(), + ) + .await + .expect("failed to connect to local test postgresql database"); + + let pg_job: Option> = + queue.dequeue().await.expect("failed to dequeue job"); + + assert!(pg_job.is_none()); } #[tokio::test] async fn test_can_dequeue_tx_job() { - let job_parameters = JobParameters { - method: "POST".to_string(), - body: "{\"event\":\"event-name\"}".to_string(), - url: "https://localhost".to_string(), - }; - let job_target = "https://myhost/endpoint"; - let new_job = NewJob::new(1, job_parameters, job_target); + let job_target = job_target(); + let job_parameters = JobParameters::default(); + let worker_id = worker_id(); + let new_job = NewJob::new(1, job_parameters, &job_target); - let worker_id = std::process::id().to_string(); - let retry_policy = RetryPolicy::default(); let queue = PgQueue::new( - "test_queue_tx_1", + "test_can_dequeue_tx_job", "job_queue", - retry_policy, "postgres://posthog:posthog@localhost:15432/test_database", &worker_id, + RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); @@ -750,107 +807,117 @@ mod tests { .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); - let another_job: Option> = - queue.dequeue_tx().await.expect("failed to dequeue job"); - - assert!(another_job.is_none()); assert_eq!(tx_job.job.attempt, 1); assert!(tx_job.job.attempted_by.contains(&worker_id)); assert_eq!(tx_job.job.attempted_by.len(), 1); assert_eq!(tx_job.job.max_attempts, 1); - assert_eq!(tx_job.job.parameters.method, "POST".to_string()); - assert_eq!( - tx_job.job.parameters.body, - "{\"event\":\"event-name\"}".to_string() - ); - assert_eq!(tx_job.job.parameters.url, "https://localhost".to_string()); + assert_eq!(*tx_job.job.parameters.as_ref(), JobParameters::default()); assert_eq!(tx_job.job.status, JobStatus::Running); - assert_eq!(tx_job.job.target, job_target.to_string()); + assert_eq!(tx_job.job.target, job_target); } #[tokio::test] - async fn test_can_retry_job_with_remaining_attempts() { - let job_parameters = JobParameters { - method: "POST".to_string(), - body: "{\"event\":\"event-name\"}".to_string(), - url: "https://localhost".to_string(), - }; - let job_target = "https://myhost/endpoint"; - let new_job = NewJob::new(2, job_parameters, job_target); + async fn test_dequeue_tx_returns_none_on_no_jobs() { + let worker_id = worker_id(); + let queue = PgQueue::new( + "test_dequeue_tx_returns_none_on_no_jobs", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + &worker_id, + RetryPolicy::default(), + ) + .await + .expect("failed to connect to local test postgresql database"); + + let tx_job: Option> = + queue.dequeue_tx().await.expect("failed to dequeue job"); + + assert!(tx_job.is_none()); + } - let worker_id = std::process::id().to_string(); + #[tokio::test] + async fn test_can_retry_job_with_remaining_attempts() { + let job_target = job_target(); + let job_parameters = JobParameters::default(); + let worker_id = worker_id(); + let new_job = NewJob::new(2, job_parameters, &job_target); let retry_policy = RetryPolicy { backoff_coefficient: 0, initial_interval: Duration::seconds(0), maximum_interval: None, }; + let queue = PgQueue::new( - "test_queue_2", + "test_can_retry_job_with_remaining_attempts", "job_queue", - retry_policy, "postgres://posthog:posthog@localhost:15432/test_database", &worker_id, + retry_policy, ) .await .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: Job = queue.dequeue().await.expect("failed to dequeue job"); - let retryable_job = job + let job: PgJob = queue + .dequeue() + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + let _ = job .retry("a very reasonable failure reason") + .await .expect("failed to retry job"); - - queue - .enqueue_retryable(retryable_job) + let retried_job: PgJob = queue + .dequeue() .await - .expect("failed to enqueue retryable job"); - let retried_job: Job = queue.dequeue().await.expect("failed to dequeue job"); - - assert_eq!(retried_job.attempt, 2); - assert!(retried_job.attempted_by.contains(&worker_id)); - assert_eq!(retried_job.attempted_by.len(), 2); - assert_eq!(retried_job.max_attempts, 2); - assert_eq!(retried_job.parameters.method, "POST".to_string()); + .expect("failed to dequeue job") + .expect("didn't find retried job to dequeue"); + + assert_eq!(retried_job.job.attempt, 2); + assert!(retried_job.job.attempted_by.contains(&worker_id)); + assert_eq!(retried_job.job.attempted_by.len(), 2); + assert_eq!(retried_job.job.max_attempts, 2); assert_eq!( - retried_job.parameters.body, - "{\"event\":\"event-name\"}".to_string() + *retried_job.job.parameters.as_ref(), + JobParameters::default() ); - assert_eq!(retried_job.parameters.url, "https://localhost".to_string()); - assert_eq!(retried_job.status, JobStatus::Running); - assert_eq!(retried_job.target, job_target.to_string()); + assert_eq!(retried_job.job.status, JobStatus::Running); + assert_eq!(retried_job.job.target, job_target); } #[tokio::test] #[should_panic(expected = "failed to retry job")] async fn test_cannot_retry_job_without_remaining_attempts() { - let job_parameters = JobParameters { - method: "POST".to_string(), - body: "{\"event\":\"event-name\"}".to_string(), - url: "https://localhost".to_string(), - }; - let job_target = "https://myhost/endpoint"; - let new_job = NewJob::new(1, job_parameters, job_target); - - let worker_id = std::process::id().to_string(); + let job_target = job_target(); + let job_parameters = JobParameters::default(); + let worker_id = worker_id(); + let new_job = NewJob::new(1, job_parameters, &job_target); let retry_policy = RetryPolicy { backoff_coefficient: 0, initial_interval: Duration::seconds(0), maximum_interval: None, }; + let queue = PgQueue::new( - "test_queue_3", + "test_cannot_retry_job_without_remaining_attempts", "job_queue", - retry_policy, "postgres://posthog:posthog@localhost:15432/test_database", &worker_id, + retry_policy, ) .await .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: Job = queue.dequeue().await.expect("failed to dequeue job"); + + let job: PgJob = queue + .dequeue() + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); job.retry("a very reasonable failure reason") + .await .expect("failed to retry job"); } } From 401336a853a1c5a8eec0505c00fc5ff811a7d994 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 1 Dec 2023 18:27:51 +0100 Subject: [PATCH 076/249] refactor: Return from match and close connection as recommended --- hook-common/src/pgqueue.rs | 61 ++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 451724debd364..599785cf3f914 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -574,23 +574,28 @@ RETURNING .fetch_one(&mut *connection) .await; - let job: Job = match query_result { - Ok(j) => j, - Err(sqlx::Error::RowNotFound) => return Ok(None), + match query_result { + Ok(job) => Ok(Some(PgJob { + job, + table: self.table.to_owned(), + connection, + retry_policy: self.retry_policy, + })), + + // Although connection would be closed once it goes out of scope, sqlx recommends explicitly calling close(). + // See: https://docs.rs/sqlx/latest/sqlx/postgres/any/trait.AnyConnectionBackend.html#tymethod.close. + Err(sqlx::Error::RowNotFound) => { + let _ = connection.close().await; + Ok(None) + } Err(e) => { - return Err(PgQueueError::QueryError { + let _ = connection.close().await; + Err(PgQueueError::QueryError { command: "UPDATE".to_owned(), error: e, }) } - }; - - Ok(Some(PgJob { - job, - table: self.table.to_owned(), - connection, - retry_policy: self.retry_policy, - })) + } } /// Dequeue a Job from this PgQueue to work on it. @@ -646,23 +651,21 @@ RETURNING .fetch_one(&mut *tx) .await; - let job: Job = match query_result { - Ok(j) => j, - Err(sqlx::Error::RowNotFound) => return Ok(None), - Err(e) => { - return Err(PgQueueError::QueryError { - command: "UPDATE".to_owned(), - error: e, - }) - } - }; - - Ok(Some(PgTransactionJob { - job, - table: self.table.to_owned(), - transaction: tx, - retry_policy: self.retry_policy, - })) + match query_result { + Ok(job) => Ok(Some(PgTransactionJob { + job, + table: self.table.to_owned(), + transaction: tx, + retry_policy: self.retry_policy, + })), + + // Transaction is rolledback on drop. + Err(sqlx::Error::RowNotFound) => Ok(None), + Err(e) => Err(PgQueueError::QueryError { + command: "UPDATE".to_owned(), + error: e, + }), + } } /// Enqueue a Job into this PgQueue. From 2ab6250078d782b7f90d08089ed7304fecf4b2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Mon, 4 Dec 2023 10:24:28 +0100 Subject: [PATCH 077/249] fix: Update docstring Co-authored-by: Brett Hoerner --- hook-common/src/pgqueue.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 599785cf3f914..dd38fa24525ee 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -109,7 +109,7 @@ impl Job { } /// Consume Job to complete it. - /// This returns a CompletedJob that can be enqueued by PgQueue. + /// This returns a CompletedJob that can be marked as completed by PgQueue. pub fn complete(self) -> CompletedJob { CompletedJob { id: self.id, From 521fb51264c1e0a7202f7d8aa74199bf4e27ee67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Mon, 4 Dec 2023 10:24:43 +0100 Subject: [PATCH 078/249] fix: More docstring updates. Co-authored-by: Brett Hoerner --- hook-common/src/pgqueue.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index dd38fa24525ee..6575be8a0b6c8 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -118,7 +118,7 @@ impl Job { } /// Consume Job to fail it. - /// This returns a FailedJob that can be enqueued by PgQueue. + /// This returns a FailedJob that can be marked as failed by PgQueue. /// /// # Arguments /// From 746120b4cd8e843c8ee53b500bc07322f79fabfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Mon, 4 Dec 2023 10:25:03 +0100 Subject: [PATCH 079/249] fix: Typo Co-authored-by: Brett Hoerner --- hook-common/src/pgqueue.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 6575be8a0b6c8..7204f976cd1eb 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -449,7 +449,7 @@ impl NewJob { #[derive(Copy, Clone)] /// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. pub struct RetryPolicy { - /// Coeficient to multiply initial_interval with for every past attempt. + /// Coefficient to multiply initial_interval with for every past attempt. backoff_coefficient: i32, /// The backoff interval for the first retry. initial_interval: Duration, From d5f345dadd6f19734b2d9c195550de1a46c24d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Mon, 4 Dec 2023 10:31:59 +0100 Subject: [PATCH 080/249] fix: Update documentation and names --- hook-common/src/pgqueue.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 7204f976cd1eb..89aab7ee45ab9 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -499,16 +499,24 @@ pub type PgQueueResult = std::result::Result; impl PgQueue { /// Initialize a new PgQueue backed by table in PostgreSQL. + /// + /// # Arguments + /// + /// * `queue_name`: A name for the queue we are going to initialize. + /// * `table_name`: The name for the table the queue will use in PostgreSQL. + /// * `url`: A URL pointing to where the PostgreSQL database is hosted. + /// * `worker_name`: The name of the worker that is operating with this queue. + /// * `retry_policy`: A retry policy to pass to jobs from this queue. pub async fn new( - name: &str, - table: &str, + queue_name: &str, + table_name: &str, url: &str, - worker: &str, + worker_name: &str, retry_policy: RetryPolicy, ) -> PgQueueResult { - let name = name.to_owned(); - let table = table.to_owned(); - let worker = worker.to_owned(); + let name = queue_name.to_owned(); + let table = table_name.to_owned(); + let worker = worker_name.to_owned(); let pool = PgPoolOptions::new() .connect(url) .await From e4554641d0afdf7c20cb195ffcc5aded4f17c03f Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 27 Nov 2023 15:13:04 +0000 Subject: [PATCH 081/249] add deps --- Cargo.lock | 348 +++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + hook-producer/Cargo.toml | 2 + 3 files changed, 334 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 16de92647176f..aa7f4880c709b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "atoi" version = "2.0.0" @@ -62,9 +73,9 @@ dependencies = [ [[package]] name = "atomic-write-file" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae364a6c1301604bbc6dfbf8c385c47ff82301dd01eef506195a029196d8d04" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" dependencies = [ "nix", "rand", @@ -76,6 +87,59 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -182,9 +246,9 @@ checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -192,9 +256,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -343,6 +407,12 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -466,6 +536,25 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "h2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -494,6 +583,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + [[package]] name = "hex" version = "0.4.3" @@ -546,6 +641,95 @@ version = "0.1.0" [[package]] name = "hook-producer" version = "0.1.0" +dependencies = [ + "axum", + "tokio", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] [[package]] name = "iana-time-zone" @@ -648,9 +832,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" @@ -668,6 +852,12 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -684,6 +874,12 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -797,6 +993,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.1" @@ -900,6 +1106,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1004,9 +1230,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af6c4b23d99685a1408194da11270ef8e9809aff951cc70ec9b17350b087e474" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ "const-oid", "digest", @@ -1030,17 +1256,23 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -1116,6 +1348,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1138,6 +1392,15 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -1454,6 +1717,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tempfile" version = "3.8.1" @@ -1512,7 +1781,10 @@ dependencies = [ "bytes", "libc", "mio", + "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -1540,6 +1812,48 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" @@ -1855,18 +2169,18 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "zerocopy" -version = "0.7.26" +version = "0.7.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index d880e2cd85940..ea188390ae387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ members = [ [workspace.dependencies] sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json" ] } +tokio = { version = "1.34.0", features = ["full"] } diff --git a/hook-producer/Cargo.toml b/hook-producer/Cargo.toml index 96fbb4d7528fe..7626cc58033d5 100644 --- a/hook-producer/Cargo.toml +++ b/hook-producer/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +tokio = { workspace = true } +axum = { version="0.7.1", features=["http2"] } From 749dd1798c7f44b4dbddd615a4c4f82ab1c0e36d Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 4 Dec 2023 15:32:38 +0100 Subject: [PATCH 082/249] feat: auto-detect gzip compression on input payloads (#63) --- capture/src/event.rs | 52 +++++++++++++++---------------- capture/tests/django_compat.rs | 7 ++++- capture/tests/requests_dump.jsonl | 3 ++ 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/capture/src/event.rs b/capture/src/event.rs index 81eb754fd5bd3..92b0f0bc9e444 100644 --- a/capture/src/event.rs +++ b/capture/src/event.rs @@ -1,15 +1,16 @@ use std::collections::HashMap; use std::io::prelude::*; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::api::CaptureError; use bytes::{Buf, Bytes}; use flate2::read::GzDecoder; +use serde::{Deserialize, Serialize}; +use serde_json::Value; use time::OffsetDateTime; +use tracing::instrument; use uuid::Uuid; +use crate::api::CaptureError; + #[derive(Deserialize, Default)] pub enum Compression { #[default] @@ -59,6 +60,8 @@ pub struct RawEvent { pub set_once: Option>, } +static GZIP_MAGIC_NUMBERS: [u8; 3] = [0x1f, 0x8b, 8]; + #[derive(Deserialize)] #[serde(untagged)] enum RawRequest { @@ -78,33 +81,30 @@ impl RawRequest { } impl RawEvent { - /// We post up _at least one_ event, so when decompressiong and deserializing there - /// could be more than one. Hence this function has to return a Vec. - /// TODO: Use an axum extractor for this - pub fn from_bytes(query: &EventQuery, bytes: Bytes) -> Result, CaptureError> { + /// Takes a request payload and tries to decompress and unmarshall it into events. + /// While posthog-js sends a compression query param, a sizable portion of requests + /// fail due to it being missing when the body is compressed. + /// Instead of trusting the parameter, we peek at the payload's first three bytes to + /// detect gzip, fallback to uncompressed utf8 otherwise. + #[instrument(skip_all)] + pub fn from_bytes(_query: &EventQuery, bytes: Bytes) -> Result, CaptureError> { tracing::debug!(len = bytes.len(), "decoding new event"); - let payload = match query.compression { - Some(Compression::Gzip) => { - let mut d = GzDecoder::new(bytes.reader()); - let mut s = String::new(); - d.read_to_string(&mut s).map_err(|e| { - tracing::error!("failed to decode gzip: {}", e); - CaptureError::RequestDecodingError(String::from("invalid gzip data")) - })?; - s - } - Some(_) => { - return Err(CaptureError::RequestDecodingError(String::from( - "unsupported compression format", - ))) - } - - None => String::from_utf8(bytes.into()).map_err(|e| { + let payload = if bytes.starts_with(&GZIP_MAGIC_NUMBERS) { + let mut d = GzDecoder::new(bytes.reader()); + let mut s = String::new(); + d.read_to_string(&mut s).map_err(|e| { + tracing::error!("failed to decode gzip: {}", e); + CaptureError::RequestDecodingError(String::from("invalid gzip data")) + })?; + s + } else { + String::from_utf8(bytes.into()).map_err(|e| { tracing::error!("failed to decode body: {}", e); CaptureError::RequestDecodingError(String::from("invalid body encoding")) - })?, + })? }; + tracing::debug!(json = payload, "decoded event data"); Ok(serde_json::from_str::(&payload)?.events()) } diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index b95c78e48501f..d1d075bdab97c 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -82,7 +82,12 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { let mut mismatches = 0; for (line_number, line_contents) in reader.lines().enumerate() { - let case: RequestDump = serde_json::from_str(&line_contents?)?; + let line_contents = line_contents?; + if line_contents.starts_with('#') { + // Skip comment lines + continue; + } + let case: RequestDump = serde_json::from_str(&line_contents)?; if !case.path.starts_with("/e/") { println!("Skipping {} test case", &case.path); continue; diff --git a/capture/tests/requests_dump.jsonl b/capture/tests/requests_dump.jsonl index 88d30102039ee..ec0f4df482afb 100644 --- a/capture/tests/requests_dump.jsonl +++ b/capture/tests/requests_dump.jsonl @@ -10,3 +10,6 @@ {"path":"/e/?compression=gzip-js&ip=1&_=1694769323385&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:23.391445+00:00","body":"H4sIAAAAAAAAA+0a7W7bNvBVDCPYrzLV90f+temSdli7ZF3bDUUhUCRlMZZFRaLiOEWAPcsebU+yOzm2ZdlOlq7YklV/bOnueHe8b5P++HlY15IPD4aGGdAwMBMSuokgvkfhKTE54dQVruXFjhGYwydDcSFyDeR7tNaK0ULXpQBwUapClFqKanjwebin4Gv4mrLBT28HvwIaANGFKCupckCYxr7p7hsIj0s1rUQJwCNZikRdIpCLC8lEpGeFAMQLUY21KhDB6rIE8VFdZoBItS4Onj7NQI0sVZU+CAzDeApqVCqvkByBQLdOgIiC6jSnE+Tepr9RZqWpafotcEbzUU1HuErk5N1bXFKxUog8SoUcpSDLNFxrBZ1KrlMAepYBwAsppoUq9ZI4NM02eEHtOgGAMxmDnKmIUQq8tM237wf7LsJlDnrpaO6/sjqvrMC6HBX2lDsK8VriHk0vdHwvtC1z34H9cFlpmbObZcWHZ+NXv8xO7cM3vm1b785PL/OXXmEfvQ/e/8D94jfP8b/nb8aXH1jLNesR48QWJ35ABQFBMYkFjyk1LGokDq6p0XhfIExWERcTFUFsnQkGBktoVglgOCpVXTSBtkQtlBEkCWKLgJ8Nwl3BSWgHhhAupdxBU6pyRHN5RfXclKtVrsvmq2jgcmK5zISwd13uctQkrzTNGXp+a9ANr0GrVkJEYGMaZ4JHsHVwXFRJDosX+lOm5YWIEkEb4iSjI9jNx0+AasOigs4yRTnuFASUgmYT1ACkwsZYJtk4VWBczMoJlVkjDL1DL+AN5S9FVhll41vwKa2iWGaZzEEqxPkSAdnBIOMg6qO6gs99sDivIXhoTrOZlgyUg+BeJuYtNBusKlFhREelAJGzLXy2EGww6Rhxg8cmfg/qjAASLDp7HKoOxM8SigbgCqyJ0dHCNmVvUZEa0zf1SLRqBVJlYgJ06MzPQ1AyuikyVQEmBc0yWsGeADvU4lITLhJaZ8gfIno6BP9TrcsoauhgWZto0JA8GeY6jVgqM0gnkIhvKrnRa65BhKtg8aFKoapgXZszTWGDWO/YDfz6yR0q/gi5lz+vtQYPREzlGgv/ho5bqdbUhIK4pqbVkUwb+27Ru62LzNHgLWHrb4Ro7D60nHXhkLi6ruALsgfKaweb1FlG5oW3g4GMIBL2s2XHoMugRdt+XumxDl3XYh230mEdvtRglwcXcE41JfgISAi/mkgtJmRFdVvIdHyRyQ693V1gdFbU2f0kcHmx7tq3UBqfU9hGlcET2nlXqO2k/KoK3Cn364jbKWadvdNhb9/FPiXz4QOgSSZwnMIviIZsU+KCdoAkzUdD9wX7m/NtenypVMcjGzHUYRErjmk756FTKKFNJOKM1NX49spiXsMWtRrD7mHWSFl0Pnp3VLjTK+v52aHzc3V09vq4OLVPjl9AzfZOjcNj4/DEefOMMuNVM8zd9JuN6Qbm4ZjbJI5tg3ie6ceJEwSBF+Kiqcy5mt65JjCNRHDPTXBWUElSCRwXQ7Nx6MYI7nkO8allkjBwLeJYTJghCwKDee0RfG1YgHkEBo4HNYqvStDfG8Vb9I96FJ9NEq8Yu06dz/K0DrC3bI7irg950c/i/Szez+L3nMXbbHByTdWU3GyTyFyXiojLJq9GC3BTUtbcWYqqgF/9K6s/qrYB0B1tIyY+Z4zQWEAeOWYchB71HYYpvGgbYEZVcrDOAOud4IOphNEzVwN0LJapQVVPJvNRuu8l/3Uvqa9c93xmqTgurSvbG23vJcC6byV9K+lbyf1aybwARphP0QTY2FYAmYRarWDAelkxI16XTYwtMSWkcjSlZY5YpmqssAAHkaqcv+MaRK6YIP8YHDenzEewHzw1iiogAN8B8HE1I2PXbxhYbvkWCeKQkzhJOHNZ4Bjcav+GKWD36Ia+2TyAZuNObR1YM31GZ0Z+LtEp25oNqNd3m77b9N3mft3mwVd1UFFqdOjiEH7w5+9/DE4gVF6qUafmN4d4GzWfWwnUfBGTIAxDyGEWJhYNE9dZq/kP9er4Wy37emzqfKbklTOK87HVRHKn7Nv7Norsy35f9vuy/6/eHXeuR0/mf1cZfDc4nkdyf0+665509c+e2+5JV1T/5J60exUV9tek/TVpf036dY4Y/B3Dpu0T3wQOsQEHDJ7J7CRgie1ZODk87AOGb/VPip4T5GcTd6Snl5ZIzs52TJphP2n2k2Y/af6PDxi6Y+yukwbPuf70FzjrKd2kLgAA","output":[{"uuid":"018a981f-95fe-76af-9f1d-da5e526b4081","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-95fe-76af-9f1d-da5e526b4081\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0rsqs282xgp3wd4o\", \"$time\": 1694769321.47, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"text-default\", \"grow\"], \"attr__class\": \"text-default grow\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Cohorts\", \"attr__href\": \"/cohorts\"}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"a\", \"$el_text\": \"Cohorts\", \"classes\": [\"Link\", \"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__class\": \"Link LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__href\": \"/cohorts\", \"attr__data-attr\": \"menu-item-cohorts\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 13, \"nth_of_type\": 10}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider__content\"], \"attr__class\": \"SideBar__slider__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1913}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9664-7a21-9852-42ce19c880c6","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9664-7a21-9852-42ce19c880c6\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ymf6pk54unynhu8h\", \"$time\": 1694769321.573, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"show-product-intro-existing-products\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1810}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-966b-7dcc-abee-f41b896a74cc","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-966b-7dcc-abee-f41b896a74cc\", \"event\": \"recording viewed with no playtime summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"uz55qy2obbr2z36g\", \"$time\": 1694769321.58, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 3288, \"play_time_ms\": 0, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"engagement_score\": 0, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1803}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-966e-7272-8b9d-bffdc5c840d2","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-966e-7272-8b9d-bffdc5c840d2\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"5w3t82ytjay0nqiw\", \"$time\": 1694769321.582, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Cohorts \\u2022 PostHog\"}, \"offset\": 1801}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9d2f-72eb-8999-bec9f2a9f542","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9d2f-72eb-8999-bec9f2a9f542\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"tk1tnyoiz4gbnk2t\", \"$time\": 1694769323.311, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"a\", \"$el_text\": \"Persons & Groups\", \"classes\": [\"Link\", \"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__class\": \"Link LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__href\": \"/persons\", \"attr__data-attr\": \"menu-item-persons\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 12, \"nth_of_type\": 9}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider__content\"], \"attr__class\": \"SideBar__slider__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 71}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9d37-712e-b09d-61c3f8cf3624","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9d37-712e-b09d-61c3f8cf3624\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"648njm5gtwx2efjj\", \"$time\": 1694769323.319, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Persons & Groups \\u2022 PostHog\"}, \"offset\": 64}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?compression=gzip-js&ip=1&_=1694769326396&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:26.539412+00:00","body":"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","output":[{"uuid":"018a981f-9db5-7188-8161-91e9fd602fd7","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9db5-7188-8161-91e9fd602fd7\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"c5yz9qfwa86vhxab\", \"$time\": 1694769323.445, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"PersonsNode\"}, \"duration\": 102, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2945}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a25d-743f-a813-6d909390f5c9","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a25d-743f-a813-6d909390f5c9\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"i100qaub5hceuld4\", \"$time\": 1694769324.637, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"cs-dashboards\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1753}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a264-7a2a-9439-198973cc7878","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a264-7a2a-9439-198973cc7878\", \"event\": \"recording viewed with no playtime summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"wzrv024h7b0m7a8c\", \"$time\": 1694769324.645, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 1295, \"play_time_ms\": 0, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"engagement_score\": 0, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1745}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a266-73d2-a66f-1fbcc32d9f02","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a266-73d2-a66f-1fbcc32d9f02\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ksstzx9julgopw7a\", \"$time\": 1694769324.647, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Persons \\u2022 PostHog\"}, \"offset\": 1743}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a4b3-7b40-b430-9495d1bbed93","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a4b3-7b40-b430-9495d1bbed93\", \"event\": \"person viewed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"s2fzjz6c7t0ekgtm\", \"$time\": 1694769325.236, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"properties_count\": 18, \"has_email\": true, \"has_name\": false, \"custom_properties_count\": 4, \"posthog_properties_count\": 14, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1154}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a676-7722-bece-2f9b3d6b84ce","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a676-7722-bece-2f9b3d6b84ce\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"npyk617r6ht5qzbh\", \"$time\": 1694769325.686, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"person-session-recordings-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Recordings\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 68.19999694824219px; --lemon-tabs-slider-offset: 0px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 704}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a67b-7a6f-9637-bcaacec2496c","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a67b-7a6f-9637-bcaacec2496c\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"fhsu6w5edy7tvvuy\", \"$time\": 1694769325.691, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 699}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a783-7924-b938-3a789f71e25a","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a783-7924-b938-3a789f71e25a\", \"event\": \"recording list fetched\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"fcebvj6tugbw47wk\", \"$time\": 1694769325.955, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"load_time\": 145, \"listing_version\": \"3\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 435}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?compression=gzip-js&ip=1&_=1694769329412&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:29.500667+00:00","body":"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","output":[{"uuid":"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0ovdk9xlnv9fhfak\", \"$time\": 1694769326.837, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"persons-related-flags-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Feature flags\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 74.26666259765625px; --lemon-tabs-slider-offset: 176.29998779296875px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2572}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-aafa-79e8-abf4-4e68eb64c30f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aafa-79e8-abf4-4e68eb64c30f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"y3x76ea6mqqi2q0a\", \"$time\": 1694769326.842, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 2567}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af3d-79d4-be4a-d729c776e0da","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af3d-79d4-be4a-d729c776e0da\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ltw7rl4fhi4bgq63\", \"$time\": 1694769327.934, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 86.23332214355469px; --lemon-tabs-slider-offset: 367.0999755859375px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1475}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af46-7832-9a69-c566751c6625","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af46-7832-9a69-c566751c6625\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"6yl35wdtv5v11o6c\", \"$time\": 1694769327.942, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 1467}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-b008-737a-bb40-6a6a81ba2244","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-b008-737a-bb40-6a6a81ba2244\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"8guwvm82yhwyhfo6\", \"$time\": 1694769328.136, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"EventsQuery\", \"select\": [\"*\", \"event\", \"person\", \"coalesce(properties.$current_url, properties.$screen_name) -- Url / Screen\", \"properties.$lib\", \"timestamp\"], \"personId\": \"018a981f-4c9a-0000-68ff-417481f7c5b5\", \"after\": \"-24h\"}, \"duration\": 171, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1273}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +### Compression query param mismatch, to confirm gzip autodetection +{"path":"/e/?compression=gzip-js&ip=1&_=1694769302319&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.321230+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l5WlMwM1ltUXpMV0ppTXpBdE5qWXhOalkxTjJNek9HWmhJaXdpWlhabGJuUWlPaUlrYjNCMFgybHVJaXdpY0hKdmNHVnlkR2xsY3lJNmV5SWtiM01pT2lKTllXTWdUMU1nV0NJc0lpUnZjMTkyWlhKemFXOXVJam9pTVRBdU1UVXVNQ0lzSWlSaWNtOTNjMlZ5SWpvaVJtbHlaV1p2ZUNJc0lpUmtaWFpwWTJWZmRIbHdaU0k2SWtSbGMydDBiM0FpTENJa1kzVnljbVZ1ZEY5MWNtd2lPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPamd3TURBdklpd2lKR2h2YzNRaU9pSnNiMk5oYkdodmMzUTZPREF3TUNJc0lpUndZWFJvYm1GdFpTSTZJaThpTENJa1luSnZkM05sY2w5MlpYSnphVzl1SWpveE1UY3NJaVJpY205M2MyVnlYMnhoYm1kMVlXZGxJam9pWlc0dFZWTWlMQ0lrYzJOeVpXVnVYMmhsYVdkb2RDSTZNVEExTWl3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRZeU1Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPVEV4TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFMU5EZ3NJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpjNExqVWlMQ0lrYVc1elpYSjBYMmxrSWpvaU1XVTRaV1p5WkdSbE1HSTBNV2RtYkNJc0lpUjBhVzFsSWpveE5qazBOelk1TXpBeUxqTXhPQ3dpWkdsemRHbHVZM1JmYVdRaU9pSXdNVGhoT1RneFppMDBZakprTFRjNFlXVXRPVFEzWWkxaVpXUmlZV0V3TW1Fd1pqUWlMQ0lrWkdWMmFXTmxYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaQzAzT0dGbExUazBOMkl0WW1Wa1ltRmhNREpoTUdZMElpd2lKSEpsWm1WeWNtVnlJam9pSkdScGNtVmpkQ0lzSWlSeVpXWmxjbkpwYm1kZlpHOXRZV2x1SWpvaUpHUnBjbVZqZENJc0luUnZhMlZ1SWpvaWNHaGpYM0ZuVlVad05YZDZNa0pxUXpSU2MwWnFUVWR3VVROUVIwUnljMmsyVVRCRFJ6QkRVRFJPUVdGak1Fa2lMQ0lrYzJWemMybHZibDlwWkNJNklqQXhPR0U1T0RGbUxUUmlNbVV0TjJKa015MWlZak13TFRZMk1UZGlaalE0T0RnMk9TSXNJaVIzYVc1a2IzZGZhV1FpT2lJd01UaGhPVGd4WmkwMFlqSmxMVGRpWkRNdFltSXpNQzAyTmpFNE1UQm1aV1EyTldZaWZTd2lkR2x0WlhOMFlXMXdJam9pTWpBeU15MHdPUzB4TlZRd09Ub3hOVG93TWk0ek1UaGFJbjAlM0Q=","output":[{"uuid":"018a981f-4b2e-7bd3-bb30-6616657c38fa","distinct_id":"018a981f-4b2d-78ae-947b-bedbaa02a0f4","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b2e-7bd3-bb30-6616657c38fa\", \"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"1e8efrdde0b41gfl\", \"$time\": 1694769302.318, \"distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"timestamp\": \"2023-09-15T09:15:02.318Z\"}","now":"2023-09-15T09:15:02.321230+00:00","sent_at":"2023-09-15T09:15:02.319000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +{"path":"/e/?ip=1&_=1694769329412&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:29.500667+00:00","body":"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","output":[{"uuid":"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0ovdk9xlnv9fhfak\", \"$time\": 1694769326.837, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"persons-related-flags-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Feature flags\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 74.26666259765625px; --lemon-tabs-slider-offset: 176.29998779296875px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2572}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-aafa-79e8-abf4-4e68eb64c30f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aafa-79e8-abf4-4e68eb64c30f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"y3x76ea6mqqi2q0a\", \"$time\": 1694769326.842, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 2567}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af3d-79d4-be4a-d729c776e0da","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af3d-79d4-be4a-d729c776e0da\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ltw7rl4fhi4bgq63\", \"$time\": 1694769327.934, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 86.23332214355469px; --lemon-tabs-slider-offset: 367.0999755859375px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1475}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af46-7832-9a69-c566751c6625","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af46-7832-9a69-c566751c6625\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"6yl35wdtv5v11o6c\", \"$time\": 1694769327.942, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 1467}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-b008-737a-bb40-6a6a81ba2244","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-b008-737a-bb40-6a6a81ba2244\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"8guwvm82yhwyhfo6\", \"$time\": 1694769328.136, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"EventsQuery\", \"select\": [\"*\", \"event\", \"person\", \"coalesce(properties.$current_url, properties.$screen_name) -- Url / Screen\", \"properties.$lib\", \"timestamp\"], \"personId\": \"018a981f-4c9a-0000-68ff-417481f7c5b5\", \"after\": \"-24h\"}, \"duration\": 171, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1273}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} From efe67f8fef07926cab24fe6799d43b1458f4e3d8 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 27 Nov 2023 15:29:45 +0000 Subject: [PATCH 083/249] boot http server --- Cargo.lock | 108 ++++++++++++++++++++++++++++ hook-producer/Cargo.toml | 3 + hook-producer/src/handlers/index.rs | 3 + hook-producer/src/handlers/mod.rs | 9 +++ hook-producer/src/main.rs | 23 +++++- 5 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 hook-producer/src/handlers/index.rs create mode 100644 hook-producer/src/handlers/mod.rs diff --git a/Cargo.lock b/Cargo.lock index aa7f4880c709b..be15d2dc20319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,6 +384,16 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "eyre" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -643,7 +653,10 @@ name = "hook-producer" version = "0.1.0" dependencies = [ "axum", + "eyre", "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -764,6 +777,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "2.1.0" @@ -945,6 +964,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1062,6 +1091,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1392,6 +1427,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1756,6 +1800,16 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1884,6 +1938,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -1948,6 +2028,12 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2026,6 +2112,28 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.51.1" diff --git a/hook-producer/Cargo.toml b/hook-producer/Cargo.toml index 7626cc58033d5..35af92d685ccb 100644 --- a/hook-producer/Cargo.toml +++ b/hook-producer/Cargo.toml @@ -8,3 +8,6 @@ edition = "2021" [dependencies] tokio = { workspace = true } axum = { version="0.7.1", features=["http2"] } +eyre = "0.6.9" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/hook-producer/src/handlers/index.rs b/hook-producer/src/handlers/index.rs new file mode 100644 index 0000000000000..56896fa63a483 --- /dev/null +++ b/hook-producer/src/handlers/index.rs @@ -0,0 +1,3 @@ +pub async fn get() -> &'static str { + "rusty hook" +} diff --git a/hook-producer/src/handlers/mod.rs b/hook-producer/src/handlers/mod.rs new file mode 100644 index 0000000000000..a83e46ec58f42 --- /dev/null +++ b/hook-producer/src/handlers/mod.rs @@ -0,0 +1,9 @@ +use axum::{Router, routing}; + +mod index; + +pub fn router() -> Router { + let app = Router::new().route("/", routing::get(index::get)); + + app +} diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index e7a11a969c037..a8a1ce528d704 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -1,3 +1,22 @@ -fn main() { - println!("Hello, world!"); +use axum::Router; +use eyre::Result; +mod handlers; + +async fn listen(app: Router) -> Result<()> { + let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let app = handlers::router(); + + match listen(app).await { + Ok(_) => {}, + Err(e) => tracing::error!("failed to start hook-producer http server, {}", e) + } } From 9fb81ef3ca045d18f9163c70eda57c74cb6a7389 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 4 Dec 2023 14:51:19 +0000 Subject: [PATCH 084/249] Formatting, move shared deps into workspace --- Cargo.toml | 9 ++++++++- hook-common/Cargo.toml | 12 ++++++------ hook-producer/Cargo.toml | 8 ++++---- hook-producer/src/handlers/mod.rs | 2 +- hook-producer/src/main.rs | 4 ++-- 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ea188390ae387..8f557673559dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,12 @@ members = [ ] [workspace.dependencies] -sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json" ] } +chrono = { version = "0.4" } +serde = { version = "1.0" } +serde_derive = { version = "1.0" } +thiserror = { version = "1.0" } +sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json", "chrono" ] } tokio = { version = "1.34.0", features = ["full"] } +eyre = "0.6.9" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 673d8877f726a..b55a9ecd84d4d 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -6,11 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { version = "0.4" } -serde = { version = "1.0" } -serde_derive = { version = "1.0" } -sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json", "chrono" ] } -thiserror = { version = "1.0" } +chrono = { workspace = true} +serde = { workspace = true } +serde_derive = { workspace = true} +thiserror = { workspace = true } +sqlx = { workspace = true } [dev-dependencies] -tokio = { version = "1.34", features = ["macros"] } # We need a runtime for async tests +tokio = { workspace = true } # We need a runtime for async tests diff --git a/hook-producer/Cargo.toml b/hook-producer/Cargo.toml index 35af92d685ccb..85100d099bf4e 100644 --- a/hook-producer/Cargo.toml +++ b/hook-producer/Cargo.toml @@ -6,8 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { workspace = true } axum = { version="0.7.1", features=["http2"] } -eyre = "0.6.9" -tracing = "0.1.40" -tracing-subscriber = "0.3.18" +tokio = { workspace = true } +eyre = {workspace = true } +tracing = {workspace = true} +tracing-subscriber = {workspace = true} diff --git a/hook-producer/src/handlers/mod.rs b/hook-producer/src/handlers/mod.rs index a83e46ec58f42..8b4f83c0f19fc 100644 --- a/hook-producer/src/handlers/mod.rs +++ b/hook-producer/src/handlers/mod.rs @@ -1,4 +1,4 @@ -use axum::{Router, routing}; +use axum::{routing, Router}; mod index; diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index a8a1ce528d704..f05edab10fee2 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -16,7 +16,7 @@ async fn main() { let app = handlers::router(); match listen(app).await { - Ok(_) => {}, - Err(e) => tracing::error!("failed to start hook-producer http server, {}", e) + Ok(_) => {} + Err(e) => tracing::error!("failed to start hook-producer http server, {}", e), } } From 731ae143e7a58586973b441b6ec863bfe85a46a2 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Mon, 4 Dec 2023 15:08:00 +0000 Subject: [PATCH 085/249] add metrics --- Cargo.lock | 297 ++++++++++++++++++++++++++++-- Cargo.toml | 3 + hook-producer/Cargo.toml | 3 + hook-producer/src/config.rs | 16 ++ hook-producer/src/handlers/mod.rs | 10 +- hook-producer/src/main.rs | 16 +- hook-producer/src/metrics.rs | 53 ++++++ 7 files changed, 373 insertions(+), 25 deletions(-) create mode 100644 hook-producer/src/config.rs create mode 100644 hook-producer/src/metrics.rs diff --git a/Cargo.lock b/Cargo.lock index be15d2dc20319..451d42483e301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,10 +97,10 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "http-body-util", - "hyper", + "hyper 1.0.1", "hyper-util", "itoa", "matchit", @@ -129,8 +129,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "http-body-util", "mime", "pin-project-lite", @@ -284,6 +284,19 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -351,6 +364,26 @@ dependencies = [ "serde", ] +[[package]] +name = "envconfig" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea81cc7e21f55a9d9b1efb6816904978d0bfbe31a50347cb24b2e75564bcac9b" +dependencies = [ + "envconfig_derive", +] + +[[package]] +name = "envconfig_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfca278e5f84b45519acaaff758ebfa01f18e96998bc24b8f1b722dd804b9bf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -557,14 +590,29 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", - "indexmap", + "http 1.0.0", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -581,7 +629,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -653,12 +701,26 @@ name = "hook-producer" version = "0.1.0" dependencies = [ "axum", + "envconfig", "eyre", + "metrics", + "metrics-exporter-prometheus", "tokio", "tracing", "tracing-subscriber", ] +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.0.0" @@ -670,6 +732,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.0" @@ -677,7 +750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http", + "http 1.0.0", ] [[package]] @@ -688,8 +761,8 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" dependencies = [ "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -705,6 +778,29 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 0.2.11", + "http-body 0.4.5", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.0.1" @@ -715,8 +811,8 @@ dependencies = [ "futures-channel", "futures-util", "h2", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", "httparse", "httpdate", "itoa", @@ -733,11 +829,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.0.1", "pin-project-lite", - "socket2", + "socket2 0.5.5", "tokio", "tower", "tower-service", @@ -783,6 +879,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -790,9 +896,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itertools" version = "0.11.0" @@ -871,6 +983,15 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + [[package]] name = "matchit" version = "0.7.3" @@ -893,6 +1014,70 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metrics" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5" +dependencies = [ + "ahash", + "metrics-macros", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" +dependencies = [ + "base64", + "hyper 0.14.27", + "indexmap 1.9.3", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "metrics-util" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de2ed6e491ed114b40b732e4d1659a9d53992ebd87490c44a6ffe23739d973e" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.13.1", + "metrics", + "num_cpus", + "quanta", + "sketches-ddsketch", +] + [[package]] name = "mime" version = "0.3.17" @@ -1200,6 +1385,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "portable-atomic" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1215,6 +1406,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.33" @@ -1254,6 +1461,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1455,6 +1671,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "sketches-ddsketch" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" + [[package]] name = "slab" version = "0.4.9" @@ -1470,6 +1692,16 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.5" @@ -1552,7 +1784,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 2.1.0", "log", "memchr", "native-tls", @@ -1839,7 +2071,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", ] @@ -1966,6 +2198,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "typenum" version = "1.17.0" @@ -2046,6 +2284,15 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2106,6 +2353,16 @@ version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 8f557673559dc..e92db695f324a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,6 @@ tokio = { version = "1.34.0", features = ["full"] } eyre = "0.6.9" tracing = "0.1.40" tracing-subscriber = "0.3.18" +envconfig = "0.10.0" +metrics = "0.21.1" +metrics-exporter-prometheus = "0.12.1" diff --git a/hook-producer/Cargo.toml b/hook-producer/Cargo.toml index 85100d099bf4e..47ef532891436 100644 --- a/hook-producer/Cargo.toml +++ b/hook-producer/Cargo.toml @@ -11,3 +11,6 @@ tokio = { workspace = true } eyre = {workspace = true } tracing = {workspace = true} tracing-subscriber = {workspace = true} +envconfig = { workspace = true } +metrics = { workspace = true } +metrics-exporter-prometheus = { workspace = true } diff --git a/hook-producer/src/config.rs b/hook-producer/src/config.rs new file mode 100644 index 0000000000000..9d093c652efbb --- /dev/null +++ b/hook-producer/src/config.rs @@ -0,0 +1,16 @@ +use envconfig::Envconfig; + +#[derive(Envconfig)] +pub struct Config { + #[envconfig(from = "BIND_HOST", default = "0.0.0.0")] + pub host: String, + + #[envconfig(from = "BIND_PORT", default = "8000")] + pub port: u16, +} + +impl Config { + pub fn bind(&self) -> String { + format!("{}:{}", self.host, self.port) + } +} diff --git a/hook-producer/src/handlers/mod.rs b/hook-producer/src/handlers/mod.rs index 8b4f83c0f19fc..25040731d5688 100644 --- a/hook-producer/src/handlers/mod.rs +++ b/hook-producer/src/handlers/mod.rs @@ -3,7 +3,13 @@ use axum::{routing, Router}; mod index; pub fn router() -> Router { - let app = Router::new().route("/", routing::get(index::get)); + let recorder_handle = crate::metrics::setup_metrics_recorder(); - app + Router::new() + .route("/", routing::get(index::get)) + .route( + "/metrics", + routing::get(move || std::future::ready(recorder_handle.render())), + ) + .layer(axum::middleware::from_fn(crate::metrics::track_metrics)) } diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index f05edab10fee2..118829b00d78b 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -1,9 +1,17 @@ use axum::Router; + +use config::Config; +use envconfig::Envconfig; + use eyre::Result; + +mod config; mod handlers; +mod metrics; + +async fn listen(app: Router, bind: String) -> Result<()> { + let listener = tokio::net::TcpListener::bind(bind).await?; -async fn listen(app: Router) -> Result<()> { - let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await?; axum::serve(listener, app).await?; Ok(()) @@ -15,7 +23,9 @@ async fn main() { let app = handlers::router(); - match listen(app).await { + let config = Config::init_from_env().expect("failed to load configuration from env"); + + match listen(app, config.bind()).await { Ok(_) => {} Err(e) => tracing::error!("failed to start hook-producer http server, {}", e), } diff --git a/hook-producer/src/metrics.rs b/hook-producer/src/metrics.rs new file mode 100644 index 0000000000000..dbdc7b1fa1107 --- /dev/null +++ b/hook-producer/src/metrics.rs @@ -0,0 +1,53 @@ +use std::time::Instant; + +use axum::{ + body::Body, extract::MatchedPath, http::Request, middleware::Next, response::IntoResponse, +}; +use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; + +pub fn setup_metrics_recorder() -> PrometheusHandle { + const EXPONENTIAL_SECONDS: &[f64] = &[ + 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, + ]; + + PrometheusBuilder::new() + .set_buckets_for_metric( + Matcher::Full("http_requests_duration_seconds".to_string()), + EXPONENTIAL_SECONDS, + ) + .unwrap() + .install_recorder() + .unwrap() +} + +/// Middleware to record some common HTTP metrics +/// Someday tower-http might provide a metrics middleware: https://github.com/tower-rs/tower-http/issues/57 +pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse { + let start = Instant::now(); + + let path = if let Some(matched_path) = req.extensions().get::() { + matched_path.as_str().to_owned() + } else { + req.uri().path().to_owned() + }; + + let method = req.method().clone(); + + // Run the rest of the request handling first, so we can measure it and get response + // codes. + let response = next.run(req).await; + + let latency = start.elapsed().as_secs_f64(); + let status = response.status().as_u16().to_string(); + + let labels = [ + ("method", method.to_string()), + ("path", path), + ("status", status), + ]; + + metrics::increment_counter!("http_requests_total", &labels); + metrics::histogram!("http_requests_duration_seconds", latency, &labels); + + response +} From 3524d26ecedbd4b9cd59016669b93dbb4be8a4e6 Mon Sep 17 00:00:00 2001 From: Pranav Rajveer Date: Tue, 5 Dec 2023 15:25:48 +0530 Subject: [PATCH 086/249] refactor:typo (#64) --- capture/src/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capture/src/api.rs b/capture/src/api.rs index ff245b5072ac1..b27b1a9630c45 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -29,7 +29,7 @@ pub struct CaptureResponse { pub enum CaptureError { #[error("failed to decode request: {0}")] RequestDecodingError(String), - #[error("failed to decode request: {0}")] + #[error("failed to parse request: {0}")] RequestParsingError(#[from] serde_json::Error), #[error("request holds no event")] From 7ff5b80c28d7a11de6b92e42c18041a9e0b9b9b4 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 5 Dec 2023 14:17:59 +0100 Subject: [PATCH 087/249] feat: accept other JSON types as distinct_id, stringify them (#65) --- capture/src/capture.rs | 27 ++----- capture/src/event.rs | 159 +++++++++++++++++++++++++++++++++++------ 2 files changed, 146 insertions(+), 40 deletions(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 8cc37e0737a00..37e2872a9f9b4 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -79,12 +79,12 @@ pub async fn event( let payload = base64::engine::general_purpose::STANDARD .decode(input.data) .unwrap(); - RawEvent::from_bytes(&meta, payload.into()) + RawEvent::from_bytes(payload.into()) } ct => { tracing::Span::current().record("content_type", ct); - RawEvent::from_bytes(&meta, body) + RawEvent::from_bytes(body) } }?; @@ -165,19 +165,6 @@ pub fn process_single_event( event: &RawEvent, context: &ProcessingContext, ) -> Result { - let distinct_id = match &event.distinct_id { - Some(id) => id, - None => match event.properties.get("distinct_id").map(|v| v.as_str()) { - Some(Some(id)) => id, - _ => return Err(CaptureError::MissingDistinctId), - }, - }; - // Limit the size of distinct_id to 200 chars - let distinct_id: String = match distinct_id.len() { - 0..=200 => distinct_id.to_owned(), - _ => distinct_id.chars().take(200).collect(), - }; - if event.event.is_empty() { return Err(CaptureError::MissingEventName); } @@ -189,7 +176,7 @@ pub fn process_single_event( Ok(ProcessedEvent { uuid: event.uuid.unwrap_or_else(uuid_v7), - distinct_id, + distinct_id: event.extract_distinct_id()?, ip: context.client_ip.clone(), data, now: context.now.clone(), @@ -252,7 +239,7 @@ mod tests { let events = vec![ RawEvent { token: Some(String::from("hello")), - distinct_id: Some("testing".to_string()), + distinct_id: Some(json!("testing")), uuid: None, event: String::new(), properties: HashMap::new(), @@ -263,7 +250,7 @@ mod tests { }, RawEvent { token: None, - distinct_id: Some("testing".to_string()), + distinct_id: Some(json!("testing")), uuid: None, event: String::new(), properties: HashMap::from([(String::from("token"), json!("hello"))]), @@ -283,7 +270,7 @@ mod tests { let events = vec![ RawEvent { token: Some(String::from("hello")), - distinct_id: Some("testing".to_string()), + distinct_id: Some(json!("testing")), uuid: None, event: String::new(), properties: HashMap::new(), @@ -294,7 +281,7 @@ mod tests { }, RawEvent { token: None, - distinct_id: Some("testing".to_string()), + distinct_id: Some(json!("testing")), uuid: None, event: String::new(), properties: HashMap::from([(String::from("token"), json!("goodbye"))]), diff --git a/capture/src/event.rs b/capture/src/event.rs index 92b0f0bc9e444..ea71a3f276704 100644 --- a/capture/src/event.rs +++ b/capture/src/event.rs @@ -44,8 +44,8 @@ pub struct RawEvent { skip_serializing_if = "Option::is_none" )] pub token: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub distinct_id: Option, + #[serde(alias = "$distinct_id", skip_serializing_if = "Option::is_none")] + pub distinct_id: Option, // posthog-js accepts arbitrary values as distinct_id pub uuid: Option, pub event: String, #[serde(default)] @@ -87,7 +87,7 @@ impl RawEvent { /// Instead of trusting the parameter, we peek at the payload's first three bytes to /// detect gzip, fallback to uncompressed utf8 otherwise. #[instrument(skip_all)] - pub fn from_bytes(_query: &EventQuery, bytes: Bytes) -> Result, CaptureError> { + pub fn from_bytes(bytes: Bytes) -> Result, CaptureError> { tracing::debug!(len = bytes.len(), "decoding new event"); let payload = if bytes.starts_with(&GZIP_MAGIC_NUMBERS) { @@ -119,6 +119,30 @@ impl RawEvent { .map(String::from), } } + + /// Extracts, stringifies and trims the distinct_id to a 200 chars String. + /// SDKs send the distinct_id either in the root field or as a property, + /// and can send string, number, array, or map values. We try to best-effort + /// stringify complex values, and make sure it's not longer than 200 chars. + pub fn extract_distinct_id(&self) -> Result { + // Breaking change compared to capture-py: None / Null is not allowed. + let value = match &self.distinct_id { + None | Some(Value::Null) => match self.properties.get("distinct_id") { + None | Some(Value::Null) => return Err(CaptureError::MissingDistinctId), + Some(id) => id, + }, + Some(id) => id, + }; + + let distinct_id = value + .as_str() + .map(|s| s.to_owned()) + .unwrap_or_else(|| value.to_string()); + Ok(match distinct_id.len() { + 0..=200 => distinct_id, + _ => distinct_id.chars().take(200).collect(), + }) + } } #[derive(Debug)] @@ -150,29 +174,124 @@ impl ProcessedEvent { #[cfg(test)] mod tests { - use super::Compression; use base64::Engine as _; use bytes::Bytes; + use rand::distributions::Alphanumeric; + use rand::Rng; + use serde_json::json; - use super::{EventQuery, RawEvent}; + use super::CaptureError; + use super::RawEvent; #[test] - fn decode_bytes() { - let horrible_blob = "H4sIAAAAAAAAA31T207cMBD9lSrikSy+5bIrVX2g4oWWUlEqBEKRY08Sg4mD4+xCEf/e8XLZBSGeEp+ZOWOfmXPxkMAS+pAskp1BtmBBLiHZTQbvBvDBwJgsHpIdh5/kp1Rffp18OcMwAtUS/GhcjwFKZjSbkYjX3q1G8AgeGA+Nu4ughqVRUIX7ATDwHcbr4IYYUJP32LyavMVAF8Kw2NuzTknbuTEsSkIIHlvTf+vhLnzdizUxgslvs2JgkKHr5U1s8VS0dZ/NZSnlW7CVfTvhs7EG+vT0JJaMygP0VQem7bDTvBAbcGV06JAkIwTBpYHV4Hx4zS1FJH+FX7IFj7A1NbZZQR2b4GFbwFlWzFjETY/XCpXRiN538yt/S9mdnm7bSa+lDCY+kOalKDJGs/msZMVuos0YTK+e62hZciHqes7LnDcpoVmTg+TAaqnKMhWUaaa4TllBoCDpJn2uYK3k87xeyFjZFHWdzxmdq5Q0IstBzRXlDMiHbM/5kgnerKfs+tFZqHAolQflvDZ9W0Evawu6wveiENVoND4s+Ami2jBGZbayn/42g3xblizX4skp4FYMYfJQoSQf8DfSjrGBVMEsoWpArpMbK1vc8ItLDG1j1SDvrZM6muBxN/Eg7U1cVFw70KmyRl13bhqjYeBGGrtuFqWTSzzF/q8tRyvV9SfxHXQLoBuidXY0ekeF+KQnNCqgHXaIy7KJBncNERk6VUFhhB33j8zv5uhQ/rCTvbq9/9seH5Pj3Bf/TsuzYf9g2j+3h9N6yZ8Vfpmx4KSguSY5S0lOqc5LmgmhidoMmOaixoFvktFKOo9kK9Nrt3rPxViWk5RwIhtJykZzXohP2DjmZ08+bnH/4B1fkUnGSp2SMmNlIYTguS5ga//eERZZTSVeD8cWPTMGeTMgHSOMpyRLGftDyUKwBV9b6Dx5vPwPzQHjFwsFAAA="; - let decoded_horrible_blob = base64::engine::general_purpose::STANDARD - .decode(horrible_blob) - .unwrap(); - - let bytes = Bytes::from(decoded_horrible_blob); - let events = RawEvent::from_bytes( - &EventQuery { - compression: Some(Compression::Gzip), - lib_version: None, - sent_at: None, - }, - bytes, + fn decode_uncompressed_raw_event() { + let base64_payload = "ewogICAgImRpc3RpbmN0X2lkIjogIm15X2lkMSIsCiAgICAiZXZlbnQiOiAibXlfZXZlbnQxIiwKICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICIkZGV2aWNlX3R5cGUiOiAiRGVza3RvcCIKICAgIH0sCiAgICAiYXBpX2tleSI6ICJteV90b2tlbjEiCn0K"; + let compressed_bytes = Bytes::from( + base64::engine::general_purpose::STANDARD + .decode(base64_payload) + .expect("payload is not base64"), + ); + + let events = RawEvent::from_bytes(compressed_bytes).expect("failed to parse"); + assert_eq!(1, events.len()); + assert_eq!(Some("my_token1".to_string()), events[0].extract_token()); + assert_eq!("my_event1".to_string(), events[0].event); + assert_eq!( + "my_id1".to_string(), + events[0] + .extract_distinct_id() + .expect("cannot find distinct_id") + ); + } + #[test] + fn decode_gzipped_raw_event() { + let base64_payload = "H4sIADQSbmUCAz2MsQqAMAxE936FBEcnR2f/o4i9IRTb0AahiP9urcVMx3t3ucxQjxxn5bCrZUfLQEepYabpkzgRtOOWfyMpCpIyctVXY42PDifvsFoE73BF9hqFWuPu403YepT+WKNHmMnc5gENoFu2kwAAAA=="; + let compressed_bytes = Bytes::from( + base64::engine::general_purpose::STANDARD + .decode(base64_payload) + .expect("payload is not base64"), ); - assert!(events.is_ok()); + let events = RawEvent::from_bytes(compressed_bytes).expect("failed to parse"); + assert_eq!(1, events.len()); + assert_eq!(Some("my_token2".to_string()), events[0].extract_token()); + assert_eq!("my_event2".to_string(), events[0].event); + assert_eq!( + "my_id2".to_string(), + events[0] + .extract_distinct_id() + .expect("cannot find distinct_id") + ); + } + + #[test] + fn extract_distinct_id() { + let parse_and_extract = |input: &'static str| -> Result { + let parsed = RawEvent::from_bytes(input.into()).expect("failed to parse"); + parsed[0].extract_distinct_id() + }; + // Return MissingDistinctId if not found + assert!(matches!( + parse_and_extract(r#"{"event": "e"}"#), + Err(CaptureError::MissingDistinctId) + )); + // Return MissingDistinctId if null, breaking compat with capture-py + assert!(matches!( + parse_and_extract(r#"{"event": "e", "distinct_id": null}"#), + Err(CaptureError::MissingDistinctId) + )); + + let assert_extracted_id = |input: &'static str, expected: &str| { + let id = parse_and_extract(input).expect("failed to extract"); + assert_eq!(id, expected); + }; + // Happy path: toplevel field present + assert_extracted_id(r#"{"event": "e", "distinct_id": "myid"}"#, "myid"); + assert_extracted_id(r#"{"event": "e", "$distinct_id": "23"}"#, "23"); + + // Sourced from properties if not present in toplevel field, but toplevel wins if both present + assert_extracted_id( + r#"{"event": "e", "properties":{"distinct_id": "myid"}}"#, + "myid", + ); + assert_extracted_id( + r#"{"event": "e", "distinct_id": 23, "properties":{"distinct_id": "myid"}}"#, + "23", + ); + + // Numbers are stringified + assert_extracted_id(r#"{"event": "e", "distinct_id": 23}"#, "23"); + assert_extracted_id(r#"{"event": "e", "distinct_id": 23.4}"#, "23.4"); + + // Containers are stringified + assert_extracted_id( + r#"{"event": "e", "distinct_id": ["a", "b"]}"#, + r#"["a","b"]"#, + ); + assert_extracted_id( + r#"{"event": "e", "distinct_id": {"string": "a", "number": 3}}"#, + r#"{"number":3,"string":"a"}"#, + ); + } + + #[test] + fn extract_distinct_id_trims_to_200_chars() { + let distinct_id: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(222) + .map(char::from) + .collect(); + let (expected_distinct_id, _) = distinct_id.split_at(200); // works because ascii chars only + let input = json!([{ + "token": "mytoken", + "event": "myevent", + "distinct_id": distinct_id + }]); + + let parsed = RawEvent::from_bytes(input.to_string().into()).expect("failed to parse"); + assert_eq!( + parsed[0].extract_distinct_id().expect("failed to extract"), + expected_distinct_id + ); } } From fa43c0a8362d2ab2f045747abc6106a420871bb3 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 7 Dec 2023 16:50:03 +0100 Subject: [PATCH 088/249] fix MessageSizeTooLarge handling (#66) --- capture/src/sink.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/capture/src/sink.rs b/capture/src/sink.rs index 13397dcf83465..af83e20c1a763 100644 --- a/capture/src/sink.rs +++ b/capture/src/sink.rs @@ -192,7 +192,7 @@ impl KafkaSink { }) { Ok(ack) => Ok(ack), Err((e, _)) => match e.rdkafka_error_code() { - Some(RDKafkaErrorCode::InvalidMessageSize) => { + Some(RDKafkaErrorCode::MessageSizeTooLarge) => { report_dropped_events("kafka_message_size", 1); Err(CaptureError::EventTooBig) } @@ -297,6 +297,8 @@ mod tests { use crate::partition_limits::PartitionLimiter; use crate::sink::{EventSink, KafkaSink}; use crate::utils::uuid_v7; + use rand::distributions::Alphanumeric; + use rand::Rng; use rdkafka::mocking::MockCluster; use rdkafka::producer::DefaultProducerContext; use rdkafka::types::{RDKafkaApiKey, RDKafkaRespErr}; @@ -358,6 +360,27 @@ mod tests { .await .expect("failed to send initial event batch"); + // Producer should reject a 2MB message, twice the default `message.max.bytes` + let big_data = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(2_000_000) + .map(char::from) + .collect(); + let big_event: ProcessedEvent = ProcessedEvent { + uuid: uuid_v7(), + distinct_id: "id1".to_string(), + ip: "".to_string(), + data: big_data, + now: "".to_string(), + sent_at: None, + token: "token1".to_string(), + }; + match sink.send(big_event).await { + Err(CaptureError::EventTooBig) => {} // Expected + Err(err) => panic!("wrong error code {}", err), + Ok(()) => panic!("should have errored"), + }; + // Simulate unretriable errors cluster.clear_request_errors(RDKafkaApiKey::Produce); let err = [RDKafkaRespErr::RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE; 1]; From a63a00ce667c99fb4a185da3dd4b20c431f5d3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 13:46:09 +0100 Subject: [PATCH 089/249] feat: Include migrations and echo-server in docker-compose stack --- Dockerfile.sqlx | 5 +++++ README.md | 14 +++----------- docker-compose.yml | 25 +++++++++++++++++++++++++ docker/echo-server/Caddyfile | 17 +++++++++++++++++ hook-consumer/README.md | 2 ++ 5 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 Dockerfile.sqlx create mode 100644 docker/echo-server/Caddyfile create mode 100644 hook-consumer/README.md diff --git a/Dockerfile.sqlx b/Dockerfile.sqlx new file mode 100644 index 0000000000000..c55dfaa8a960a --- /dev/null +++ b/Dockerfile.sqlx @@ -0,0 +1,5 @@ +FROM docker.io/library/rust:1.74.0 + +RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres + +WORKDIR /sqlx diff --git a/README.md b/README.md index b17e7ae6ffbe7..a3a674c28ff38 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,16 @@ A reliable and performant webhook system for PostHog ## Requirements 1. [Rust](https://www.rust-lang.org/tools/install). -2. [sqlx-cli](https://crates.io/crates/sqlx-cli): To setup database and run migrations. -3. [Docker](https://docs.docker.com/engine/install/) or [podman](https://podman.io/docs/installation) (and [podman-compose](https://github.com/containers/podman-compose#installation)): To setup testing services. +2. [Docker](https://docs.docker.com/engine/install/), or [podman](https://podman.io/docs/installation) and [podman-compose](https://github.com/containers/podman-compose#installation): To setup development stack. ## Testing -1. Start a PostgreSQL instance: +1. Start development stack: ```bash docker compose -f docker-compose.yml up -d --wait ``` -2. Prepare test database: -```bash -export DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database -sqlx database create -sqlx migrate run -``` - -3. Test: +2. Test: ```bash cargo test ``` diff --git a/docker-compose.yml b/docker-compose.yml index 35b7a498d44b4..6f62692df0743 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ services: db: + container_name: db image: docker.io/library/postgres:16-alpine restart: on-failure environment: @@ -13,3 +14,27 @@ services: ports: - '15432:5432' command: postgres -c max_connections=1000 -c idle_in_transaction_session_timeout=300000 + + setup_test_db: + container_name: setup-test-db + build: + context: . + dockerfile: Dockerfile.sqlx + restart: on-failure + command: > + sh -c "sqlx database create && sqlx migrate run" + requires: + - db + environment: + DATABASE_URL: postgres://posthog:posthog@db:5432/test_database + volumes: + - ./migrations:/sqlx/migrations/ + + echo_server: + image: docker.io/library/caddy:2 + container_name: echo-server + restart: on-failure + ports: + - '18081:8081' + volumes: + - ./docker/echo-server/Caddyfile:/etc/caddy/Caddyfile diff --git a/docker/echo-server/Caddyfile b/docker/echo-server/Caddyfile new file mode 100644 index 0000000000000..a13ac68a24d6b --- /dev/null +++ b/docker/echo-server/Caddyfile @@ -0,0 +1,17 @@ +{ + auto_https off +} + +:8081 + +route /echo { + respond `{http.request.body}` 200 { + close + } +} + +route /fail { + respond `{http.request.body}` 400 { + close + } +} diff --git a/hook-consumer/README.md b/hook-consumer/README.md new file mode 100644 index 0000000000000..1adab6ea571f4 --- /dev/null +++ b/hook-consumer/README.md @@ -0,0 +1,2 @@ +# hook-consumer +Consume and process webhook jobs From 690888a250733c0f7036895fd64901f55b419380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 13:55:06 +0100 Subject: [PATCH 090/249] refactor: Support in pgqueue for consumer --- hook-common/src/pgqueue.rs | 237 ++++++++++++++++++++++--------------- 1 file changed, 140 insertions(+), 97 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 89aab7ee45ab9..7ed46550bd8bd 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -4,9 +4,10 @@ use std::default::Default; use std::str::FromStr; +use std::time; -use chrono::{prelude::*, Duration}; -use serde::{de::DeserializeOwned, Serialize}; +use chrono; +use serde; use sqlx::postgres::{PgPool, PgPoolOptions}; use thiserror::Error; @@ -18,12 +19,18 @@ pub enum PgQueueError { ConnectionError { error: sqlx::Error }, #[error("{command} query failed with: {error}")] QueryError { command: String, error: sqlx::Error }, - #[error("transaction {command} failed with: {error}")] - TransactionError { command: String, error: sqlx::Error }, #[error("{0} is not a valid JobStatus")] ParseJobStatusError(String), - #[error("{0} Job has reached max attempts and cannot be retried further")] - MaxAttemptsReachedError(String), +} + +#[derive(Error, Debug)] +pub enum PgJobError { + #[error("retry is an invalid state for this PgJob: {error}")] + RetryInvalidError { job: T, error: String }, + #[error("{command} query failed with: {error}")] + QueryError { command: String, error: sqlx::Error }, + #[error("transaction {command} failed with: {error}")] + TransactionError { command: String, error: sqlx::Error }, } /// Enumeration of possible statuses for a Job. @@ -64,18 +71,18 @@ impl FromStr for JobStatus { pub type JobParameters = sqlx::types::Json; /// A Job to be executed by a worker dequeueing a PgQueue. -#[derive(sqlx::FromRow)] +#[derive(sqlx::FromRow, Debug)] pub struct Job { /// A unique id identifying a job. pub id: i64, /// A number corresponding to the current job attempt. pub attempt: i32, /// A datetime corresponding to when the job was attempted. - pub attempted_at: DateTime, + pub attempted_at: chrono::DateTime, /// A vector of identifiers that have attempted this job. E.g. thread ids, pod names, etc... pub attempted_by: Vec, /// A datetime corresponding to when the job was created. - pub created_at: DateTime, + pub created_at: chrono::DateTime, /// The current job's number of max attempts. pub max_attempts: i32, /// Arbitrary job parameters stored as JSON. @@ -89,28 +96,29 @@ pub struct Job { } impl Job { + /// Return true if this job attempt is greater or equal to the maximum number of possible attempts. + pub fn is_gte_max_attempts(&self) -> bool { + self.attempt >= self.max_attempts + } + /// Consume Job to retry it. /// This returns a RetryableJob that can be enqueued by PgQueue. /// /// # Arguments /// /// * `error`: Any JSON-serializable value to be stored as an error. - pub fn retry(self, error: E) -> Result, PgQueueError> { - if self.attempt >= self.max_attempts { - Err(PgQueueError::MaxAttemptsReachedError(self.target)) - } else { - Ok(RetryableJob { - id: self.id, - attempt: self.attempt, - error: sqlx::types::Json(error), - queue: self.queue, - }) + fn retry(self, error: E) -> RetryableJob { + RetryableJob { + id: self.id, + attempt: self.attempt, + error: sqlx::types::Json(error), + queue: self.queue, } } /// Consume Job to complete it. /// This returns a CompletedJob that can be marked as completed by PgQueue. - pub fn complete(self) -> CompletedJob { + fn complete(self) -> CompletedJob { CompletedJob { id: self.id, queue: self.queue, @@ -123,7 +131,7 @@ impl Job { /// # Arguments /// /// * `error`: Any JSON-serializable value to be stored as an error. - pub fn fail(self, error: E) -> FailedJob { + fn fail(self, error: E) -> FailedJob { FailedJob { id: self.id, error: sqlx::types::Json(error), @@ -133,6 +141,7 @@ impl Job { } /// A Job that can be updated in PostgreSQL. +#[derive(Debug)] pub struct PgJob { pub job: Job, pub table: String, @@ -141,11 +150,21 @@ pub struct PgJob { } impl PgJob { - pub async fn retry( + pub async fn retry( mut self, error: E, - ) -> Result, PgQueueError> { - let retryable_job = self.job.retry(error)?; + preferred_retry_interval: Option, + ) -> Result, PgJobError>> { + if self.job.is_gte_max_attempts() { + return Err(PgJobError::RetryInvalidError { + job: self, + error: "Maximum attempts reached".to_owned(), + }); + } + let retryable_job = self.job.retry(error); + let retry_interval = self + .retry_policy + .time_until_next_retry(&retryable_job, preferred_retry_interval); let base_query = format!( r#" @@ -161,7 +180,6 @@ WHERE AND queue = $1 RETURNING "{0}".* - "#, &self.table ); @@ -169,11 +187,11 @@ RETURNING sqlx::query(&base_query) .bind(&retryable_job.queue) .bind(retryable_job.id) - .bind(self.retry_policy.time_until_next_retry(&retryable_job)) + .bind(retry_interval) .bind(&retryable_job.error) .execute(&mut *self.connection) .await - .map_err(|error| PgQueueError::QueryError { + .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -181,7 +199,7 @@ RETURNING Ok(retryable_job) } - pub async fn complete(mut self) -> Result { + pub async fn complete(mut self) -> Result>> { let completed_job = self.job.complete(); let base_query = format!( @@ -190,13 +208,12 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'completed'::job_status, + status = 'completed'::job_status WHERE "{0}".id = $2 AND queue = $1 RETURNING "{0}".* - "#, &self.table ); @@ -206,7 +223,7 @@ RETURNING .bind(completed_job.id) .execute(&mut *self.connection) .await - .map_err(|error| PgQueueError::QueryError { + .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -214,10 +231,10 @@ RETURNING Ok(completed_job) } - pub async fn fail( + pub async fn fail( mut self, error: E, - ) -> Result, PgQueueError> { + ) -> Result, PgJobError>> { let failed_job = self.job.fail(error); let base_query = format!( @@ -226,7 +243,7 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'failed'::job_status, + status = 'failed'::job_status WHERE "{0}".id = $2 AND queue = $1 @@ -242,7 +259,7 @@ RETURNING .bind(failed_job.id) .execute(&mut *self.connection) .await - .map_err(|error| PgQueueError::QueryError { + .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -253,6 +270,7 @@ RETURNING /// A Job within an open PostgreSQL transaction. /// This implementation allows 'hiding' the job from any other workers running SKIP LOCKED queries. +#[derive(Debug)] pub struct PgTransactionJob<'c, J> { pub job: Job, pub table: String, @@ -261,11 +279,21 @@ pub struct PgTransactionJob<'c, J> { } impl<'c, J> PgTransactionJob<'c, J> { - pub async fn retry( + pub async fn retry( mut self, error: E, - ) -> Result, PgQueueError> { - let retryable_job = self.job.retry(error)?; + preferred_retry_interval: Option, + ) -> Result, PgJobError>> { + if self.job.is_gte_max_attempts() { + return Err(PgJobError::RetryInvalidError { + job: self, + error: "Maximum attempts reached".to_owned(), + }); + } + let retryable_job = self.job.retry(error); + let retry_interval = self + .retry_policy + .time_until_next_retry(&retryable_job, preferred_retry_interval); let base_query = format!( r#" @@ -289,11 +317,11 @@ RETURNING sqlx::query(&base_query) .bind(&retryable_job.queue) .bind(retryable_job.id) - .bind(self.retry_policy.time_until_next_retry(&retryable_job)) + .bind(retry_interval) .bind(&retryable_job.error) .execute(&mut *self.transaction) .await - .map_err(|error| PgQueueError::QueryError { + .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -301,7 +329,7 @@ RETURNING self.transaction .commit() .await - .map_err(|error| PgQueueError::TransactionError { + .map_err(|error| PgJobError::TransactionError { command: "COMMIT".to_owned(), error, })?; @@ -309,7 +337,7 @@ RETURNING Ok(retryable_job) } - pub async fn complete(mut self) -> Result { + pub async fn complete(mut self) -> Result>> { let completed_job = self.job.complete(); let base_query = format!( @@ -318,13 +346,12 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'completed'::job_status, + status = 'completed'::job_status WHERE "{0}".id = $2 AND queue = $1 RETURNING "{0}".* - "#, &self.table ); @@ -334,7 +361,7 @@ RETURNING .bind(completed_job.id) .execute(&mut *self.transaction) .await - .map_err(|error| PgQueueError::QueryError { + .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -342,7 +369,7 @@ RETURNING self.transaction .commit() .await - .map_err(|error| PgQueueError::TransactionError { + .map_err(|error| PgJobError::TransactionError { command: "COMMIT".to_owned(), error, })?; @@ -350,10 +377,10 @@ RETURNING Ok(completed_job) } - pub async fn fail( + pub async fn fail( mut self, error: E, - ) -> Result, PgQueueError> { + ) -> Result, PgJobError>> { let failed_job = self.job.fail(error); let base_query = format!( @@ -362,13 +389,12 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'failed'::job_status, + status = 'failed'::job_status WHERE "{0}".id = $2 AND queue = $1 RETURNING "{0}".* - "#, &self.table ); @@ -378,7 +404,7 @@ RETURNING .bind(failed_job.id) .execute(&mut *self.transaction) .await - .map_err(|error| PgQueueError::QueryError { + .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -386,7 +412,7 @@ RETURNING self.transaction .commit() .await - .map_err(|error| PgQueueError::TransactionError { + .map_err(|error| PgJobError::TransactionError { command: "COMMIT".to_owned(), error, })?; @@ -399,7 +425,7 @@ RETURNING /// The time until retry will depend on the PgQueue's RetryPolicy. pub struct RetryableJob { /// A unique id identifying a job. - pub id: i64, + id: i64, /// A number corresponding to the current job attempt. pub attempt: i32, /// Any JSON-serializable value to be stored as an error. @@ -411,7 +437,7 @@ pub struct RetryableJob { /// A Job that has completed to be enqueued into a PgQueue and marked as completed. pub struct CompletedJob { /// A unique id identifying a job. - pub id: i64, + id: i64, /// A unique id identifying a job queue. pub queue: String, } @@ -419,7 +445,7 @@ pub struct CompletedJob { /// A Job that has failed to be enqueued into a PgQueue and marked as failed. pub struct FailedJob { /// A unique id identifying a job. - pub id: i64, + id: i64, /// Any JSON-serializable value to be stored as an error. pub error: sqlx::types::Json, /// A unique id identifying a job queue. @@ -446,27 +472,47 @@ impl NewJob { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] /// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. pub struct RetryPolicy { /// Coefficient to multiply initial_interval with for every past attempt. - backoff_coefficient: i32, + backoff_coefficient: u32, /// The backoff interval for the first retry. - initial_interval: Duration, + initial_interval: time::Duration, /// The maximum possible backoff between retries. - maximum_interval: Option, + maximum_interval: Option, } impl RetryPolicy { + pub fn new( + backoff_coefficient: u32, + initial_interval: time::Duration, + maximum_interval: Option, + ) -> Self { + Self { + backoff_coefficient, + initial_interval, + maximum_interval, + } + } + /// Calculate the time until the next retry for a given RetryableJob. - pub fn time_until_next_retry(&self, job: &RetryableJob) -> Duration { + pub fn time_until_next_retry( + &self, + job: &RetryableJob, + preferred_retry_interval: Option, + ) -> time::Duration { let candidate_interval = self.initial_interval * self.backoff_coefficient.pow(job.attempt as u32); - if let Some(max_interval) = self.maximum_interval { - std::cmp::min(candidate_interval, max_interval) - } else { - candidate_interval + match (preferred_retry_interval, self.maximum_interval) { + (Some(duration), Some(max_interval)) => std::cmp::min( + std::cmp::max(std::cmp::min(candidate_interval, max_interval), duration), + max_interval, + ), + (Some(duration), None) => std::cmp::max(candidate_interval, duration), + (None, Some(max_interval)) => std::cmp::min(candidate_interval, max_interval), + (None, None) => candidate_interval, } } } @@ -475,7 +521,7 @@ impl Default for RetryPolicy { fn default() -> Self { Self { backoff_coefficient: 2, - initial_interval: Duration::seconds(1), + initial_interval: time::Duration::from_secs(1), maximum_interval: None, } } @@ -491,8 +537,6 @@ pub struct PgQueue { retry_policy: RetryPolicy, /// The identifier of the PostgreSQL table this queue runs on. table: String, - /// The identifier of the worker listening on this queue. - worker: String, } pub type PgQueueResult = std::result::Result; @@ -511,12 +555,10 @@ impl PgQueue { queue_name: &str, table_name: &str, url: &str, - worker_name: &str, retry_policy: RetryPolicy, ) -> PgQueueResult { let name = queue_name.to_owned(); let table = table_name.to_owned(); - let worker = worker_name.to_owned(); let pool = PgPoolOptions::new() .connect(url) .await @@ -527,13 +569,15 @@ impl PgQueue { pool, retry_policy, table, - worker, }) } /// Dequeue a Job from this PgQueue to work on it. - pub async fn dequeue( + pub async fn dequeue< + J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, + >( &self, + attempted_by: &str, ) -> PgQueueResult>> { let mut connection = self .pool @@ -578,7 +622,7 @@ RETURNING let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) .bind(&self.name) - .bind(&self.worker) + .bind(attempted_by) .fetch_one(&mut *connection) .await; @@ -608,10 +652,12 @@ RETURNING /// Dequeue a Job from this PgQueue to work on it. pub async fn dequeue_tx< - J: DeserializeOwned + std::marker::Send + std::marker::Unpin + 'static, + 'a, + J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, >( &self, - ) -> PgQueueResult>> { + attempted_by: &str, + ) -> PgQueueResult>> { let mut tx = self .pool .begin() @@ -655,7 +701,7 @@ RETURNING let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) .bind(&self.name) - .bind(&self.worker) + .bind(attempted_by) .fetch_one(&mut *tx) .await; @@ -678,7 +724,7 @@ RETURNING /// Enqueue a Job into this PgQueue. /// We take ownership of NewJob to enforce a specific NewJob is only enqueued once. - pub async fn enqueue( + pub async fn enqueue( &self, job: NewJob, ) -> PgQueueResult<()> { @@ -712,9 +758,8 @@ VALUES #[cfg(test)] mod tests { use super::*; - use serde::Deserialize; - #[derive(Serialize, Deserialize, PartialEq, Debug)] + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] struct JobParameters { method: String, body: String, @@ -752,7 +797,6 @@ mod tests { "test_can_dequeue_job", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - &worker_id, RetryPolicy::default(), ) .await @@ -761,7 +805,7 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let pg_job: PgJob = queue - .dequeue() + .dequeue(&worker_id) .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); @@ -782,14 +826,15 @@ mod tests { "test_dequeue_returns_none_on_no_jobs", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - &worker_id, RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); - let pg_job: Option> = - queue.dequeue().await.expect("failed to dequeue job"); + let pg_job: Option> = queue + .dequeue(&worker_id) + .await + .expect("failed to dequeue job"); assert!(pg_job.is_none()); } @@ -805,7 +850,6 @@ mod tests { "test_can_dequeue_tx_job", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - &worker_id, RetryPolicy::default(), ) .await @@ -814,7 +858,7 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let tx_job: PgTransactionJob<'_, JobParameters> = queue - .dequeue_tx() + .dequeue_tx(&worker_id) .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); @@ -835,14 +879,15 @@ mod tests { "test_dequeue_tx_returns_none_on_no_jobs", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - &worker_id, RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); - let tx_job: Option> = - queue.dequeue_tx().await.expect("failed to dequeue job"); + let tx_job: Option> = queue + .dequeue_tx(&worker_id) + .await + .expect("failed to dequeue job"); assert!(tx_job.is_none()); } @@ -855,7 +900,7 @@ mod tests { let new_job = NewJob::new(2, job_parameters, &job_target); let retry_policy = RetryPolicy { backoff_coefficient: 0, - initial_interval: Duration::seconds(0), + initial_interval: time::Duration::from_secs(0), maximum_interval: None, }; @@ -863,7 +908,6 @@ mod tests { "test_can_retry_job_with_remaining_attempts", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - &worker_id, retry_policy, ) .await @@ -871,16 +915,16 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let job: PgJob = queue - .dequeue() + .dequeue(&worker_id) .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); let _ = job - .retry("a very reasonable failure reason") + .retry("a very reasonable failure reason", None) .await .expect("failed to retry job"); let retried_job: PgJob = queue - .dequeue() + .dequeue(&worker_id) .await .expect("failed to dequeue job") .expect("didn't find retried job to dequeue"); @@ -906,7 +950,7 @@ mod tests { let new_job = NewJob::new(1, job_parameters, &job_target); let retry_policy = RetryPolicy { backoff_coefficient: 0, - initial_interval: Duration::seconds(0), + initial_interval: time::Duration::from_secs(0), maximum_interval: None, }; @@ -914,7 +958,6 @@ mod tests { "test_cannot_retry_job_without_remaining_attempts", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - &worker_id, retry_policy, ) .await @@ -923,11 +966,11 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let job: PgJob = queue - .dequeue() + .dequeue(&worker_id) .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); - job.retry("a very reasonable failure reason") + job.retry("a very reasonable failure reason", None) .await .expect("failed to retry job"); } From 7824d69da6dd104650ef3f8fcc245be8b8b6a6cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 13:55:21 +0100 Subject: [PATCH 091/249] feat: First consumer implementation --- hook-consumer/Cargo.toml | 13 + hook-consumer/src/config.rs | 56 ++++ hook-consumer/src/consumer.rs | 492 ++++++++++++++++++++++++++++++++++ hook-consumer/src/lib.rs | 2 + hook-consumer/src/main.rs | 34 ++- 5 files changed, 595 insertions(+), 2 deletions(-) create mode 100644 hook-consumer/src/config.rs create mode 100644 hook-consumer/src/consumer.rs create mode 100644 hook-consumer/src/lib.rs diff --git a/hook-consumer/Cargo.toml b/hook-consumer/Cargo.toml index 49c2d9f84b17d..2e95a6b071903 100644 --- a/hook-consumer/Cargo.toml +++ b/hook-consumer/Cargo.toml @@ -6,3 +6,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-std = { version = "1.12" } +chrono = { version = "0.4" } +envconfig = { version = "0.10" } +futures = "0.3" +hook-common = { path = "../hook-common" } +http = { version = "0.2" } +reqwest = { version = "0.11" } +serde = { version = "1.0" } +serde_derive = { version = "1.0" } +sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json", "chrono" ] } +thiserror = { version = "1.0" } +tokio = { version = "1.34", features = ["macros", "rt", "rt-multi-thread"] } +url = { version = "2.2" } diff --git a/hook-consumer/src/config.rs b/hook-consumer/src/config.rs new file mode 100644 index 0000000000000..fde137337e8f8 --- /dev/null +++ b/hook-consumer/src/config.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; +use std::time; + +use envconfig::Envconfig; + +#[derive(Envconfig, Clone)] +pub struct Config { + #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] + pub database_url: String, + + #[envconfig(default = "consumer")] + pub consumer_name: String, + + #[envconfig(default = "default")] + pub queue_name: String, + + #[envconfig(default = "100")] + pub poll_interval: EnvMsDuration, + + #[envconfig(default = "5000")] + pub request_timeout: EnvMsDuration, + + #[envconfig(nested = true)] + pub retry_policy: RetryPolicyConfig, + + #[envconfig(default = "job_queue")] + pub table_name: String, +} + +#[derive(Debug, Clone, Copy)] +pub struct EnvMsDuration(pub time::Duration); + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseEnvMsDurationError; + +impl FromStr for EnvMsDuration { + type Err = ParseEnvMsDurationError; + + fn from_str(s: &str) -> Result { + let ms = s.parse::().map_err(|_| ParseEnvMsDurationError)?; + + Ok(EnvMsDuration(time::Duration::from_millis(ms))) + } +} + +#[derive(Envconfig, Clone)] +pub struct RetryPolicyConfig { + #[envconfig(default = "2")] + pub backoff_coefficient: u32, + + #[envconfig(default = "1000")] + pub initial_interval: EnvMsDuration, + + #[envconfig(default = "100000")] + pub maximum_interval: EnvMsDuration, +} diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs new file mode 100644 index 0000000000000..33b09b0caacbe --- /dev/null +++ b/hook-consumer/src/consumer.rs @@ -0,0 +1,492 @@ +use std::collections; +use std::fmt; +use std::str::FromStr; +use std::time; + +use async_std::task; +use hook_common::pgqueue::{PgJobError, PgQueue, PgQueueError, PgTransactionJob}; +use http::StatusCode; +use serde::{de::Visitor, Deserialize, Serialize}; +use thiserror::Error; + +/// Enumeration of errors for operations with WebhookConsumer. +#[derive(Error, Debug)] +pub enum WebhookConsumerError { + #[error("timed out while waiting for jobs to be available")] + TimeoutError, + #[error("{0} is not a valid HttpMethod")] + ParseHttpMethodError(String), + #[error("error parsing webhook headers")] + ParseHeadersError(http::Error), + #[error("error parsing webhook url")] + ParseUrlError(url::ParseError), + #[error("an error occurred in the underlying queue")] + QueueError(#[from] PgQueueError), + #[error("an error occurred in the underlying job")] + PgJobError(String), + #[error("an error occurred when attempting to send a request")] + RequestError(#[from] reqwest::Error), + #[error("a webhook could not be delivered but it could be retried later: {reason}")] + RetryableWebhookError { + reason: String, + retry_after: Option, + }, + #[error("a webhook could not be delivered and it cannot be retried further: {0}")] + NonRetryableWebhookError(String), +} + +/// Supported HTTP methods for webhooks. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum HttpMethod { + DELETE, + GET, + PATCH, + POST, + PUT, +} + +/// Allow casting `HttpMethod` from strings. +impl FromStr for HttpMethod { + type Err = WebhookConsumerError; + + fn from_str(s: &str) -> Result { + match s.to_ascii_uppercase().as_ref() { + "DELETE" => Ok(HttpMethod::DELETE), + "GET" => Ok(HttpMethod::GET), + "PATCH" => Ok(HttpMethod::PATCH), + "POST" => Ok(HttpMethod::POST), + "PUT" => Ok(HttpMethod::PUT), + invalid => Err(WebhookConsumerError::ParseHttpMethodError( + invalid.to_owned(), + )), + } + } +} + +/// Implement `std::fmt::Display` to convert HttpMethod to string. +impl fmt::Display for HttpMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + HttpMethod::DELETE => write!(f, "DELETE"), + HttpMethod::GET => write!(f, "GET"), + HttpMethod::PATCH => write!(f, "PATCH"), + HttpMethod::POST => write!(f, "POST"), + HttpMethod::PUT => write!(f, "PUT"), + } + } +} + +struct HttpMethodVisitor; + +impl<'de> Visitor<'de> for HttpMethodVisitor { + type Value = HttpMethod; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "the string representation of HttpMethod") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + match HttpMethod::from_str(s) { + Ok(method) => Ok(method), + Err(_) => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &self, + )), + } + } +} + +/// Deserialize required to read `HttpMethod` from database. +impl<'de> Deserialize<'de> for HttpMethod { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(HttpMethodVisitor) + } +} + +/// Serialize required to write `HttpMethod` to database. +impl Serialize for HttpMethod { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// Convinience to cast `HttpMethod` to `http::Method`. +/// Not all `http::Method` variants are valid `HttpMethod` variants, hence why we +/// can't just use the former. +impl Into for HttpMethod { + fn into(self) -> http::Method { + match self { + HttpMethod::DELETE => http::Method::DELETE, + HttpMethod::GET => http::Method::GET, + HttpMethod::PATCH => http::Method::PATCH, + HttpMethod::POST => http::Method::POST, + HttpMethod::PUT => http::Method::PUT, + } + } +} + +impl Into for &HttpMethod { + fn into(self) -> http::Method { + match self { + HttpMethod::DELETE => http::Method::DELETE, + HttpMethod::GET => http::Method::GET, + HttpMethod::PATCH => http::Method::PATCH, + HttpMethod::POST => http::Method::POST, + HttpMethod::PUT => http::Method::PUT, + } + } +} + +/// `JobParameters` required for the `WebhookConsumer` to execute a webhook. +/// These parameters should match the exported Webhook interface that PostHog plugins. +/// implement. See: https://github.com/PostHog/plugin-scaffold/blob/main/src/types.ts#L15. +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct WebhookJobParameters { + body: String, + headers: collections::HashMap, + method: HttpMethod, + url: String, +} + +/// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. +pub struct WebhookConsumer<'p> { + /// An identifier for this consumer. Used to mark jobs we have consumed. + name: String, + /// The queue we will be dequeuing jobs from. + queue: &'p PgQueue, + /// The interval for polling the queue. + poll_interval: time::Duration, + /// A timeout for webhook requests. + request_timeout: time::Duration, +} + +impl<'p> WebhookConsumer<'p> { + pub fn new( + name: &str, + queue: &'p PgQueue, + poll_interval: time::Duration, + request_timeout: time::Duration, + ) -> Self { + Self { + name: name.to_owned(), + queue, + poll_interval, + request_timeout, + } + } + + /// Wait until a job becomes available in our queue. + async fn wait_for_job<'a>( + &self, + ) -> Result, WebhookConsumerError> { + loop { + if let Some(job) = self.queue.dequeue_tx(&self.name).await? { + return Ok(job); + } else { + task::sleep(self.poll_interval).await; + } + } + } + + /// Run this consumer to continuously process any jobs that become available. + pub async fn run(&self) -> Result<(), WebhookConsumerError> { + loop { + let webhook_job = self.wait_for_job().await?; + + let request_timeout = self.request_timeout; // Required to avoid capturing self in closure. + tokio::spawn(async move { process_webhook_job(webhook_job, request_timeout) }); + } + } +} + +/// Process a webhook job by transitioning it to its appropriate state after its request is sent. +/// After we finish, the webhook job will be set as completed (if the request was successful), retryable (if the request +/// was unsuccessful but we can still attempt a retry), or failed (if the request was unsuccessful and no more retries +/// may be attempted). +/// +/// A webhook job is considered retryable after a failing request if: +/// 1. The job has attempts remaining (i.e. hasn't reached `max_attempts`), and... +/// 2. The status code indicates retrying at a later point could resolve the issue. This means: 429 and any 5XX. +/// +/// # Arguments +/// +/// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. +/// * `request_timeout`: A timeout for the HTTP request. +async fn process_webhook_job( + webhook_job: PgTransactionJob<'_, WebhookJobParameters>, + request_timeout: std::time::Duration, +) -> Result<(), WebhookConsumerError> { + match send_webhook( + &webhook_job.job.parameters.method, + &webhook_job.job.parameters.url, + &webhook_job.job.parameters.headers, + webhook_job.job.parameters.body.clone(), + request_timeout, + ) + .await + { + Ok(_) => { + webhook_job + .complete() + .await + .map_err(|error| WebhookConsumerError::PgJobError(error.to_string()))?; + Ok(()) + } + Err(WebhookConsumerError::RetryableWebhookError { + reason, + retry_after, + }) => match webhook_job.retry(reason.to_string(), retry_after).await { + Ok(_) => Ok(()), + Err(PgJobError::RetryInvalidError { + job: webhook_job, + error: fail_error, + }) => { + webhook_job + .fail(fail_error.to_string()) + .await + .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), + }, + Err(error) => { + webhook_job + .fail(error.to_string()) + .await + .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + } +} + +/// Make an HTTP request to a webhook endpoint. +/// +/// # Arguments +/// +/// * `method`: The HTTP method to use in the HTTP request. +/// * `url`: The URL we are targetting with our request. Parsing this URL fail. +/// * `headers`: Key, value pairs of HTTP headers in a `std::collections::HashMap`. Can fail if headers are not valid. +/// * `body`: The body of the request. Ownership is required. +/// * `timeout`: A timeout for the HTTP request. +async fn send_webhook( + method: &HttpMethod, + url: &str, + headers: &collections::HashMap, + body: String, + timeout: std::time::Duration, +) -> Result { + let client = reqwest::Client::new(); + let method: http::Method = method.into(); + let url: reqwest::Url = (url) + .parse() + .map_err(|error| WebhookConsumerError::ParseUrlError(error))?; + let headers: reqwest::header::HeaderMap = (headers) + .try_into() + .map_err(|error| WebhookConsumerError::ParseHeadersError(error))?; + + let body = reqwest::Body::from(body); + let response = client + .request(method, url) + .headers(headers) + .timeout(timeout) + .body(body) + .send() + .await?; + + let status = response.status(); + + if status.is_success() { + Ok(response) + } else if is_retryable_status(status) { + let retry_after = parse_retry_after_header(response.headers()); + + Err(WebhookConsumerError::RetryableWebhookError { + reason: format!("retryable status code {}", status), + retry_after, + }) + } else { + Err(WebhookConsumerError::NonRetryableWebhookError(format!( + "non-retryable status code {}", + status + ))) + } +} + +fn is_retryable_status(status: StatusCode) -> bool { + status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error() +} + +/// Attempt to parse a chrono::Duration from a Retry-After header, returning None if not possible. +/// Retry-After header can specify a date in RFC2822 or a number of seconds; we try to parse both. +/// If a Retry-After header is not present in the provided `header_map`, `None` is returned. +/// +/// # Arguments +/// +/// * `header_map`: A `&reqwest::HeaderMap` of response headers that could contain Retry-After. +fn parse_retry_after_header(header_map: &reqwest::header::HeaderMap) -> Option { + let retry_after_header = header_map.get(reqwest::header::RETRY_AFTER); + + let retry_after = match retry_after_header { + Some(header_value) => match header_value.to_str() { + Ok(s) => s, + Err(_) => { + return None; + } + }, + None => { + return None; + } + }; + + if let Ok(u) = u64::from_str_radix(retry_after, 10) { + let duration = time::Duration::from_secs(u); + return Some(duration); + } + + if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(retry_after) { + let duration = + chrono::DateTime::::from(dt) - chrono::offset::Utc::now(); + + // This can only fail when negative, in which case we return None. + return duration.to_std().ok(); + } + + None +} + +mod tests { + use super::*; + // Note we are ignoring some warnings in this module. + // This is due to a long-standing cargo bug that reports imports and helper functions as unused. + // See: https://github.com/rust-lang/rust/issues/46379. + #[allow(unused_imports)] + use hook_common::pgqueue::{JobStatus, NewJob, RetryPolicy}; + + /// Use process id as a worker id for tests. + #[allow(dead_code)] + fn worker_id() -> String { + std::process::id().to_string() + } + + #[allow(dead_code)] + async fn enqueue_job( + queue: &PgQueue, + max_attempts: i32, + job_parameters: WebhookJobParameters, + ) -> Result<(), PgQueueError> { + let job_target = job_parameters.url.to_owned(); + let new_job = NewJob::new(max_attempts, job_parameters, &job_target); + queue.enqueue(new_job).await?; + Ok(()) + } + + #[test] + fn test_is_retryable_status() { + assert!(!is_retryable_status(http::StatusCode::FORBIDDEN)); + assert!(!is_retryable_status(http::StatusCode::BAD_REQUEST)); + assert!(is_retryable_status(http::StatusCode::TOO_MANY_REQUESTS)); + assert!(is_retryable_status(http::StatusCode::INTERNAL_SERVER_ERROR)); + } + + #[test] + fn test_parse_retry_after_header() { + let mut headers = reqwest::header::HeaderMap::new(); + headers.insert(reqwest::header::RETRY_AFTER, "120".parse().unwrap()); + + let duration = parse_retry_after_header(&headers).unwrap(); + assert_eq!(duration, time::Duration::from_secs(120)); + + headers.remove(reqwest::header::RETRY_AFTER); + + let duration = parse_retry_after_header(&headers); + assert_eq!(duration, None); + + headers.insert( + reqwest::header::RETRY_AFTER, + "Wed, 21 Oct 2015 07:28:00 GMT".parse().unwrap(), + ); + + let duration = parse_retry_after_header(&headers); + assert_eq!(duration, None); + } + + #[tokio::test] + async fn test_wait_for_job() { + let worker_id = worker_id(); + let queue_name = "test_wait_for_job".to_string(); + let table_name = "job_queue".to_string(); + let db_url = "postgres://posthog:posthog@localhost:15432/test_database".to_string(); + let queue = PgQueue::new(&queue_name, &table_name, &db_url, RetryPolicy::default()) + .await + .expect("failed to connect to PG"); + + let webhook_job = WebhookJobParameters { + body: "a webhook job body. much wow.".to_owned(), + headers: collections::HashMap::new(), + method: HttpMethod::POST, + url: "localhost".to_owned(), + }; + // enqueue takes ownership of the job enqueued to avoid bugs that can cause duplicate jobs. + // Normally, a separate application would be enqueueing jobs for us to consume, so no ownership + // conflicts would arise. However, in this test we need to do the enqueueing ourselves. + // So, we clone the job to keep it around and assert the values returned by wait_for_job. + enqueue_job(&queue, 1, webhook_job.clone()) + .await + .expect("failed to enqueue job"); + let consumer = WebhookConsumer::new( + &worker_id, + &queue, + time::Duration::from_millis(100), + time::Duration::from_millis(5000), + ); + let consumed_job = consumer + .wait_for_job() + .await + .expect("failed to wait and read job"); + + assert_eq!(consumed_job.job.attempt, 1); + assert!(consumed_job.job.attempted_by.contains(&worker_id)); + assert_eq!(consumed_job.job.attempted_by.len(), 1); + assert_eq!(consumed_job.job.max_attempts, 1); + assert_eq!(*consumed_job.job.parameters.as_ref(), webhook_job); + assert_eq!(consumed_job.job.status, JobStatus::Running); + assert_eq!(consumed_job.job.target, webhook_job.url); + + consumed_job + .complete() + .await + .expect("job not successfully completed"); + } + + #[tokio::test] + async fn test_send_webhook() { + let method = HttpMethod::POST; + let url = "http://localhost:18081/echo"; + let headers = collections::HashMap::new(); + let body = "a very relevant request body"; + let response = send_webhook( + &method, + url, + &headers, + body.to_owned(), + time::Duration::from_millis(5000), + ) + .await + .expect("send_webhook failed"); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.text().await.expect("failed to read response body"), + body.to_owned(), + ); + } +} diff --git a/hook-consumer/src/lib.rs b/hook-consumer/src/lib.rs new file mode 100644 index 0000000000000..cc746b0833b0c --- /dev/null +++ b/hook-consumer/src/lib.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod consumer; diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index e7a11a969c037..22acee1263ef8 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -1,3 +1,33 @@ -fn main() { - println!("Hello, world!"); +use envconfig::Envconfig; + +use hook_common::pgqueue::{PgQueue, RetryPolicy}; +use hook_consumer::config::Config; +use hook_consumer::consumer::WebhookConsumer; + +#[tokio::main] +async fn main() { + let config = Config::init_from_env().expect("Invalid configuration:"); + + let retry_policy = RetryPolicy::new( + config.retry_policy.backoff_coefficient, + config.retry_policy.initial_interval.0, + Some(config.retry_policy.maximum_interval.0), + ); + let queue = PgQueue::new( + &config.queue_name, + &config.table_name, + &config.database_url, + retry_policy, + ) + .await + .expect("failed to initialize queue"); + + let consumer = WebhookConsumer::new( + &config.consumer_name, + &queue, + config.poll_interval.0, + config.request_timeout.0, + ); + + let _ = consumer.run().await; } From d91c90a89af31c4895b7fc4109b20caff90a9bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 14:17:08 +0100 Subject: [PATCH 092/249] chore: Cargo lock update --- Cargo.lock | 731 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 559 insertions(+), 172 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 451d42483e301..07643585e3152 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,16 +52,149 @@ dependencies = [ ] [[package]] -name = "async-trait" -version = "0.1.74" +name = "async-channel" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +dependencies = [ + "concurrent-queue", + "event-listener 4.0.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.2.0", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite 2.1.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4353121d5644cdf2beb5726ab752e79a8db1ebb52031770ec47db31d245526" +dependencies = [ + "async-channel 2.1.1", + "async-executor", + "async-io 2.2.1", + "async-lock 3.2.0", + "blocking", + "futures-lite 2.1.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" +dependencies = [ + "async-lock 3.2.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.1.0", + "parking", + "polling 3.3.1", + "rustix 0.38.25", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", ] +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + [[package]] name = "atoi" version = "2.0.0" @@ -71,6 +204,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atomic-write-file" version = "0.1.2" @@ -191,6 +330,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel 2.1.1", + "async-lock 3.2.0", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.1.0", + "piper", + "tracing", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -238,6 +393,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.5" @@ -364,6 +528,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "envconfig" version = "0.10.0" @@ -418,13 +591,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] -name = "eyre" -version = "0.6.9" +name = "event-listener" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" dependencies = [ - "indenter", - "once_cell", + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", ] [[package]] @@ -480,6 +673,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.29" @@ -524,6 +732,45 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "futures-sink" version = "0.3.29" @@ -542,8 +789,10 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -579,40 +828,37 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "h2" -version = "0.4.0" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 1.0.0", - "indexmap 2.1.0", + "http", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.14.3" @@ -695,6 +941,21 @@ dependencies = [ [[package]] name = "hook-consumer" version = "0.1.0" +dependencies = [ + "async-std", + "chrono", + "envconfig", + "futures", + "hook-common", + "http", + "reqwest", + "serde", + "serde_derive", + "sqlx", + "thiserror", + "tokio", + "url", +] [[package]] name = "hook-producer" @@ -840,6 +1101,77 @@ dependencies = [ "tracing", ] +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -899,6 +1231,26 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -929,6 +1281,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -963,7 +1324,13 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" @@ -982,6 +1349,9 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] [[package]] name = "mach2" @@ -1014,70 +1384,6 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metrics" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5" -dependencies = [ - "ahash", - "metrics-macros", - "portable-atomic", -] - -[[package]] -name = "metrics-exporter-prometheus" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" -dependencies = [ - "base64", - "hyper 0.14.27", - "indexmap 1.9.3", - "ipnet", - "metrics", - "metrics-util", - "quanta", - "thiserror", - "tokio", - "tracing", -] - -[[package]] -name = "metrics-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "metrics-util" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de2ed6e491ed114b40b732e4d1659a9d53992ebd87490c44a6ffe23739d973e" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "hashbrown 0.13.1", - "metrics", - "num_cpus", - "quanta", - "sketches-ddsketch", -] - [[package]] name = "mime" version = "0.3.17" @@ -1277,10 +1583,10 @@ dependencies = [ ] [[package]] -name = "overload" -version = "0.1.1" +name = "parking" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" @@ -1358,6 +1664,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -1386,10 +1703,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] -name = "portable-atomic" -version = "1.5.1" +name = "polling" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bccab0e7fd7cc19f820a1c8c91720af652d0c88dc9664dd72aef2614f04af3b" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.25", + "tracing", + "windows-sys 0.52.0", +] [[package]] name = "ppv-lite86" @@ -1479,6 +1820,44 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rsa" version = "0.9.6" @@ -1507,15 +1886,29 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.26" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.11", + "windows-sys 0.48.0", ] [[package]] @@ -1599,16 +1992,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1776,7 +2159,7 @@ dependencies = [ "crossbeam-queue", "dotenvy", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -1994,10 +2377,25 @@ dependencies = [ ] [[package]] -name = "sync_wrapper" -version = "0.1.2" +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "tempfile" @@ -2006,9 +2404,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", - "fastrand", + "fastrand 2.0.1", "redox_syscall", - "rustix", + "rustix 0.38.25", "windows-sys 0.48.0", ] @@ -2068,9 +2466,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", @@ -2087,6 +2483,16 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -2112,28 +2518,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - [[package]] name = "tower-service" version = "0.3.2" @@ -2173,31 +2557,6 @@ dependencies = [ "valuable", ] -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - [[package]] name = "try-lock" version = "0.2.4" @@ -2267,10 +2626,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" [[package]] -name = "valuable" -version = "0.1.0" +name = "value-bag" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" [[package]] name = "vcpkg" @@ -2284,6 +2643,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + [[package]] name = "want" version = "0.3.1" @@ -2324,6 +2689,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.89" @@ -2532,6 +2909,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "zerocopy" version = "0.7.28" From 3bdd1c05d736835eaa8454fd3348fac60430bf02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 14:17:35 +0100 Subject: [PATCH 093/249] chore: Remove install of sqlx from CI --- .github/workflows/rust.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ee0e5b3ebc785..5ddac411a47bf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -49,13 +49,6 @@ jobs: docker compose -f docker-compose.yml down docker compose -f docker-compose.yml up -d --wait - - name: Run migrations - shell: bash - run: | - cargo install sqlx-cli --no-default-features --features native-tls,postgres - DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database sqlx database create - DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database sqlx migrate run - - uses: actions/cache@v3 with: path: | From c25ac6fde1d322b1c6cdccab574ae503d4ae83da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 14:17:46 +0100 Subject: [PATCH 094/249] fix: Use depends_on instead of requires --- docker-compose.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6f62692df0743..afaf48ef86d06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,8 +23,10 @@ services: restart: on-failure command: > sh -c "sqlx database create && sqlx migrate run" - requires: - - db + depends_on: + db: + condition: service_healthy + restart: true environment: DATABASE_URL: postgres://posthog:posthog@db:5432/test_database volumes: From 8c7cb796bca701d7b2b520f15582d7c90bb8dd2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 14:25:33 +0100 Subject: [PATCH 095/249] fix: Address clippy linting issues --- hook-consumer/src/consumer.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 33b09b0caacbe..bf9f8187c7b97 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -121,10 +121,10 @@ impl Serialize for HttpMethod { /// Convinience to cast `HttpMethod` to `http::Method`. /// Not all `http::Method` variants are valid `HttpMethod` variants, hence why we -/// can't just use the former. -impl Into for HttpMethod { - fn into(self) -> http::Method { - match self { +/// can't just use the former or implement `From`. +impl From for http::Method { + fn from(val: HttpMethod) -> Self { + match val { HttpMethod::DELETE => http::Method::DELETE, HttpMethod::GET => http::Method::GET, HttpMethod::PATCH => http::Method::PATCH, @@ -134,9 +134,9 @@ impl Into for HttpMethod { } } -impl Into for &HttpMethod { - fn into(self) -> http::Method { - match self { +impl From<&HttpMethod> for http::Method { + fn from(val: &HttpMethod) -> Self { + match val { HttpMethod::DELETE => http::Method::DELETE, HttpMethod::GET => http::Method::GET, HttpMethod::PATCH => http::Method::PATCH, @@ -203,7 +203,7 @@ impl<'p> WebhookConsumer<'p> { let webhook_job = self.wait_for_job().await?; let request_timeout = self.request_timeout; // Required to avoid capturing self in closure. - tokio::spawn(async move { process_webhook_job(webhook_job, request_timeout) }); + tokio::spawn(async move { process_webhook_job(webhook_job, request_timeout).await }); } } } @@ -286,12 +286,10 @@ async fn send_webhook( ) -> Result { let client = reqwest::Client::new(); let method: http::Method = method.into(); - let url: reqwest::Url = (url) - .parse() - .map_err(|error| WebhookConsumerError::ParseUrlError(error))?; + let url: reqwest::Url = (url).parse().map_err(WebhookConsumerError::ParseUrlError)?; let headers: reqwest::header::HeaderMap = (headers) .try_into() - .map_err(|error| WebhookConsumerError::ParseHeadersError(error))?; + .map_err(WebhookConsumerError::ParseHeadersError)?; let body = reqwest::Body::from(body); let response = client @@ -347,7 +345,7 @@ fn parse_retry_after_header(header_map: &reqwest::header::HeaderMap) -> Option() { let duration = time::Duration::from_secs(u); return Some(duration); } From 2d99f77d80ffaae8fb1e754e85d4b8774b8dfc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 6 Dec 2023 14:36:44 +0100 Subject: [PATCH 096/249] fix: Split up docker compose start up in CI --- .github/workflows/rust.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5ddac411a47bf..b811c1a52e668 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -47,7 +47,8 @@ jobs: shell: bash run: | docker compose -f docker-compose.yml down - docker compose -f docker-compose.yml up -d --wait + docker compose -f docker-compose.yml up db echo_server -d --wait + docker compose -f docker-compose.yml up setup_test_db - uses: actions/cache@v3 with: From 71d59299c2dc49608ae8807a57b0ecc738c73d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 7 Dec 2023 10:51:33 +0100 Subject: [PATCH 097/249] fix: Typo Co-authored-by: Brett Hoerner --- hook-consumer/src/consumer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index bf9f8187c7b97..a8f2fa61941c8 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -119,7 +119,7 @@ impl Serialize for HttpMethod { } } -/// Convinience to cast `HttpMethod` to `http::Method`. +/// Convenience to cast `HttpMethod` to `http::Method`. /// Not all `http::Method` variants are valid `HttpMethod` variants, hence why we /// can't just use the former or implement `From`. impl From for http::Method { From 09acfef888a5c1835ca9118a3320fe7995c6a1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 11:37:24 +0100 Subject: [PATCH 098/249] fix: Delete Cargo.lock --- Cargo.lock | 2946 ---------------------------------------------------- 1 file changed, 2946 deletions(-) delete mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 07643585e3152..0000000000000 --- a/Cargo.lock +++ /dev/null @@ -1,2946 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" -dependencies = [ - "cfg-if", - "getrandom", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" -dependencies = [ - "concurrent-queue", - "event-listener 4.0.0", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" -dependencies = [ - "async-lock 3.2.0", - "async-task", - "concurrent-queue", - "fastrand 2.0.1", - "futures-lite 2.1.0", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4353121d5644cdf2beb5726ab752e79a8db1ebb52031770ec47db31d245526" -dependencies = [ - "async-channel 2.1.1", - "async-executor", - "async-io 2.2.1", - "async-lock 3.2.0", - "blocking", - "futures-lite 2.1.0", - "once_cell", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" -dependencies = [ - "async-lock 3.2.0", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.1.0", - "parking", - "polling 3.3.1", - "rustix 0.38.25", - "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" -dependencies = [ - "event-listener 4.0.0", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-std" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 1.13.0", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "atomic-write-file" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" -dependencies = [ - "nix", - "rand", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "axum" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" -dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "http-body-util", - "hyper 1.0.1", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base64" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" -dependencies = [ - "serde", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blocking" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" -dependencies = [ - "async-channel 2.1.1", - "async-lock 3.2.0", - "async-task", - "fastrand 2.0.1", - "futures-io", - "futures-lite 2.1.0", - "piper", - "tracing", -] - -[[package]] -name = "bumpalo" -version = "3.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" - -[[package]] -name = "cc" -version = "1.0.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets 0.48.5", -] - -[[package]] -name = "concurrent-queue" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "const-oid" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" - -[[package]] -name = "cpufeatures" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "der" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" -dependencies = [ - "serde", -] - -[[package]] -name = "encoding_rs" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "envconfig" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea81cc7e21f55a9d9b1efb6816904978d0bfbe31a50347cb24b2e75564bcac9b" -dependencies = [ - "envconfig_derive", -] - -[[package]] -name = "envconfig_derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfca278e5f84b45519acaaff758ebfa01f18e96998bc24b8f1b722dd804b9bf" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "equivalent" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" - -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.0", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fastrand" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" - -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "futures-core", - "futures-sink", - "spin 0.9.8", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" - -[[package]] -name = "futures-executor" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - -[[package]] -name = "futures-io" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" - -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" -dependencies = [ - "fastrand 2.0.1", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "futures-sink" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" - -[[package]] -name = "futures-task" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" - -[[package]] -name = "futures-util" -version = "0.3.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "gimli" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" - -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "h2" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.3", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "hook-common" -version = "0.1.0" -dependencies = [ - "chrono", - "serde", - "serde_derive", - "sqlx", - "thiserror", - "tokio", -] - -[[package]] -name = "hook-consumer" -version = "0.1.0" -dependencies = [ - "async-std", - "chrono", - "envconfig", - "futures", - "hook-common", - "http", - "reqwest", - "serde", - "serde_derive", - "sqlx", - "thiserror", - "tokio", - "url", -] - -[[package]] -name = "hook-producer" -version = "0.1.0" -dependencies = [ - "axum", - "envconfig", - "eyre", - "metrics", - "metrics-exporter-prometheus", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "http" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http 0.2.11", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http 1.0.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" -dependencies = [ - "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 0.2.11", - "http-body 0.4.5", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.4.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http 1.0.0", - "http-body 1.0.0", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "hyper-util" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", - "hyper 1.0.1", - "pin-project-lite", - "socket2 0.5.5", - "tokio", - "tower", - "tower-service", - "tracing", -] - -[[package]] -name = "http" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.4.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" -dependencies = [ - "equivalent", - "hashbrown 0.14.3", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "ipnet" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" - -[[package]] -name = "js-sys" -version = "0.3.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin 0.5.2", -] - -[[package]] -name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "libsqlite3-sys" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" - -[[package]] -name = "lock_api" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -dependencies = [ - "value-bag", -] - -[[package]] -name = "mach2" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" -dependencies = [ - "libc", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.4.1", - "cfg-if", - "libc", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "openssl" -version = "0.10.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800" -dependencies = [ - "bitflags 2.4.1", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.48.5", -] - -[[package]] -name = "paste" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" -dependencies = [ - "atomic-waker", - "fastrand 2.0.1", - "futures-io", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" -dependencies = [ - "cfg-if", - "concurrent-queue", - "pin-project-lite", - "rustix 0.38.25", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quanta" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" -dependencies = [ - "crossbeam-utils", - "libc", - "mach2", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - -[[package]] -name = "quote" -version = "1.0.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "raw-cpuid" -version = "10.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "reqwest" -version = "0.11.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "rsa" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" - -[[package]] -name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" -dependencies = [ - "bitflags 2.4.1", - "errno", - "libc", - "linux-raw-sys 0.4.11", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustversion" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" - -[[package]] -name = "ryu" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" - -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "serde" -version = "1.0.193" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.193" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "serde_json" -version = "1.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "sketches-ddsketch" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlformat" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" -dependencies = [ - "itertools", - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" -dependencies = [ - "ahash", - "atoi", - "byteorder", - "bytes", - "chrono", - "crc", - "crossbeam-queue", - "dotenvy", - "either", - "event-listener 2.5.3", - "futures-channel", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashlink", - "hex", - "indexmap 2.1.0", - "log", - "memchr", - "native-tls", - "once_cell", - "paste", - "percent-encoding", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlformat", - "thiserror", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sqlx-macros" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 1.0.109", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" -dependencies = [ - "atomic-write-file", - "dotenvy", - "either", - "heck", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 1.0.109", - "tempfile", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" -dependencies = [ - "atoi", - "base64", - "bitflags 2.4.1", - "byteorder", - "bytes", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand", - "rsa", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" -dependencies = [ - "atoi", - "base64", - "bitflags 2.4.1", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "rand", - "serde", - "serde_json", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "sqlx-core", - "tracing", - "url", - "urlencoding", - "uuid", -] - -[[package]] -name = "stringprep" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" -dependencies = [ - "finl_unicode", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" -dependencies = [ - "cfg-if", - "fastrand 2.0.1", - "redox_syscall", - "rustix 0.38.25", - "windows-sys 0.48.0", -] - -[[package]] -name = "thiserror" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "pin-project-lite", - "socket2 0.5.5", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "tracing-core" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "typenum" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "url" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "uuid" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" - -[[package]] -name = "value-bag" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.39", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" - -[[package]] -name = "web-sys" -version = "0.3.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "whoami" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.51.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "zerocopy" -version = "0.7.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6f15f7ade05d2a4935e34a457b936c23dc70a05cc1d97133dc99e7a3fe0f0e" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbbad221e3f78500350ecbd7dfa4e63ef945c05f4c61cb7f4d3f84cd0bba649b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.39", -] - -[[package]] -name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" From 6d3080de965279d6b0bb82ce9c331eee2b6a9ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 11:39:00 +0100 Subject: [PATCH 099/249] fix: Re-add Cargo.lock --- Cargo.lock | 3091 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3091 insertions(+) create mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000000..b24af98ae2169 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3091 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" +dependencies = [ + "concurrent-queue", + "event-listener 4.0.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.2.0", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite 2.1.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4353121d5644cdf2beb5726ab752e79a8db1ebb52031770ec47db31d245526" +dependencies = [ + "async-channel 2.1.1", + "async-executor", + "async-io 2.2.1", + "async-lock 3.2.0", + "blocking", + "futures-lite 2.1.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" +dependencies = [ + "async-lock 3.2.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.1.0", + "parking", + "polling 3.3.1", + "rustix 0.38.27", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" +dependencies = [ + "event-listener 4.0.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic-write-file" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" +dependencies = [ + "nix", + "rand", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.0.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel 2.1.1", + "async-lock 3.2.0", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.1.0", + "piper", + "tracing", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.48.5", +] + +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "envconfig" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea81cc7e21f55a9d9b1efb6816904978d0bfbe31a50347cb24b2e75564bcac9b" +dependencies = [ + "envconfig_derive", +] + +[[package]] +name = "envconfig_derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfca278e5f84b45519acaaff758ebfa01f18e96998bc24b8f1b722dd804b9bf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.0", + "pin-project-lite", +] + +[[package]] +name = "eyre" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bbb8258be8305fb0237d7b295f47bb24ff1b136a535f473baf40e70468515aa" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.3", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "hook-common" +version = "0.1.0" +dependencies = [ + "chrono", + "serde", + "serde_derive", + "sqlx", + "thiserror", + "tokio", +] + +[[package]] +name = "hook-consumer" +version = "0.1.0" +dependencies = [ + "async-std", + "chrono", + "envconfig", + "futures", + "hook-common", + "http 0.2.11", + "reqwest", + "serde", + "serde_derive", + "sqlx", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "hook-producer" +version = "0.1.0" +dependencies = [ + "axum", + "envconfig", + "eyre", + "metrics", + "metrics-exporter-prometheus", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.5", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.0", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.27", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.0.1", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metrics" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5" +dependencies = [ + "ahash", + "metrics-macros", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" +dependencies = [ + "base64", + "hyper 0.14.27", + "indexmap 1.9.3", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "metrics-util" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de2ed6e491ed114b40b732e4d1659a9d53992ebd87490c44a6ffe23739d973e" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.13.1", + "metrics", + "num_cpus", + "quanta", + "sketches-ddsketch", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite", + "rustix 0.38.27", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.22", + "http 0.2.11", + "http-body 0.4.5", + "hyper 0.14.27", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfeae074e687625746172d639330f1de242a178bf3189b51e35a7a21573513ac" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys 0.4.12", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener 2.5.3", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.1.0", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +dependencies = [ + "atomic-write-file", + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +dependencies = [ + "atoi", + "base64", + "bitflags 2.4.1", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", + "uuid", +] + +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand 2.0.1", + "redox_syscall", + "rustix 0.38.27", + "windows-sys 0.48.0", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d075cf85bbb114e933343e087b92f2146bac0d55b534cbb8188becf0039948e" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86cd5ca076997b97ef09d3ad65efe811fa68c9e874cb636ccb211223a813b0c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" From f76915e0b54eeb852910844c63f3afb433b1867f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 11:41:06 +0100 Subject: [PATCH 100/249] chore: Add comment referencing connection limit --- hook-consumer/src/consumer.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index a8f2fa61941c8..cff54f68f21ef 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -200,6 +200,7 @@ impl<'p> WebhookConsumer<'p> { /// Run this consumer to continuously process any jobs that become available. pub async fn run(&self) -> Result<(), WebhookConsumerError> { loop { + // TODO: The number of jobs processed will be capped by the PG connection limit when running in transactional mode. let webhook_job = self.wait_for_job().await?; let request_timeout = self.request_timeout; // Required to avoid capturing self in closure. From 104717937ec5f1537f6b587e0b3fb1839b98435a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 12:23:41 +0100 Subject: [PATCH 101/249] refactor: Re-use client in consumer --- hook-consumer/src/consumer.rs | 83 ++++++++++++++--------------------- hook-consumer/src/error.rs | 30 +++++++++++++ hook-consumer/src/lib.rs | 1 + hook-consumer/src/main.rs | 7 ++- 4 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 hook-consumer/src/error.rs diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index cff54f68f21ef..d18aed5eee368 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -6,34 +6,10 @@ use std::time; use async_std::task; use hook_common::pgqueue::{PgJobError, PgQueue, PgQueueError, PgTransactionJob}; use http::StatusCode; +use reqwest::header; use serde::{de::Visitor, Deserialize, Serialize}; -use thiserror::Error; - -/// Enumeration of errors for operations with WebhookConsumer. -#[derive(Error, Debug)] -pub enum WebhookConsumerError { - #[error("timed out while waiting for jobs to be available")] - TimeoutError, - #[error("{0} is not a valid HttpMethod")] - ParseHttpMethodError(String), - #[error("error parsing webhook headers")] - ParseHeadersError(http::Error), - #[error("error parsing webhook url")] - ParseUrlError(url::ParseError), - #[error("an error occurred in the underlying queue")] - QueueError(#[from] PgQueueError), - #[error("an error occurred in the underlying job")] - PgJobError(String), - #[error("an error occurred when attempting to send a request")] - RequestError(#[from] reqwest::Error), - #[error("a webhook could not be delivered but it could be retried later: {reason}")] - RetryableWebhookError { - reason: String, - retry_after: Option, - }, - #[error("a webhook could not be delivered and it cannot be retried further: {0}")] - NonRetryableWebhookError(String), -} + +use crate::error::WebhookConsumerError; /// Supported HTTP methods for webhooks. #[derive(Debug, PartialEq, Clone, Copy)] @@ -165,8 +141,8 @@ pub struct WebhookConsumer<'p> { queue: &'p PgQueue, /// The interval for polling the queue. poll_interval: time::Duration, - /// A timeout for webhook requests. - request_timeout: time::Duration, + /// The client used for HTTP requests. + client: reqwest::Client, } impl<'p> WebhookConsumer<'p> { @@ -175,13 +151,24 @@ impl<'p> WebhookConsumer<'p> { queue: &'p PgQueue, poll_interval: time::Duration, request_timeout: time::Duration, - ) -> Self { - Self { + ) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + + let client = reqwest::Client::builder() + .default_headers(headers) + .timeout(request_timeout) + .build()?; + + Ok(Self { name: name.to_owned(), queue, poll_interval, - request_timeout, - } + client, + }) } /// Wait until a job becomes available in our queue. @@ -203,8 +190,9 @@ impl<'p> WebhookConsumer<'p> { // TODO: The number of jobs processed will be capped by the PG connection limit when running in transactional mode. let webhook_job = self.wait_for_job().await?; - let request_timeout = self.request_timeout; // Required to avoid capturing self in closure. - tokio::spawn(async move { process_webhook_job(webhook_job, request_timeout).await }); + // reqwest::Client internally wraps with Arc, so this allocation is cheap. + let client = self.client.clone(); + tokio::spawn(async move { process_webhook_job(client, webhook_job).await }); } } } @@ -223,15 +211,15 @@ impl<'p> WebhookConsumer<'p> { /// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. /// * `request_timeout`: A timeout for the HTTP request. async fn process_webhook_job( + client: reqwest::Client, webhook_job: PgTransactionJob<'_, WebhookJobParameters>, - request_timeout: std::time::Duration, ) -> Result<(), WebhookConsumerError> { match send_webhook( + client, &webhook_job.job.parameters.method, &webhook_job.job.parameters.url, &webhook_job.job.parameters.headers, webhook_job.job.parameters.body.clone(), - request_timeout, ) .await { @@ -279,13 +267,12 @@ async fn process_webhook_job( /// * `body`: The body of the request. Ownership is required. /// * `timeout`: A timeout for the HTTP request. async fn send_webhook( + client: reqwest::Client, method: &HttpMethod, url: &str, headers: &collections::HashMap, body: String, - timeout: std::time::Duration, ) -> Result { - let client = reqwest::Client::new(); let method: http::Method = method.into(); let url: reqwest::Url = (url).parse().map_err(WebhookConsumerError::ParseUrlError)?; let headers: reqwest::header::HeaderMap = (headers) @@ -296,7 +283,6 @@ async fn send_webhook( let response = client .request(method, url) .headers(headers) - .timeout(timeout) .body(body) .send() .await?; @@ -446,7 +432,8 @@ mod tests { &queue, time::Duration::from_millis(100), time::Duration::from_millis(5000), - ); + ) + .expect("consumer failed to initialize"); let consumed_job = consumer .wait_for_job() .await @@ -472,15 +459,11 @@ mod tests { let url = "http://localhost:18081/echo"; let headers = collections::HashMap::new(); let body = "a very relevant request body"; - let response = send_webhook( - &method, - url, - &headers, - body.to_owned(), - time::Duration::from_millis(5000), - ) - .await - .expect("send_webhook failed"); + let client = reqwest::Client::new(); + + let response = send_webhook(client, &method, url, &headers, body.to_owned()) + .await + .expect("send_webhook failed"); assert_eq!(response.status(), StatusCode::OK); assert_eq!( diff --git a/hook-consumer/src/error.rs b/hook-consumer/src/error.rs new file mode 100644 index 0000000000000..34f0619ada4f0 --- /dev/null +++ b/hook-consumer/src/error.rs @@ -0,0 +1,30 @@ +use std::time; + +use hook_common::pgqueue; +use thiserror::Error; + +/// Enumeration of errors for operations with WebhookConsumer. +#[derive(Error, Debug)] +pub enum WebhookConsumerError { + #[error("timed out while waiting for jobs to be available")] + TimeoutError, + #[error("{0} is not a valid HttpMethod")] + ParseHttpMethodError(String), + #[error("error parsing webhook headers")] + ParseHeadersError(http::Error), + #[error("error parsing webhook url")] + ParseUrlError(url::ParseError), + #[error("an error occurred in the underlying queue")] + QueueError(#[from] pgqueue::PgQueueError), + #[error("an error occurred in the underlying job")] + PgJobError(String), + #[error("an error occurred when attempting to send a request")] + RequestError(#[from] reqwest::Error), + #[error("a webhook could not be delivered but it could be retried later: {reason}")] + RetryableWebhookError { + reason: String, + retry_after: Option, + }, + #[error("a webhook could not be delivered and it cannot be retried further: {0}")] + NonRetryableWebhookError(String), +} diff --git a/hook-consumer/src/lib.rs b/hook-consumer/src/lib.rs index cc746b0833b0c..b99481bbc3b58 100644 --- a/hook-consumer/src/lib.rs +++ b/hook-consumer/src/lib.rs @@ -1,2 +1,3 @@ pub mod config; pub mod consumer; +pub mod error; diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index 22acee1263ef8..16515640914d8 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -3,9 +3,10 @@ use envconfig::Envconfig; use hook_common::pgqueue::{PgQueue, RetryPolicy}; use hook_consumer::config::Config; use hook_consumer::consumer::WebhookConsumer; +use hook_consumer::error::WebhookConsumerError; #[tokio::main] -async fn main() { +async fn main() -> Result<(), WebhookConsumerError> { let config = Config::init_from_env().expect("Invalid configuration:"); let retry_policy = RetryPolicy::new( @@ -27,7 +28,9 @@ async fn main() { &queue, config.poll_interval.0, config.request_timeout.0, - ); + )?; let _ = consumer.run().await; + + Ok(()) } From 0525d7b71728fdfbd7915c6c49308325a921460f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 14:59:42 +0100 Subject: [PATCH 102/249] fix: Limit number of concurrent HTTP requests with Semaphore --- hook-consumer/src/config.rs | 3 +++ hook-consumer/src/consumer.rs | 19 +++++++++++++++++-- hook-consumer/src/main.rs | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/hook-consumer/src/config.rs b/hook-consumer/src/config.rs index fde137337e8f8..50d0f1cd4ec7d 100644 --- a/hook-consumer/src/config.rs +++ b/hook-consumer/src/config.rs @@ -20,6 +20,9 @@ pub struct Config { #[envconfig(default = "5000")] pub request_timeout: EnvMsDuration, + #[envconfig(default = "1024")] + pub max_requests: usize, + #[envconfig(nested = true)] pub retry_policy: RetryPolicyConfig, diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index d18aed5eee368..fba6cde74f301 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -1,6 +1,7 @@ use std::collections; use std::fmt; use std::str::FromStr; +use std::sync::Arc; use std::time; use async_std::task; @@ -8,6 +9,7 @@ use hook_common::pgqueue::{PgJobError, PgQueue, PgQueueError, PgTransactionJob}; use http::StatusCode; use reqwest::header; use serde::{de::Visitor, Deserialize, Serialize}; +use tokio::sync; use crate::error::WebhookConsumerError; @@ -143,6 +145,8 @@ pub struct WebhookConsumer<'p> { poll_interval: time::Duration, /// The client used for HTTP requests. client: reqwest::Client, + /// Maximum number of concurrent HTTP requests. + max_requests: usize, } impl<'p> WebhookConsumer<'p> { @@ -151,6 +155,7 @@ impl<'p> WebhookConsumer<'p> { queue: &'p PgQueue, poll_interval: time::Duration, request_timeout: time::Duration, + max_requests: usize, ) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( @@ -168,6 +173,7 @@ impl<'p> WebhookConsumer<'p> { queue, poll_interval, client, + max_requests, }) } @@ -186,13 +192,21 @@ impl<'p> WebhookConsumer<'p> { /// Run this consumer to continuously process any jobs that become available. pub async fn run(&self) -> Result<(), WebhookConsumerError> { + let semaphore = Arc::new(sync::Semaphore::new(self.max_requests)); + loop { // TODO: The number of jobs processed will be capped by the PG connection limit when running in transactional mode. let webhook_job = self.wait_for_job().await?; // reqwest::Client internally wraps with Arc, so this allocation is cheap. let client = self.client.clone(); - tokio::spawn(async move { process_webhook_job(client, webhook_job).await }); + let permit = semaphore.clone().acquire_owned().await.unwrap(); + + tokio::spawn(async move { + let result = process_webhook_job(client, webhook_job).await; + drop(permit); + result.expect("webhook processing failed"); + }); } } } @@ -278,8 +292,8 @@ async fn send_webhook( let headers: reqwest::header::HeaderMap = (headers) .try_into() .map_err(WebhookConsumerError::ParseHeadersError)?; - let body = reqwest::Body::from(body); + let response = client .request(method, url) .headers(headers) @@ -432,6 +446,7 @@ mod tests { &queue, time::Duration::from_millis(100), time::Duration::from_millis(5000), + 10, ) .expect("consumer failed to initialize"); let consumed_job = consumer diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index 16515640914d8..5f0d46dce133c 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -28,6 +28,7 @@ async fn main() -> Result<(), WebhookConsumerError> { &queue, config.poll_interval.0, config.request_timeout.0, + config.max_requests, )?; let _ = consumer.run().await; From 35825378b0fa68a71bcb98403e57d33c4e602858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 16:31:06 +0100 Subject: [PATCH 103/249] chore: Use workspace dependencies --- hook-consumer/Cargo.toml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/hook-consumer/Cargo.toml b/hook-consumer/Cargo.toml index 2e95a6b071903..5ff1eb08eb660 100644 --- a/hook-consumer/Cargo.toml +++ b/hook-consumer/Cargo.toml @@ -3,19 +3,17 @@ name = "hook-consumer" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] async-std = { version = "1.12" } -chrono = { version = "0.4" } -envconfig = { version = "0.10" } +chrono = { workspace = true } +envconfig = { workspace = true } futures = "0.3" hook-common = { path = "../hook-common" } http = { version = "0.2" } reqwest = { version = "0.11" } -serde = { version = "1.0" } -serde_derive = { version = "1.0" } -sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json", "chrono" ] } -thiserror = { version = "1.0" } -tokio = { version = "1.34", features = ["macros", "rt", "rt-multi-thread"] } +serde = { workspace = true } +serde_derive = { workspace = true } +sqlx = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } url = { version = "2.2" } From 31b1e7ae0e6f19fcf9cf40fd0e2a87e779667f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 16:37:43 +0100 Subject: [PATCH 104/249] refactor: Rename to max_concurrent_jobs --- hook-consumer/src/config.rs | 2 +- hook-consumer/src/consumer.rs | 10 +++++----- hook-consumer/src/main.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/hook-consumer/src/config.rs b/hook-consumer/src/config.rs index 50d0f1cd4ec7d..8e4bde9d956be 100644 --- a/hook-consumer/src/config.rs +++ b/hook-consumer/src/config.rs @@ -21,7 +21,7 @@ pub struct Config { pub request_timeout: EnvMsDuration, #[envconfig(default = "1024")] - pub max_requests: usize, + pub max_concurrent_jobs: usize, #[envconfig(nested = true)] pub retry_policy: RetryPolicyConfig, diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index fba6cde74f301..9abe9bddd09f6 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -145,8 +145,8 @@ pub struct WebhookConsumer<'p> { poll_interval: time::Duration, /// The client used for HTTP requests. client: reqwest::Client, - /// Maximum number of concurrent HTTP requests. - max_requests: usize, + /// Maximum number of concurrent jobs being processed. + max_concurrent_jobs: usize, } impl<'p> WebhookConsumer<'p> { @@ -155,7 +155,7 @@ impl<'p> WebhookConsumer<'p> { queue: &'p PgQueue, poll_interval: time::Duration, request_timeout: time::Duration, - max_requests: usize, + max_concurrent_jobs: usize, ) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( @@ -173,7 +173,7 @@ impl<'p> WebhookConsumer<'p> { queue, poll_interval, client, - max_requests, + max_concurrent_jobs, }) } @@ -192,7 +192,7 @@ impl<'p> WebhookConsumer<'p> { /// Run this consumer to continuously process any jobs that become available. pub async fn run(&self) -> Result<(), WebhookConsumerError> { - let semaphore = Arc::new(sync::Semaphore::new(self.max_requests)); + let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); loop { // TODO: The number of jobs processed will be capped by the PG connection limit when running in transactional mode. diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index 5f0d46dce133c..f165b32409de1 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -28,7 +28,7 @@ async fn main() -> Result<(), WebhookConsumerError> { &queue, config.poll_interval.0, config.request_timeout.0, - config.max_requests, + config.max_concurrent_jobs, )?; let _ = consumer.run().await; From 5cdff6a884d6ed2ecaae8b880d2bd70ccfcf6d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 16:38:39 +0100 Subject: [PATCH 105/249] chore: Remove deprecated comment --- hook-consumer/src/consumer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 9abe9bddd09f6..653a8907df984 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -195,7 +195,6 @@ impl<'p> WebhookConsumer<'p> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); loop { - // TODO: The number of jobs processed will be capped by the PG connection limit when running in transactional mode. let webhook_job = self.wait_for_job().await?; // reqwest::Client internally wraps with Arc, so this allocation is cheap. From 1358e3b62260dab524751a1d4a4538c99b6ddcce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 16:58:30 +0100 Subject: [PATCH 106/249] refactor: Return result from processing task --- hook-consumer/src/consumer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 653a8907df984..8da2df690c2fd 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -204,7 +204,7 @@ impl<'p> WebhookConsumer<'p> { tokio::spawn(async move { let result = process_webhook_job(client, webhook_job).await; drop(permit); - result.expect("webhook processing failed"); + result }); } } From 332c3d569afce1702edd0fe90714b457ccbcc5a3 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 11 Dec 2023 18:00:05 +0100 Subject: [PATCH 107/249] release: ship debug symbols in image for continuous profiling (#67) --- .dockerignore | 1 + Cargo.toml | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000..eb5a316cbd195 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +target diff --git a/Cargo.toml b/Cargo.toml index 983cbf2b7d696..faf9d2a0088be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,9 @@ members = [ "capture-server" ] +[profile.release] +debug = 2 # https://www.polarsignals.com/docs/rust + [workspace.dependencies] assert-json-diff = "2.0.2" axum = "0.6.15" From fb078fcc80cb275c7cb59ee16d3ebbe827e8eec2 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 12 Dec 2023 11:29:58 +0100 Subject: [PATCH 108/249] report partition_limits_key_count metric (#69) --- capture/src/partition_limits.rs | 11 +++++++++++ capture/src/server.rs | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/capture/src/partition_limits.rs b/capture/src/partition_limits.rs index 386665780ad1c..cd0148f10e1f4 100644 --- a/capture/src/partition_limits.rs +++ b/capture/src/partition_limits.rs @@ -11,6 +11,7 @@ use std::num::NonZeroU32; use std::sync::Arc; use governor::{clock, state::keyed::DefaultKeyedStateStore, Quota, RateLimiter}; +use metrics::gauge; // See: https://docs.rs/governor/latest/governor/_guide/index.html#usage-in-multiple-threads #[derive(Clone)] @@ -38,6 +39,16 @@ impl PartitionLimiter { pub fn is_limited(&self, key: &String) -> bool { self.forced_keys.contains(key) || self.limiter.check_key(key).is_err() } + + /// Reports the number of tracked keys to prometheus every 10 seconds, + /// needs to be spawned in a separate task. + pub async fn report_metrics(&self) { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(10)); + loop { + interval.tick().await; + gauge!("partition_limits_key_count", self.limiter.len() as f64); + } + } } #[cfg(test)] diff --git a/capture/src/server.rs b/capture/src/server.rs index 32bafa83b252c..3eca676f3763f 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -49,6 +49,12 @@ where config.burst_limit, config.overflow_forced_keys, ); + if config.export_prometheus { + let partition = partition.clone(); + tokio::spawn(async move { + partition.report_metrics().await; + }); + } let sink = sink::KafkaSink::new(config.kafka, sink_liveness, partition) .expect("failed to start Kafka sink"); From f714f32ddfde6b0793180a09fa3a81b35d5abaec Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 12 Dec 2023 11:30:25 +0100 Subject: [PATCH 109/249] revert rdkafka to 0.34 to evaluate impact (#68) --- .github/workflows/rust.yml | 1 + Cargo.lock | 8 ++++---- Cargo.toml | 2 +- capture-server/tests/common.rs | 1 + 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index cadd2fb6ee3a6..91cf2018ee7e4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -35,6 +35,7 @@ jobs: test: runs-on: buildjet-4vcpu-ubuntu-2204 + timeout-minutes: 10 steps: - uses: actions/checkout@v3 diff --git a/Cargo.lock b/Cargo.lock index 6bf02dfc43650..8e3b8737e2daa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1531,9 +1531,9 @@ dependencies = [ [[package]] name = "rdkafka" -version = "0.36.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54f02a5a40220f8a2dfa47ddb38ba9064475a5807a69504b6f91711df2eea63" +checksum = "053adfa02fab06e86c01d586cc68aa47ee0ff4489a59469081dc12cbcde578bf" dependencies = [ "futures-channel", "futures-util", @@ -1549,9 +1549,9 @@ dependencies = [ [[package]] name = "rdkafka-sys" -version = "4.7.0+2.3.0" +version = "4.6.0+2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e0d2f9ba6253f6ec72385e453294f8618e9e15c2c6aba2a5c01ccf9622d615" +checksum = "ad63c279fca41a27c231c450a2d2ad18288032e9cbb159ad16c9d96eba35aaaf" dependencies = [ "cmake", "libc", diff --git a/Cargo.toml b/Cargo.toml index faf9d2a0088be..ecd9b6a0ad78b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ uuid = { version = "1.3.3", features = ["serde"] } async-trait = "0.1.68" serde_urlencoded = "0.7.1" rand = "0.8.5" -rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl"] } +rdkafka = { version = "0.34.0", features = ["cmake-build", "ssl"] } metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" thiserror = "1.0.48" diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index b71fdf62c9a35..ce31897583f16 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -177,6 +177,7 @@ impl EphemeralTopic { impl Drop for EphemeralTopic { fn drop(&mut self) { debug!("dropping EphemeralTopic {}...", self.topic_name); + _ = self.consumer.unassign(); self.consumer.unsubscribe(); match futures::executor::block_on(timeout( Duration::from_secs(10), From 3c7479a51a59cb5a512c84a0a76b9447965f28e6 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Tue, 12 Dec 2023 16:16:27 +0000 Subject: [PATCH 110/249] chore(ci): add short sha to docker ci image (#71) --- .github/workflows/docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1b5f29bd90da3..77e731ecebcf2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -31,6 +31,7 @@ jobs: type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} + type=sha - name: Set up Docker Buildx id: buildx From eae1cb13b6c534f73aba37e21d211b71e6b1eb36 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 7 Dec 2023 15:34:39 -0700 Subject: [PATCH 111/249] Add basic webhook produce endpoint --- Cargo.lock | 9 + Cargo.toml | 20 +- hook-common/Cargo.toml | 7 +- hook-common/src/lib.rs | 1 + hook-common/src/pgqueue.rs | 10 +- hook-common/src/webhook.rs | 139 +++++++++++++ hook-consumer/src/consumer.rs | 132 +----------- hook-producer/Cargo.toml | 18 +- hook-producer/src/config.rs | 9 + hook-producer/src/handlers/app.rs | 60 ++++++ hook-producer/src/handlers/index.rs | 3 - hook-producer/src/handlers/mod.rs | 17 +- hook-producer/src/handlers/webhook.rs | 278 ++++++++++++++++++++++++++ hook-producer/src/main.rs | 22 +- 14 files changed, 562 insertions(+), 163 deletions(-) create mode 100644 hook-common/src/webhook.rs create mode 100644 hook-producer/src/handlers/app.rs delete mode 100644 hook-producer/src/handlers/index.rs create mode 100644 hook-producer/src/handlers/webhook.rs diff --git a/Cargo.lock b/Cargo.lock index b24af98ae2169..73fd7cb4b7f75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,6 +986,7 @@ name = "hook-common" version = "0.1.0" dependencies = [ "chrono", + "http 0.2.11", "serde", "serde_derive", "sqlx", @@ -1019,11 +1020,19 @@ dependencies = [ "axum", "envconfig", "eyre", + "hook-common", + "http-body-util", "metrics", "metrics-exporter-prometheus", + "serde", + "serde_derive", + "serde_json", + "sqlx", "tokio", + "tower", "tracing", "tracing-subscriber", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e92db695f324a..60a7219d66630 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,22 @@ [workspace] resolver = "2" -members = [ - "hook-common", - "hook-producer", - "hook-consumer", -] +members = ["hook-common", "hook-producer", "hook-consumer"] [workspace.dependencies] chrono = { version = "0.4" } serde = { version = "1.0" } serde_derive = { version = "1.0" } +serde_json = { version = "1.0" } thiserror = { version = "1.0" } -sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", "postgres", "uuid", "json", "chrono" ] } +sqlx = { version = "0.7", features = [ + "runtime-tokio", + "tls-native-tls", + "postgres", + "uuid", + "json", + "chrono", +] } tokio = { version = "1.34.0", features = ["full"] } eyre = "0.6.9" tracing = "0.1.40" @@ -20,3 +24,7 @@ tracing-subscriber = "0.3.18" envconfig = "0.10.0" metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" +http = { version = "0.2" } +url = { version = "2.5.0 " } +tower = "0.4.13" +http-body-util = "0.1.0" diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index b55a9ecd84d4d..24d1a0d663a5f 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -6,11 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = { workspace = true} +chrono = { workspace = true } +http = { workspace = true } serde = { workspace = true } -serde_derive = { workspace = true} -thiserror = { workspace = true } +serde_derive = { workspace = true } sqlx = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] tokio = { workspace = true } # We need a runtime for async tests diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs index d1dadf32ba63e..3138f087a1ebc 100644 --- a/hook-common/src/lib.rs +++ b/hook-common/src/lib.rs @@ -1 +1,2 @@ pub mod pgqueue; +pub mod webhook; diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 7ed46550bd8bd..fb2211b33b02f 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -15,12 +15,16 @@ use thiserror::Error; /// Errors that can originate from sqlx and are wrapped by us to provide additional context. #[derive(Error, Debug)] pub enum PgQueueError { + #[error("pool creation failed with: {error}")] + PoolCreationError { error: sqlx::Error }, #[error("connection failed with: {error}")] ConnectionError { error: sqlx::Error }, #[error("{command} query failed with: {error}")] QueryError { command: String, error: sqlx::Error }, #[error("{0} is not a valid JobStatus")] ParseJobStatusError(String), + #[error("{0} is not a valid HttpMethod")] + ParseHttpMethodError(String), } #[derive(Error, Debug)] @@ -528,6 +532,7 @@ impl Default for RetryPolicy { } /// A queue implemented on top of a PostgreSQL table. +#[derive(Clone)] pub struct PgQueue { /// A name to identify this PgQueue as multiple may share a table. name: String, @@ -560,9 +565,8 @@ impl PgQueue { let name = queue_name.to_owned(); let table = table_name.to_owned(); let pool = PgPoolOptions::new() - .connect(url) - .await - .map_err(|error| PgQueueError::ConnectionError { error })?; + .connect_lazy(url) + .map_err(|error| PgQueueError::PoolCreationError { error })?; Ok(Self { name, diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs new file mode 100644 index 0000000000000..b17959ce0f6da --- /dev/null +++ b/hook-common/src/webhook.rs @@ -0,0 +1,139 @@ +use std::collections; +use std::fmt; +use std::str::FromStr; + +use serde::{de::Visitor, Deserialize, Serialize}; + +use crate::pgqueue::PgQueueError; + +/// Supported HTTP methods for webhooks. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum HttpMethod { + DELETE, + GET, + PATCH, + POST, + PUT, +} + +/// Allow casting `HttpMethod` from strings. +impl FromStr for HttpMethod { + type Err = PgQueueError; + + fn from_str(s: &str) -> Result { + match s.to_ascii_uppercase().as_ref() { + "DELETE" => Ok(HttpMethod::DELETE), + "GET" => Ok(HttpMethod::GET), + "PATCH" => Ok(HttpMethod::PATCH), + "POST" => Ok(HttpMethod::POST), + "PUT" => Ok(HttpMethod::PUT), + invalid => Err(PgQueueError::ParseHttpMethodError(invalid.to_owned())), + } + } +} + +/// Implement `std::fmt::Display` to convert HttpMethod to string. +impl fmt::Display for HttpMethod { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + HttpMethod::DELETE => write!(f, "DELETE"), + HttpMethod::GET => write!(f, "GET"), + HttpMethod::PATCH => write!(f, "PATCH"), + HttpMethod::POST => write!(f, "POST"), + HttpMethod::PUT => write!(f, "PUT"), + } + } +} + +struct HttpMethodVisitor; + +impl<'de> Visitor<'de> for HttpMethodVisitor { + type Value = HttpMethod; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "the string representation of HttpMethod") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + match HttpMethod::from_str(s) { + Ok(method) => Ok(method), + Err(_) => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &self, + )), + } + } +} + +/// Deserialize required to read `HttpMethod` from database. +impl<'de> Deserialize<'de> for HttpMethod { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(HttpMethodVisitor) + } +} + +/// Serialize required to write `HttpMethod` to database. +impl Serialize for HttpMethod { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// Convenience to cast `HttpMethod` to `http::Method`. +/// Not all `http::Method` variants are valid `HttpMethod` variants, hence why we +/// can't just use the former or implement `From`. +impl From for http::Method { + fn from(val: HttpMethod) -> Self { + match val { + HttpMethod::DELETE => http::Method::DELETE, + HttpMethod::GET => http::Method::GET, + HttpMethod::PATCH => http::Method::PATCH, + HttpMethod::POST => http::Method::POST, + HttpMethod::PUT => http::Method::PUT, + } + } +} + +impl From<&HttpMethod> for http::Method { + fn from(val: &HttpMethod) -> Self { + match val { + HttpMethod::DELETE => http::Method::DELETE, + HttpMethod::GET => http::Method::GET, + HttpMethod::PATCH => http::Method::PATCH, + HttpMethod::POST => http::Method::POST, + HttpMethod::PUT => http::Method::PUT, + } + } +} + +/// `JobParameters` required for the `WebhookConsumer` to execute a webhook. +/// These parameters should match the exported Webhook interface that PostHog plugins. +/// implement. See: https://github.com/PostHog/plugin-scaffold/blob/main/src/types.ts#L15. +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct WebhookJobParameters { + pub body: String, + pub headers: collections::HashMap, + pub method: HttpMethod, + pub url: String, + + // These should be set if the Webhook is associated with a plugin `composeWebhook` invocation. + pub team_id: Option, + pub plugin_id: Option, + pub plugin_config_id: Option, + + #[serde(default = "default_max_attempts")] + pub max_attempts: i32, +} + +fn default_max_attempts() -> i32 { + 3 +} diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 8da2df690c2fd..406dd732ae797 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -1,140 +1,16 @@ use std::collections; -use std::fmt; -use std::str::FromStr; use std::sync::Arc; use std::time; use async_std::task; use hook_common::pgqueue::{PgJobError, PgQueue, PgQueueError, PgTransactionJob}; +use hook_common::webhook::{HttpMethod, WebhookJobParameters}; use http::StatusCode; use reqwest::header; -use serde::{de::Visitor, Deserialize, Serialize}; use tokio::sync; use crate::error::WebhookConsumerError; -/// Supported HTTP methods for webhooks. -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum HttpMethod { - DELETE, - GET, - PATCH, - POST, - PUT, -} - -/// Allow casting `HttpMethod` from strings. -impl FromStr for HttpMethod { - type Err = WebhookConsumerError; - - fn from_str(s: &str) -> Result { - match s.to_ascii_uppercase().as_ref() { - "DELETE" => Ok(HttpMethod::DELETE), - "GET" => Ok(HttpMethod::GET), - "PATCH" => Ok(HttpMethod::PATCH), - "POST" => Ok(HttpMethod::POST), - "PUT" => Ok(HttpMethod::PUT), - invalid => Err(WebhookConsumerError::ParseHttpMethodError( - invalid.to_owned(), - )), - } - } -} - -/// Implement `std::fmt::Display` to convert HttpMethod to string. -impl fmt::Display for HttpMethod { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - HttpMethod::DELETE => write!(f, "DELETE"), - HttpMethod::GET => write!(f, "GET"), - HttpMethod::PATCH => write!(f, "PATCH"), - HttpMethod::POST => write!(f, "POST"), - HttpMethod::PUT => write!(f, "PUT"), - } - } -} - -struct HttpMethodVisitor; - -impl<'de> Visitor<'de> for HttpMethodVisitor { - type Value = HttpMethod; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "the string representation of HttpMethod") - } - - fn visit_str(self, s: &str) -> Result - where - E: serde::de::Error, - { - match HttpMethod::from_str(s) { - Ok(method) => Ok(method), - Err(_) => Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Str(s), - &self, - )), - } - } -} - -/// Deserialize required to read `HttpMethod` from database. -impl<'de> Deserialize<'de> for HttpMethod { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - deserializer.deserialize_str(HttpMethodVisitor) - } -} - -/// Serialize required to write `HttpMethod` to database. -impl Serialize for HttpMethod { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -/// Convenience to cast `HttpMethod` to `http::Method`. -/// Not all `http::Method` variants are valid `HttpMethod` variants, hence why we -/// can't just use the former or implement `From`. -impl From for http::Method { - fn from(val: HttpMethod) -> Self { - match val { - HttpMethod::DELETE => http::Method::DELETE, - HttpMethod::GET => http::Method::GET, - HttpMethod::PATCH => http::Method::PATCH, - HttpMethod::POST => http::Method::POST, - HttpMethod::PUT => http::Method::PUT, - } - } -} - -impl From<&HttpMethod> for http::Method { - fn from(val: &HttpMethod) -> Self { - match val { - HttpMethod::DELETE => http::Method::DELETE, - HttpMethod::GET => http::Method::GET, - HttpMethod::PATCH => http::Method::PATCH, - HttpMethod::POST => http::Method::POST, - HttpMethod::PUT => http::Method::PUT, - } - } -} - -/// `JobParameters` required for the `WebhookConsumer` to execute a webhook. -/// These parameters should match the exported Webhook interface that PostHog plugins. -/// implement. See: https://github.com/PostHog/plugin-scaffold/blob/main/src/types.ts#L15. -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] -pub struct WebhookJobParameters { - body: String, - headers: collections::HashMap, - method: HttpMethod, - url: String, -} - /// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. pub struct WebhookConsumer<'p> { /// An identifier for this consumer. Used to mark jobs we have consumed. @@ -432,6 +308,12 @@ mod tests { headers: collections::HashMap::new(), method: HttpMethod::POST, url: "localhost".to_owned(), + + team_id: Some(1), + plugin_id: Some(2), + plugin_config_id: Some(3), + + max_attempts: 1, }; // enqueue takes ownership of the job enqueued to avoid bugs that can cause duplicate jobs. // Normally, a separate application would be enqueueing jobs for us to consume, so no ownership diff --git a/hook-producer/Cargo.toml b/hook-producer/Cargo.toml index 47ef532891436..ef1a24b01820a 100644 --- a/hook-producer/Cargo.toml +++ b/hook-producer/Cargo.toml @@ -6,11 +6,19 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = { version="0.7.1", features=["http2"] } -tokio = { workspace = true } -eyre = {workspace = true } -tracing = {workspace = true} -tracing-subscriber = {workspace = true} +axum = { version = "0.7.1", features = ["http2"] } envconfig = { workspace = true } +eyre = { workspace = true } +hook-common = { path = "../hook-common" } +http-body-util = { workspace = true } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true } +tokio = { workspace = true } +tower = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } diff --git a/hook-producer/src/config.rs b/hook-producer/src/config.rs index 9d093c652efbb..87fad5d07dbb1 100644 --- a/hook-producer/src/config.rs +++ b/hook-producer/src/config.rs @@ -7,6 +7,15 @@ pub struct Config { #[envconfig(from = "BIND_PORT", default = "8000")] pub port: u16, + + #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] + pub database_url: String, + + #[envconfig(default = "job_queue")] + pub table_name: String, + + #[envconfig(default = "default")] + pub queue_name: String, } impl Config { diff --git a/hook-producer/src/handlers/app.rs b/hook-producer/src/handlers/app.rs new file mode 100644 index 0000000000000..911a04d397416 --- /dev/null +++ b/hook-producer/src/handlers/app.rs @@ -0,0 +1,60 @@ +use axum::{routing, Router}; +use metrics_exporter_prometheus::PrometheusHandle; + +use hook_common::pgqueue::PgQueue; + +use super::webhook; + +pub fn app(pg_pool: PgQueue, metrics: Option) -> Router { + Router::new() + .route("/", routing::get(index)) + .route( + "/metrics", + routing::get(move || match metrics { + Some(ref recorder_handle) => std::future::ready(recorder_handle.render()), + None => std::future::ready("no metrics recorder installed".to_owned()), + }), + ) + .route("/webhook", routing::post(webhook::post).with_state(pg_pool)) + .layer(axum::middleware::from_fn(crate::metrics::track_metrics)) +} + +pub async fn index() -> &'static str { + "rusty hook" +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; + use hook_common::pgqueue::{PgQueue, RetryPolicy}; + use http_body_util::BodyExt; // for `collect` + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + + #[tokio::test] + async fn index() { + let pg_queue = PgQueue::new( + "test_index", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + RetryPolicy::default(), + ) + .await + .expect("failed to construct pg_queue"); + + let app = app(pg_queue, None); + + let response = app + .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(&body[..], b"rusty hook"); + } +} diff --git a/hook-producer/src/handlers/index.rs b/hook-producer/src/handlers/index.rs deleted file mode 100644 index 56896fa63a483..0000000000000 --- a/hook-producer/src/handlers/index.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub async fn get() -> &'static str { - "rusty hook" -} diff --git a/hook-producer/src/handlers/mod.rs b/hook-producer/src/handlers/mod.rs index 25040731d5688..88f96717c130a 100644 --- a/hook-producer/src/handlers/mod.rs +++ b/hook-producer/src/handlers/mod.rs @@ -1,15 +1,4 @@ -use axum::{routing, Router}; +mod app; +mod webhook; -mod index; - -pub fn router() -> Router { - let recorder_handle = crate::metrics::setup_metrics_recorder(); - - Router::new() - .route("/", routing::get(index::get)) - .route( - "/metrics", - routing::get(move || std::future::ready(recorder_handle.render())), - ) - .layer(axum::middleware::from_fn(crate::metrics::track_metrics)) -} +pub use app::app; diff --git a/hook-producer/src/handlers/webhook.rs b/hook-producer/src/handlers/webhook.rs new file mode 100644 index 0000000000000..7de1126fe4db8 --- /dev/null +++ b/hook-producer/src/handlers/webhook.rs @@ -0,0 +1,278 @@ +use axum::{extract::State, http::StatusCode, Json}; +use hook_common::webhook::WebhookJobParameters; +use serde_derive::Deserialize; +use url::Url; + +use hook_common::pgqueue::{NewJob, PgQueue}; +use serde::Serialize; +use tracing::{debug, error}; + +const MAX_BODY_SIZE: usize = 1_000_000; + +#[derive(Serialize, Deserialize)] +pub struct WebhookPostResponse { + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, +} + +pub async fn post( + State(pg_queue): State, + Json(payload): Json, +) -> Result, (StatusCode, Json)> { + debug!("received payload: {:?}", payload); + + if payload.body.len() > MAX_BODY_SIZE { + return Err(( + StatusCode::BAD_REQUEST, + Json(WebhookPostResponse { + error: Some("body too large".to_owned()), + }), + )); + } + + let url_hostname = get_hostname(&payload.url)?; + let job = NewJob::new(payload.max_attempts, payload, url_hostname.as_str()); + + pg_queue.enqueue(job).await.map_err(internal_error)?; + + Ok(Json(WebhookPostResponse { error: None })) +} + +fn internal_error(err: E) -> (StatusCode, Json) +where + E: std::error::Error, +{ + error!("internal error: {}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(WebhookPostResponse { + error: Some(err.to_string()), + }), + ) +} + +fn get_hostname(url_str: &str) -> Result)> { + let url = Url::parse(url_str).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(WebhookPostResponse { + error: Some("could not parse url".to_owned()), + }), + ) + })?; + + match url.host_str() { + Some(hostname) => Ok(hostname.to_owned()), + None => Err(( + StatusCode::BAD_REQUEST, + Json(WebhookPostResponse { + error: Some("couldn't extract hostname from url".to_owned()), + }), + )), + } +} + +#[cfg(test)] +mod tests { + use axum::{ + body::Body, + http::{self, Request, StatusCode}, + }; + use hook_common::pgqueue::{PgQueue, RetryPolicy}; + use hook_common::webhook::{HttpMethod, WebhookJobParameters}; + use http_body_util::BodyExt; // for `collect` + use std::collections; + use tower::ServiceExt; // for `call`, `oneshot`, and `ready` + + use crate::handlers::app; + + #[tokio::test] + async fn webhook_success() { + let pg_queue = PgQueue::new( + "test_index", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + RetryPolicy::default(), + ) + .await + .expect("failed to construct pg_queue"); + + let app = app(pg_queue, None); + + let mut headers = collections::HashMap::new(); + headers.insert("Content-Type".to_owned(), "application/json".to_owned()); + let response = app + .oneshot( + Request::builder() + .method(http::Method::POST) + .uri("/webhook") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_string(&WebhookJobParameters { + headers, + method: HttpMethod::POST, + url: "http://example.com/".to_owned(), + body: r#"{"a": "b"}"#.to_owned(), + + team_id: Some(1), + plugin_id: Some(2), + plugin_config_id: Some(3), + + max_attempts: 1, + }) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(&body[..], b"{}"); + } + + #[tokio::test] + async fn webhook_bad_url() { + let pg_queue = PgQueue::new( + "test_index", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + RetryPolicy::default(), + ) + .await + .expect("failed to construct pg_queue"); + + let app = app(pg_queue, None); + + let response = app + .oneshot( + Request::builder() + .method(http::Method::POST) + .uri("/webhook") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_string(&WebhookJobParameters { + headers: collections::HashMap::new(), + method: HttpMethod::POST, + url: "invalid".to_owned(), + body: r#"{"a": "b"}"#.to_owned(), + + team_id: Some(1), + plugin_id: Some(2), + plugin_config_id: Some(3), + + max_attempts: 1, + }) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn webhook_payload_missing_fields() { + let pg_queue = PgQueue::new( + "test_index", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + RetryPolicy::default(), + ) + .await + .expect("failed to construct pg_queue"); + + let app = app(pg_queue, None); + + let response = app + .oneshot( + Request::builder() + .method(http::Method::POST) + .uri("/webhook") + .header(http::header::CONTENT_TYPE, "application/json") + .body("{}".to_owned()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); + } + + #[tokio::test] + async fn webhook_payload_not_json() { + let pg_queue = PgQueue::new( + "test_index", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + RetryPolicy::default(), + ) + .await + .expect("failed to construct pg_queue"); + + let app = app(pg_queue, None); + + let response = app + .oneshot( + Request::builder() + .method(http::Method::POST) + .uri("/webhook") + .header(http::header::CONTENT_TYPE, "application/json") + .body("x".to_owned()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn webhook_payload_body_too_large() { + let pg_queue = PgQueue::new( + "test_index", + "job_queue", + "postgres://posthog:posthog@localhost:15432/test_database", + RetryPolicy::default(), + ) + .await + .expect("failed to construct pg_queue"); + + let app = app(pg_queue, None); + + let bytes: Vec = vec![b'a'; 1_000_000 * 2]; + let long_string = String::from_utf8_lossy(&bytes); + + let response = app + .oneshot( + Request::builder() + .method(http::Method::POST) + .uri("/webhook") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from( + serde_json::to_string(&WebhookJobParameters { + headers: collections::HashMap::new(), + method: HttpMethod::POST, + url: "http://example.com".to_owned(), + body: long_string.to_string(), + + team_id: Some(1), + plugin_id: Some(2), + plugin_config_id: Some(3), + + max_attempts: 1, + }) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } +} diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index 118829b00d78b..29da8dd6efe0e 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -1,10 +1,10 @@ use axum::Router; - use config::Config; use envconfig::Envconfig; - use eyre::Result; +use hook_common::pgqueue::{PgQueue, RetryPolicy}; + mod config; mod handlers; mod metrics; @@ -21,10 +21,24 @@ async fn listen(app: Router, bind: String) -> Result<()> { async fn main() { tracing_subscriber::fmt::init(); - let app = handlers::router(); - let config = Config::init_from_env().expect("failed to load configuration from env"); + let pg_queue = PgQueue::new( + // TODO: Coupling the queue name to the PgQueue object doesn't seem ideal from the producer + // side, but we don't need more than one queue for now. + &config.queue_name, + &config.table_name, + &config.database_url, + // TODO: It seems unnecessary that the producer side needs to know about the retry policy. + RetryPolicy::default(), + ) + .await + .expect("failed to initialize queue"); + + let recorder_handle = crate::metrics::setup_metrics_recorder(); + + let app = handlers::app(pg_queue, Some(recorder_handle)); + match listen(app, config.bind()).await { Ok(_) => {} Err(e) => tracing::error!("failed to start hook-producer http server, {}", e), From 454b96d75c144b4dd428fc7862565d5a447ec4d9 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Wed, 13 Dec 2023 13:34:45 +0000 Subject: [PATCH 112/249] feat: clean up partition limiter state on schedule (#72) * feat: clean up partition limiter state on schedule * add randomness --- capture/src/partition_limits.rs | 18 ++++++++++++++++++ capture/src/server.rs | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/capture/src/partition_limits.rs b/capture/src/partition_limits.rs index cd0148f10e1f4..7059b452ce51f 100644 --- a/capture/src/partition_limits.rs +++ b/capture/src/partition_limits.rs @@ -12,6 +12,7 @@ use std::sync::Arc; use governor::{clock, state::keyed::DefaultKeyedStateStore, Quota, RateLimiter}; use metrics::gauge; +use rand::Rng; // See: https://docs.rs/governor/latest/governor/_guide/index.html#usage-in-multiple-threads #[derive(Clone)] @@ -49,6 +50,23 @@ impl PartitionLimiter { gauge!("partition_limits_key_count", self.limiter.len() as f64); } } + + /// Clean up the rate limiter state, once per minute. Ensure we don't use more memory than + /// necessary. + pub async fn clean_state(&self) { + // Give a small amount of randomness to the interval to ensure we don't have all replicas + // locking at the same time. The lock isn't going to be held for long, but this will reduce + // impact regardless. + let interval_secs = rand::thread_rng().gen_range(60..70); + + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(interval_secs)); + loop { + interval.tick().await; + + self.limiter.retain_recent(); + self.limiter.shrink_to_fit(); + } + } } #[cfg(test)] diff --git a/capture/src/server.rs b/capture/src/server.rs index 3eca676f3763f..9b8d60cbc23c4 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -55,6 +55,17 @@ where partition.report_metrics().await; }); } + + { + // Ensure that the rate limiter state does not grow unbounded + + let partition = partition.clone(); + + tokio::spawn(async move { + partition.clean_state().await; + }); + } + let sink = sink::KafkaSink::new(config.kafka, sink_liveness, partition) .expect("failed to start Kafka sink"); From 1a7e87aa267aa36d6988a186a8cb4f7851582e15 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 13 Dec 2023 15:33:45 +0100 Subject: [PATCH 113/249] carve out the limiters and sinks sub-modules (#70) --- capture-server/tests/common.rs | 4 +- capture-server/tests/events.rs | 12 ++--- capture/src/capture.rs | 6 +-- capture/src/config.rs | 4 +- capture/src/lib.rs | 5 +- .../billing.rs} | 2 +- capture/src/limiters/mod.rs | 2 + .../overflow.rs} | 29 ++++++----- capture/src/router.rs | 6 +-- capture/src/server.rs | 22 ++++---- capture/src/{sink.rs => sinks/kafka.rs} | 51 ++++--------------- capture/src/sinks/mod.rs | 13 +++++ capture/src/sinks/print.rs | 31 +++++++++++ capture/tests/django_compat.rs | 6 +-- 14 files changed, 104 insertions(+), 89 deletions(-) rename capture/src/{billing_limits.rs => limiters/billing.rs} (99%) create mode 100644 capture/src/limiters/mod.rs rename capture/src/{partition_limits.rs => limiters/overflow.rs} (80%) rename capture/src/{sink.rs => sinks/kafka.rs} (91%) create mode 100644 capture/src/sinks/mod.rs create mode 100644 capture/src/sinks/print.rs diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index ce31897583f16..214ecc80a62b5 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -28,8 +28,8 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { print_sink: false, address: SocketAddr::from_str("127.0.0.1:0").unwrap(), redis_url: "redis://localhost:6379/".to_string(), - burst_limit: NonZeroU32::new(5).unwrap(), - per_second_limit: NonZeroU32::new(10).unwrap(), + overflow_burst_limit: NonZeroU32::new(5).unwrap(), + overflow_per_second_limit: NonZeroU32::new(10).unwrap(), overflow_forced_keys: None, kafka: KafkaConfig { kafka_producer_linger_ms: 0, // Send messages as soon as possible diff --git a/capture-server/tests/events.rs b/capture-server/tests/events.rs index b38ac5a1a63fb..56fcdf79bf15d 100644 --- a/capture-server/tests/events.rs +++ b/capture-server/tests/events.rs @@ -77,7 +77,7 @@ async fn it_captures_a_batch() -> Result<()> { } #[tokio::test] -async fn it_is_limited_with_burst() -> Result<()> { +async fn it_overflows_events_on_burst() -> Result<()> { setup_tracing(); let token = random_string("token", 16); @@ -87,8 +87,8 @@ async fn it_is_limited_with_burst() -> Result<()> { let mut config = DEFAULT_CONFIG.clone(); config.kafka.kafka_topic = topic.topic_name().to_string(); - config.burst_limit = NonZeroU32::new(2).unwrap(); - config.per_second_limit = NonZeroU32::new(1).unwrap(); + config.overflow_burst_limit = NonZeroU32::new(2).unwrap(); + config.overflow_per_second_limit = NonZeroU32::new(1).unwrap(); let server = ServerHandle::for_config(config); @@ -125,7 +125,7 @@ async fn it_is_limited_with_burst() -> Result<()> { } #[tokio::test] -async fn it_does_not_partition_limit_different_ids() -> Result<()> { +async fn it_does_not_overflow_team_with_different_ids() -> Result<()> { setup_tracing(); let token = random_string("token", 16); @@ -136,8 +136,8 @@ async fn it_does_not_partition_limit_different_ids() -> Result<()> { let mut config = DEFAULT_CONFIG.clone(); config.kafka.kafka_topic = topic.topic_name().to_string(); - config.burst_limit = NonZeroU32::new(1).unwrap(); - config.per_second_limit = NonZeroU32::new(1).unwrap(); + config.overflow_burst_limit = NonZeroU32::new(1).unwrap(); + config.overflow_per_second_limit = NonZeroU32::new(1).unwrap(); let server = ServerHandle::for_config(config); diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 37e2872a9f9b4..6a903787a2c32 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -15,14 +15,14 @@ use metrics::counter; use time::OffsetDateTime; use tracing::instrument; -use crate::billing_limits::QuotaResource; use crate::event::{Compression, ProcessingContext}; +use crate::limiters::billing::QuotaResource; use crate::prometheus::report_dropped_events; use crate::token::validate_token; use crate::{ api::{CaptureError, CaptureResponse, CaptureResponseCode}, event::{EventFormData, EventQuery, ProcessedEvent, RawEvent}, - router, sink, + router, sinks, utils::uuid_v7, }; @@ -209,7 +209,7 @@ pub fn extract_and_verify_token(events: &[RawEvent]) -> Result( - sink: Arc, + sink: Arc, events: &'a [RawEvent], context: &'a ProcessingContext, ) -> Result<(), CaptureError> { diff --git a/capture/src/config.rs b/capture/src/config.rs index 69a085dd927b0..0c6ab1ce9eb62 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -14,10 +14,10 @@ pub struct Config { pub otel_url: Option, #[envconfig(default = "100")] - pub per_second_limit: NonZeroU32, + pub overflow_per_second_limit: NonZeroU32, #[envconfig(default = "1000")] - pub burst_limit: NonZeroU32, + pub overflow_burst_limit: NonZeroU32, pub overflow_forced_keys: Option, // Coma-delimited keys diff --git a/capture/src/lib.rs b/capture/src/lib.rs index eea915c307f71..058e994186edb 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -1,15 +1,14 @@ pub mod api; -pub mod billing_limits; pub mod capture; pub mod config; pub mod event; pub mod health; -pub mod partition_limits; +pub mod limiters; pub mod prometheus; pub mod redis; pub mod router; pub mod server; -pub mod sink; +pub mod sinks; pub mod time; pub mod token; pub mod utils; diff --git a/capture/src/billing_limits.rs b/capture/src/limiters/billing.rs similarity index 99% rename from capture/src/billing_limits.rs rename to capture/src/limiters/billing.rs index 9fa0fdd0e953e..b908519dda265 100644 --- a/capture/src/billing_limits.rs +++ b/capture/src/limiters/billing.rs @@ -166,7 +166,7 @@ mod tests { use time::Duration; use crate::{ - billing_limits::{BillingLimiter, QuotaResource}, + limiters::billing::{BillingLimiter, QuotaResource}, redis::MockRedisClient, }; diff --git a/capture/src/limiters/mod.rs b/capture/src/limiters/mod.rs new file mode 100644 index 0000000000000..58b2dcc1a5c8c --- /dev/null +++ b/capture/src/limiters/mod.rs @@ -0,0 +1,2 @@ +pub mod billing; +pub mod overflow; diff --git a/capture/src/partition_limits.rs b/capture/src/limiters/overflow.rs similarity index 80% rename from capture/src/partition_limits.rs rename to capture/src/limiters/overflow.rs index 7059b452ce51f..0e91a99d213b7 100644 --- a/capture/src/partition_limits.rs +++ b/capture/src/limiters/overflow.rs @@ -1,11 +1,12 @@ -/// When a customer is writing too often to the same key, we get hot partitions. This negatively -/// affects our write latency and cluster health. We try to provide ordering guarantees wherever -/// possible, but this does require that we map key -> partition. +/// The analytics ingestion pipeline provides ordering guarantees for events of the same +/// token and distinct_id. We currently achieve this through a locality constraint on the +/// Kafka partition (consistent partition hashing through a computed key). /// -/// If the write-rate reaches a certain amount, we need to be able to handle the hot partition -/// before it causes a negative impact. In this case, instead of passing the error to the customer -/// with a 429, we relax our ordering constraints and temporarily override the key, meaning the -/// customers data will be spread across all partitions. +/// Volume spikes to a given key can create lag on the destination partition and induce +/// ingestion lag. To protect the downstream systems, capture can relax this locality +/// constraint when bursts are detected. When that happens, the excess traffic will be +/// spread across all partitions and be processed by the overflow consumer, without +/// strict ordering guarantees. use std::collections::HashSet; use std::num::NonZeroU32; use std::sync::Arc; @@ -16,12 +17,12 @@ use rand::Rng; // See: https://docs.rs/governor/latest/governor/_guide/index.html#usage-in-multiple-threads #[derive(Clone)] -pub struct PartitionLimiter { +pub struct OverflowLimiter { limiter: Arc, clock::DefaultClock>>, forced_keys: HashSet, } -impl PartitionLimiter { +impl OverflowLimiter { pub fn new(per_second: NonZeroU32, burst: NonZeroU32, forced_keys: Option) -> Self { let quota = Quota::per_second(per_second).allow_burst(burst); let limiter = Arc::new(governor::RateLimiter::dashmap(quota)); @@ -31,7 +32,7 @@ impl PartitionLimiter { Some(values) => values.split(',').map(String::from).collect(), }; - PartitionLimiter { + OverflowLimiter { limiter, forced_keys, } @@ -71,12 +72,12 @@ impl PartitionLimiter { #[cfg(test)] mod tests { - use crate::partition_limits::PartitionLimiter; + use crate::limiters::overflow::OverflowLimiter; use std::num::NonZeroU32; #[tokio::test] async fn low_limits() { - let limiter = PartitionLimiter::new( + let limiter = OverflowLimiter::new( NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap(), None, @@ -89,7 +90,7 @@ mod tests { #[tokio::test] async fn bursting() { - let limiter = PartitionLimiter::new( + let limiter = OverflowLimiter::new( NonZeroU32::new(1).unwrap(), NonZeroU32::new(3).unwrap(), None, @@ -109,7 +110,7 @@ mod tests { let key_three = String::from("three"); let forced_keys = Some(String::from("one,three")); - let limiter = PartitionLimiter::new( + let limiter = OverflowLimiter::new( NonZeroU32::new(1).unwrap(), NonZeroU32::new(1).unwrap(), forced_keys, diff --git a/capture/src/router.rs b/capture/src/router.rs index 6f2f044f88c67..d02e63faaad5d 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -10,13 +10,13 @@ use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; use tower_http::trace::TraceLayer; use crate::health::HealthRegistry; -use crate::{billing_limits::BillingLimiter, capture, redis::Client, sink, time::TimeSource}; +use crate::{capture, limiters::billing::BillingLimiter, redis::Client, sinks, time::TimeSource}; use crate::prometheus::{setup_metrics_recorder, track_metrics}; #[derive(Clone)] pub struct State { - pub sink: Arc, + pub sink: Arc, pub timesource: Arc, pub redis: Arc, pub billing: BillingLimiter, @@ -28,7 +28,7 @@ async fn index() -> &'static str { pub fn router< TZ: TimeSource + Send + Sync + 'static, - S: sink::EventSink + Send + Sync + 'static, + S: sinks::Event + Send + Sync + 'static, R: Client + Send + Sync + 'static, >( timesource: TZ, diff --git a/capture/src/server.rs b/capture/src/server.rs index 9b8d60cbc23c4..22a1f3bc0bf04 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -4,12 +4,14 @@ use std::sync::Arc; use time::Duration; -use crate::billing_limits::BillingLimiter; use crate::config::Config; use crate::health::{ComponentStatus, HealthRegistry}; -use crate::partition_limits::PartitionLimiter; +use crate::limiters::billing::BillingLimiter; +use crate::limiters::overflow::OverflowLimiter; use crate::redis::RedisClient; -use crate::{router, sink}; +use crate::router; +use crate::sinks::kafka::KafkaSink; +use crate::sinks::print::PrintSink; pub async fn serve(config: Config, listener: TcpListener, shutdown: F) where @@ -34,7 +36,7 @@ where router::router( crate::time::SystemTime {}, liveness, - sink::PrintSink {}, + PrintSink {}, redis_client, billing, config.export_prometheus, @@ -44,9 +46,9 @@ where .register("rdkafka".to_string(), Duration::seconds(30)) .await; - let partition = PartitionLimiter::new( - config.per_second_limit, - config.burst_limit, + let partition = OverflowLimiter::new( + config.overflow_per_second_limit, + config.overflow_burst_limit, config.overflow_forced_keys, ); if config.export_prometheus { @@ -55,18 +57,14 @@ where partition.report_metrics().await; }); } - { // Ensure that the rate limiter state does not grow unbounded - let partition = partition.clone(); - tokio::spawn(async move { partition.clean_state().await; }); } - - let sink = sink::KafkaSink::new(config.kafka, sink_liveness, partition) + let sink = KafkaSink::new(config.kafka, sink_liveness, partition) .expect("failed to start Kafka sink"); router::router( diff --git a/capture/src/sink.rs b/capture/src/sinks/kafka.rs similarity index 91% rename from capture/src/sink.rs rename to capture/src/sinks/kafka.rs index af83e20c1a763..dc57c11e458be 100644 --- a/capture/src/sink.rs +++ b/capture/src/sinks/kafka.rs @@ -2,11 +2,10 @@ use std::time::Duration; use async_trait::async_trait; use metrics::{absolute_counter, counter, gauge, histogram}; -use rdkafka::config::ClientConfig; use rdkafka::error::{KafkaError, RDKafkaErrorCode}; -use rdkafka::producer::future_producer::{FutureProducer, FutureRecord}; -use rdkafka::producer::{DeliveryFuture, Producer}; +use rdkafka::producer::{DeliveryFuture, FutureProducer, FutureRecord, Producer}; use rdkafka::util::Timeout; +use rdkafka::ClientConfig; use tokio::task::JoinSet; use tracing::log::{debug, error, info}; use tracing::{info_span, instrument, Instrument}; @@ -15,38 +14,9 @@ use crate::api::CaptureError; use crate::config::KafkaConfig; use crate::event::ProcessedEvent; use crate::health::HealthHandle; -use crate::partition_limits::PartitionLimiter; +use crate::limiters::overflow::OverflowLimiter; use crate::prometheus::report_dropped_events; - -#[async_trait] -pub trait EventSink { - async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError>; - async fn send_batch(&self, events: Vec) -> Result<(), CaptureError>; -} - -pub struct PrintSink {} - -#[async_trait] -impl EventSink for PrintSink { - async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { - info!("single event: {:?}", event); - counter!("capture_events_ingested_total", 1); - - Ok(()) - } - async fn send_batch(&self, events: Vec) -> Result<(), CaptureError> { - let span = tracing::span!(tracing::Level::INFO, "batch of events"); - let _enter = span.enter(); - - histogram!("capture_event_batch_size", events.len() as f64); - counter!("capture_events_ingested_total", events.len() as u64); - for event in events { - info!("event: {:?}", event); - } - - Ok(()) - } -} +use crate::sinks::Event; struct KafkaContext { liveness: HealthHandle, @@ -113,14 +83,14 @@ impl rdkafka::ClientContext for KafkaContext { pub struct KafkaSink { producer: FutureProducer, topic: String, - partition: PartitionLimiter, + partition: OverflowLimiter, } impl KafkaSink { pub fn new( config: KafkaConfig, liveness: HealthHandle, - partition: PartitionLimiter, + partition: OverflowLimiter, ) -> anyhow::Result { info!("connecting to Kafka brokers at {}...", config.kafka_hosts); @@ -234,7 +204,7 @@ impl KafkaSink { } #[async_trait] -impl EventSink for KafkaSink { +impl Event for KafkaSink { #[instrument(skip_all)] async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { let limited = self.partition.is_limited(&event.key()); @@ -294,8 +264,9 @@ mod tests { use crate::config; use crate::event::ProcessedEvent; use crate::health::HealthRegistry; - use crate::partition_limits::PartitionLimiter; - use crate::sink::{EventSink, KafkaSink}; + use crate::limiters::overflow::OverflowLimiter; + use crate::sinks::kafka::KafkaSink; + use crate::sinks::Event; use crate::utils::uuid_v7; use rand::distributions::Alphanumeric; use rand::Rng; @@ -310,7 +281,7 @@ mod tests { let handle = registry .register("one".to_string(), Duration::seconds(30)) .await; - let limiter = PartitionLimiter::new( + let limiter = OverflowLimiter::new( NonZeroU32::new(10).unwrap(), NonZeroU32::new(10).unwrap(), None, diff --git a/capture/src/sinks/mod.rs b/capture/src/sinks/mod.rs new file mode 100644 index 0000000000000..0747f0e222a79 --- /dev/null +++ b/capture/src/sinks/mod.rs @@ -0,0 +1,13 @@ +use async_trait::async_trait; + +use crate::api::CaptureError; +use crate::event::ProcessedEvent; + +pub mod kafka; +pub mod print; + +#[async_trait] +pub trait Event { + async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError>; + async fn send_batch(&self, events: Vec) -> Result<(), CaptureError>; +} diff --git a/capture/src/sinks/print.rs b/capture/src/sinks/print.rs new file mode 100644 index 0000000000000..50bc1ade690ef --- /dev/null +++ b/capture/src/sinks/print.rs @@ -0,0 +1,31 @@ +use async_trait::async_trait; +use metrics::{counter, histogram}; +use tracing::log::info; + +use crate::api::CaptureError; +use crate::event::ProcessedEvent; +use crate::sinks::Event; + +pub struct PrintSink {} + +#[async_trait] +impl Event for PrintSink { + async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { + info!("single event: {:?}", event); + counter!("capture_events_ingested_total", 1); + + Ok(()) + } + async fn send_batch(&self, events: Vec) -> Result<(), CaptureError> { + let span = tracing::span!(tracing::Level::INFO, "batch of events"); + let _enter = span.enter(); + + histogram!("capture_event_batch_size", events.len() as f64); + counter!("capture_events_ingested_total", events.len() as u64); + for event in events { + info!("event: {:?}", event); + } + + Ok(()) + } +} diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index d1d075bdab97c..5d778997a89a9 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -5,12 +5,12 @@ use axum_test_helper::TestClient; use base64::engine::general_purpose; use base64::Engine; use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode}; -use capture::billing_limits::BillingLimiter; use capture::event::ProcessedEvent; use capture::health::HealthRegistry; +use capture::limiters::billing::BillingLimiter; use capture::redis::MockRedisClient; use capture::router::router; -use capture::sink::EventSink; +use capture::sinks::Event; use capture::time::TimeSource; use serde::Deserialize; use serde_json::{json, Value}; @@ -61,7 +61,7 @@ impl MemorySink { } #[async_trait] -impl EventSink for MemorySink { +impl Event for MemorySink { async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { self.events.lock().unwrap().push(event); Ok(()) From 56dfe44303ab694e7a649b1a4bccc6ae04962642 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 13 Dec 2023 15:51:35 +0100 Subject: [PATCH 114/249] return rdkafka to 0.36 (#73) --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- capture-server/tests/common.rs | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e3b8737e2daa..6bf02dfc43650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1531,9 +1531,9 @@ dependencies = [ [[package]] name = "rdkafka" -version = "0.34.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053adfa02fab06e86c01d586cc68aa47ee0ff4489a59469081dc12cbcde578bf" +checksum = "d54f02a5a40220f8a2dfa47ddb38ba9064475a5807a69504b6f91711df2eea63" dependencies = [ "futures-channel", "futures-util", @@ -1549,9 +1549,9 @@ dependencies = [ [[package]] name = "rdkafka-sys" -version = "4.6.0+2.2.0" +version = "4.7.0+2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad63c279fca41a27c231c450a2d2ad18288032e9cbb159ad16c9d96eba35aaaf" +checksum = "55e0d2f9ba6253f6ec72385e453294f8618e9e15c2c6aba2a5c01ccf9622d615" dependencies = [ "cmake", "libc", diff --git a/Cargo.toml b/Cargo.toml index ecd9b6a0ad78b..faf9d2a0088be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ uuid = { version = "1.3.3", features = ["serde"] } async-trait = "0.1.68" serde_urlencoded = "0.7.1" rand = "0.8.5" -rdkafka = { version = "0.34.0", features = ["cmake-build", "ssl"] } +rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl"] } metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" thiserror = "1.0.48" diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index 214ecc80a62b5..fa8688156e650 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -177,7 +177,6 @@ impl EphemeralTopic { impl Drop for EphemeralTopic { fn drop(&mut self) { debug!("dropping EphemeralTopic {}...", self.topic_name); - _ = self.consumer.unassign(); self.consumer.unsubscribe(); match futures::executor::block_on(timeout( Duration::from_secs(10), From 74c52079098463680207a05ed43e8650d0779cd5 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 12 Dec 2023 11:44:21 -0700 Subject: [PATCH 115/249] Add indexes, drop redundant column --- migrations/20231129172339_job_queue_table.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/migrations/20231129172339_job_queue_table.sql b/migrations/20231129172339_job_queue_table.sql index 4631f0b0a5d3f..8627556c5542f 100644 --- a/migrations/20231129172339_job_queue_table.sql +++ b/migrations/20231129172339_job_queue_table.sql @@ -10,7 +10,6 @@ CREATE TABLE job_queue( attempt INT NOT NULL DEFAULT 0, attempted_at TIMESTAMPTZ DEFAULT NULL, attempted_by TEXT[] DEFAULT ARRAY[]::TEXT[], - completed_at TIMESTAMPTZ DEFAULT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), errors jsonb[], max_attempts INT NOT NULL DEFAULT 1, @@ -21,3 +20,9 @@ CREATE TABLE job_queue( status job_status NOT NULL DEFAULT 'available'::job_status, target TEXT NOT NULL ); + +-- Needed for `dequeue` queries +CREATE INDEX idx_queue_scheduled_at ON job_queue(queue, status, scheduled_at); + +-- Needed for UPDATE-ing incomplete jobs with a specific target (i.e. slow destinations) +CREATE INDEX idx_queue_target ON job_queue(queue, status, target); \ No newline at end of file From c00bc04db16994971934a11686cc43521999b6e5 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 14 Dec 2023 17:24:43 -0700 Subject: [PATCH 116/249] Append final errors to error array and treat request errors as retryable --- hook-common/src/pgqueue.rs | 4 ++++ hook-consumer/src/consumer.rs | 9 +++++++-- hook-consumer/src/error.rs | 2 -- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index fb2211b33b02f..47938ecd83760 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -248,6 +248,7 @@ UPDATE SET finished_at = NOW(), status = 'failed'::job_status + errors = array_append("{0}".errors, $3) WHERE "{0}".id = $2 AND queue = $1 @@ -261,6 +262,7 @@ RETURNING sqlx::query(&base_query) .bind(&failed_job.queue) .bind(failed_job.id) + .bind(&failed_job.error) .execute(&mut *self.connection) .await .map_err(|error| PgJobError::QueryError { @@ -394,6 +396,7 @@ UPDATE SET finished_at = NOW(), status = 'failed'::job_status + errors = array_append("{0}".errors, $3) WHERE "{0}".id = $2 AND queue = $1 @@ -406,6 +409,7 @@ RETURNING sqlx::query(&base_query) .bind(&failed_job.queue) .bind(failed_job.id) + .bind(&failed_job.error) .execute(&mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 406dd732ae797..dd8ab4297900b 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -42,7 +42,8 @@ impl<'p> WebhookConsumer<'p> { let client = reqwest::Client::builder() .default_headers(headers) .timeout(request_timeout) - .build()?; + .build() + .expect("failed to construct reqwest client for webhook consumer"); Ok(Self { name: name.to_owned(), @@ -174,7 +175,11 @@ async fn send_webhook( .headers(headers) .body(body) .send() - .await?; + .await + .map_err(|e| WebhookConsumerError::RetryableWebhookError { + reason: e.to_string(), + retry_after: None, + })?; let status = response.status(); diff --git a/hook-consumer/src/error.rs b/hook-consumer/src/error.rs index 34f0619ada4f0..a19664359caf7 100644 --- a/hook-consumer/src/error.rs +++ b/hook-consumer/src/error.rs @@ -18,8 +18,6 @@ pub enum WebhookConsumerError { QueueError(#[from] pgqueue::PgQueueError), #[error("an error occurred in the underlying job")] PgJobError(String), - #[error("an error occurred when attempting to send a request")] - RequestError(#[from] reqwest::Error), #[error("a webhook could not be delivered but it could be retried later: {reason}")] RetryableWebhookError { reason: String, From 9084c6e5c519013c2a4ee65f8cd5dd2b2fc0d844 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 15 Dec 2023 05:35:26 -0700 Subject: [PATCH 117/249] Remove Result from WebhookConsumer::new --- hook-consumer/src/consumer.rs | 10 +++++----- hook-consumer/src/main.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index dd8ab4297900b..59b8e9f24d949 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -32,7 +32,7 @@ impl<'p> WebhookConsumer<'p> { poll_interval: time::Duration, request_timeout: time::Duration, max_concurrent_jobs: usize, - ) -> Result { + ) -> Self { let mut headers = header::HeaderMap::new(); headers.insert( header::CONTENT_TYPE, @@ -45,13 +45,13 @@ impl<'p> WebhookConsumer<'p> { .build() .expect("failed to construct reqwest client for webhook consumer"); - Ok(Self { + Self { name: name.to_owned(), queue, poll_interval, client, max_concurrent_jobs, - }) + } } /// Wait until a job becomes available in our queue. @@ -333,8 +333,8 @@ mod tests { time::Duration::from_millis(100), time::Duration::from_millis(5000), 10, - ) - .expect("consumer failed to initialize"); + ); + let consumed_job = consumer .wait_for_job() .await diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index f165b32409de1..bf76503041d3a 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -29,7 +29,7 @@ async fn main() -> Result<(), WebhookConsumerError> { config.poll_interval.0, config.request_timeout.0, config.max_concurrent_jobs, - )?; + ); let _ = consumer.run().await; From 71bf5531cefa8e71a64dad3e4e887072ea9cd83b Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 13 Dec 2023 12:03:43 -0700 Subject: [PATCH 118/249] Add hook-janitor skeleton --- Cargo.lock | 139 ++++++++++++++++++ Cargo.toml | 23 +-- hook-common/Cargo.toml | 3 + hook-common/src/lib.rs | 1 + {hook-producer => hook-common}/src/metrics.rs | 0 hook-janitor/Cargo.toml | 28 ++++ hook-janitor/src/cleanup.rs | 36 +++++ hook-janitor/src/config.rs | 66 +++++++++ hook-janitor/src/handlers/app.rs | 21 +++ hook-janitor/src/handlers/mod.rs | 3 + hook-janitor/src/kafka_producer.rs | 48 ++++++ hook-janitor/src/main.rs | 86 +++++++++++ hook-janitor/src/webhooks.rs | 54 +++++++ hook-producer/Cargo.toml | 2 +- hook-producer/src/handlers/app.rs | 7 +- hook-producer/src/main.rs | 4 +- 16 files changed, 506 insertions(+), 15 deletions(-) rename {hook-producer => hook-common}/src/metrics.rs (100%) create mode 100644 hook-janitor/Cargo.toml create mode 100644 hook-janitor/src/cleanup.rs create mode 100644 hook-janitor/src/config.rs create mode 100644 hook-janitor/src/handlers/app.rs create mode 100644 hook-janitor/src/handlers/mod.rs create mode 100644 hook-janitor/src/kafka_producer.rs create mode 100644 hook-janitor/src/main.rs create mode 100644 hook-janitor/src/webhooks.rs diff --git a/Cargo.lock b/Cargo.lock index 73fd7cb4b7f75..c86f175f1c487 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -985,8 +994,11 @@ dependencies = [ name = "hook-common" version = "0.1.0" dependencies = [ + "axum", "chrono", "http 0.2.11", + "metrics", + "metrics-exporter-prometheus", "serde", "serde_derive", "sqlx", @@ -1013,6 +1025,32 @@ dependencies = [ "url", ] +[[package]] +name = "hook-janitor" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "envconfig", + "eyre", + "futures", + "hook-common", + "http-body-util", + "metrics", + "metrics-exporter-prometheus", + "rdkafka", + "serde", + "serde_derive", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tower", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "hook-producer" version = "0.1.0" @@ -1329,6 +1367,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1594,6 +1644,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "object" version = "0.32.1" @@ -1821,6 +1892,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.70" @@ -1894,6 +1975,38 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "rdkafka" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16c17f411935214a5870e40aff9291f8b40a73e97bf8de29e5959c473d5ef33" +dependencies = [ + "futures-channel", + "futures-util", + "libc", + "log", + "rdkafka-sys", + "serde", + "serde_derive", + "serde_json", + "slab", + "tokio", +] + +[[package]] +name = "rdkafka-sys" +version = "4.7.0+2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e0d2f9ba6253f6ec72385e453294f8618e9e15c2c6aba2a5c01ccf9622d615" +dependencies = [ + "cmake", + "libc", + "libz-sys", + "num_enum", + "openssl-sys", + "pkg-config", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2619,6 +2732,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -3063,6 +3193,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 60a7219d66630..535c2429412c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,24 @@ [workspace] resolver = "2" -members = ["hook-common", "hook-producer", "hook-consumer"] +members = ["hook-common", "hook-producer", "hook-consumer", "hook-janitor"] [workspace.dependencies] +async-trait = "0.1.74" +axum = { version = "0.7.1", features = ["http2"] } chrono = { version = "0.4" } +envconfig = "0.10.0" +eyre = "0.6.9" +futures = { version = "0.3.29" } +http = { version = "0.2" } +http-body-util = "0.1.0" +metrics = "0.21.1" +metrics-exporter-prometheus = "0.12.1" +rdkafka = { version = "0.35.0", features = ["cmake-build", "ssl"] } +regex = "1.10.2" serde = { version = "1.0" } serde_derive = { version = "1.0" } serde_json = { version = "1.0" } -thiserror = { version = "1.0" } sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-native-tls", @@ -17,14 +27,9 @@ sqlx = { version = "0.7", features = [ "json", "chrono", ] } +thiserror = { version = "1.0" } tokio = { version = "1.34.0", features = ["full"] } -eyre = "0.6.9" +tower = "0.4.13" tracing = "0.1.40" tracing-subscriber = "0.3.18" -envconfig = "0.10.0" -metrics = "0.21.1" -metrics-exporter-prometheus = "0.12.1" -http = { version = "0.2" } url = { version = "2.5.0 " } -tower = "0.4.13" -http-body-util = "0.1.0" diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 24d1a0d663a5f..213d2e98f7634 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -6,8 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum = { workspace = true, features = ["http2"] } chrono = { workspace = true } http = { workspace = true } +metrics = { workspace = true } +metrics-exporter-prometheus = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } sqlx = { workspace = true } diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs index 3138f087a1ebc..3b154c834224a 100644 --- a/hook-common/src/lib.rs +++ b/hook-common/src/lib.rs @@ -1,2 +1,3 @@ +pub mod metrics; pub mod pgqueue; pub mod webhook; diff --git a/hook-producer/src/metrics.rs b/hook-common/src/metrics.rs similarity index 100% rename from hook-producer/src/metrics.rs rename to hook-common/src/metrics.rs diff --git a/hook-janitor/Cargo.toml b/hook-janitor/Cargo.toml new file mode 100644 index 0000000000000..f23626bea05a6 --- /dev/null +++ b/hook-janitor/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "hook-janitor" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = { workspace = true } +axum = { workspace = true } +envconfig = { workspace = true } +eyre = { workspace = true } +futures = { workspace = true } +hook-common = { path = "../hook-common" } +http-body-util = { workspace = true } +metrics = { workspace = true } +metrics-exporter-prometheus = { workspace = true } +rdkafka = { workspace = true } +serde = { workspace = true } +serde_derive = { workspace = true } +serde_json = { workspace = true } +sqlx = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tower = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +url = { workspace = true } diff --git a/hook-janitor/src/cleanup.rs b/hook-janitor/src/cleanup.rs new file mode 100644 index 0000000000000..e6e91e0be922f --- /dev/null +++ b/hook-janitor/src/cleanup.rs @@ -0,0 +1,36 @@ +use async_trait::async_trait; +use std::result::Result; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CleanerError { + #[error("pool creation failed with: {error}")] + PoolCreationError { error: sqlx::Error }, + #[error("invalid cleaner mode")] + InvalidCleanerMode, +} + +// Mode names, used by config/environment parsing to verify the mode is supported. +#[derive(Debug)] +pub enum CleanerModeName { + Webhooks, +} + +impl FromStr for CleanerModeName { + type Err = CleanerError; + + fn from_str(s: &str) -> Result { + match s { + "webhooks" => Ok(CleanerModeName::Webhooks), + _ => Err(CleanerError::InvalidCleanerMode), + } + } +} + +// Right now, all this trait does is allow us to call `cleanup` in a loop in `main.rs`. There may +// be other benefits as we build this out, or we could remove it if it doesn't end up being useful. +#[async_trait] +pub trait Cleaner { + async fn cleanup(&self); +} diff --git a/hook-janitor/src/config.rs b/hook-janitor/src/config.rs new file mode 100644 index 0000000000000..89621a2354617 --- /dev/null +++ b/hook-janitor/src/config.rs @@ -0,0 +1,66 @@ +use envconfig::Envconfig; + +#[derive(Envconfig)] +pub struct Config { + #[envconfig(from = "BIND_HOST", default = "0.0.0.0")] + pub host: String, + + #[envconfig(from = "BIND_PORT", default = "8000")] + pub port: u16, + + #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] + pub database_url: String, + + #[envconfig(default = "job_queue")] + pub table_name: String, + + #[envconfig(default = "default")] + pub queue_name: String, + + #[envconfig(default = "30")] + pub cleanup_interval_secs: u64, + + #[envconfig(default = "10000")] + pub cleanup_batch_size: u32, + + // The cleanup task needs to have special knowledge of the queue it's cleaning up. This is so it + // can do things like flush the proper app_metrics or plugin_log_entries, and so it knows what + // to expect in the job's payload JSONB column. + #[envconfig(default = "webhooks")] + pub mode: String, + + #[envconfig(nested = true)] + pub kafka: KafkaConfig, +} + +#[derive(Envconfig, Clone)] +pub struct KafkaConfig { + #[envconfig(default = "20")] + pub kafka_producer_linger_ms: u32, // Maximum time between producer batches during low traffic + + #[envconfig(default = "400")] + pub kafka_producer_queue_mib: u32, // Size of the in-memory producer queue in mebibytes + + #[envconfig(default = "20000")] + pub kafka_message_timeout_ms: u32, // Time before we stop retrying producing a message: 20 seconds + + #[envconfig(default = "none")] + pub kafka_compression_codec: String, // none, gzip, snappy, lz4, zstd + + #[envconfig(default = "false")] + pub kafka_tls: bool, + + #[envconfig(default = "app_metrics")] + pub app_metrics_topic: String, + + #[envconfig(default = "plugin_log_entries")] + pub plugin_log_entries_topic: String, + + pub kafka_hosts: String, +} + +impl Config { + pub fn bind(&self) -> String { + format!("{}:{}", self.host, self.port) + } +} diff --git a/hook-janitor/src/handlers/app.rs b/hook-janitor/src/handlers/app.rs new file mode 100644 index 0000000000000..279fa0e35923e --- /dev/null +++ b/hook-janitor/src/handlers/app.rs @@ -0,0 +1,21 @@ +use axum::{routing, Router}; +use metrics_exporter_prometheus::PrometheusHandle; + +use hook_common::metrics; + +pub fn app(metrics: Option) -> Router { + Router::new() + .route("/", routing::get(index)) + .route( + "/metrics", + routing::get(move || match metrics { + Some(ref recorder_handle) => std::future::ready(recorder_handle.render()), + None => std::future::ready("no metrics recorder installed".to_owned()), + }), + ) + .layer(axum::middleware::from_fn(metrics::track_metrics)) +} + +pub async fn index() -> &'static str { + "rusty-hook janitor" +} diff --git a/hook-janitor/src/handlers/mod.rs b/hook-janitor/src/handlers/mod.rs new file mode 100644 index 0000000000000..a884c04897bf9 --- /dev/null +++ b/hook-janitor/src/handlers/mod.rs @@ -0,0 +1,3 @@ +mod app; + +pub use app::app; diff --git a/hook-janitor/src/kafka_producer.rs b/hook-janitor/src/kafka_producer.rs new file mode 100644 index 0000000000000..4e905b3e9bae2 --- /dev/null +++ b/hook-janitor/src/kafka_producer.rs @@ -0,0 +1,48 @@ +use crate::config::KafkaConfig; + +use rdkafka::error::{KafkaError, RDKafkaErrorCode}; +use rdkafka::producer::{DeliveryFuture, FutureProducer, FutureRecord, Producer}; +use rdkafka::util::Timeout; +use rdkafka::ClientConfig; +use std::{str::FromStr, time::Duration}; +use tokio::sync::Semaphore; +use tracing::debug; + +// TODO: Take stats recording pieces that we want from `capture-rs`. +pub struct KafkaContext {} + +impl rdkafka::ClientContext for KafkaContext {} + +pub async fn create_kafka_producer( + config: &KafkaConfig, +) -> Result, KafkaError> { + let mut client_config = ClientConfig::new(); + client_config + .set("bootstrap.servers", &config.kafka_hosts) + .set("statistics.interval.ms", "10000") + .set("linger.ms", config.kafka_producer_linger_ms.to_string()) + .set( + "message.timeout.ms", + config.kafka_message_timeout_ms.to_string(), + ) + .set( + "compression.codec", + config.kafka_compression_codec.to_owned(), + ) + .set( + "queue.buffering.max.kbytes", + (config.kafka_producer_queue_mib * 1024).to_string(), + ); + + if config.kafka_tls { + client_config + .set("security.protocol", "ssl") + .set("enable.ssl.certificate.verification", "false"); + }; + + debug!("rdkafka configuration: {:?}", client_config); + let producer: FutureProducer = + client_config.create_with_context(KafkaContext {})?; + + Ok(producer) +} diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs new file mode 100644 index 0000000000000..b487fda796690 --- /dev/null +++ b/hook-janitor/src/main.rs @@ -0,0 +1,86 @@ +use axum::Router; +use cleanup::{Cleaner, CleanerModeName}; +use config::Config; +use envconfig::Envconfig; +use eyre::Result; +use futures::future::{select, Either}; +use kafka_producer::create_kafka_producer; +use std::{str::FromStr, time::Duration}; +use tokio::sync::Semaphore; +use webhooks::WebhookCleaner; + +use hook_common::metrics; + +mod cleanup; +mod config; +mod handlers; +mod kafka_producer; +mod webhooks; + +async fn listen(app: Router, bind: String) -> Result<()> { + let listener = tokio::net::TcpListener::bind(bind).await?; + + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn cleanup_loop(cleaner: Box, interval_secs: u64) -> Result<()> { + let semaphore = Semaphore::new(1); + let mut interval = tokio::time::interval(Duration::from_secs(interval_secs)); + + loop { + let _permit = semaphore.acquire().await; + interval.tick().await; + cleaner.cleanup().await; + drop(_permit); + } +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let config = Config::init_from_env().expect("failed to load configuration from env"); + + let mode_name = CleanerModeName::from_str(&config.mode) + .unwrap_or_else(|_| panic!("invalid cleaner mode: {}", config.mode)); + + let cleaner = match mode_name { + CleanerModeName::Webhooks => { + let kafka_producer = create_kafka_producer(&config.kafka) + .await + .expect("failed to create kafka producer"); + + Box::new( + WebhookCleaner::new( + &config.queue_name, + &config.table_name, + &config.database_url, + config.cleanup_batch_size, + kafka_producer, + config.kafka.app_metrics_topic.to_owned(), + config.kafka.plugin_log_entries_topic.to_owned(), + ) + .expect("unable to create webhook cleaner"), + ) + } + }; + + let cleanup_loop = Box::pin(cleanup_loop(cleaner, config.cleanup_interval_secs)); + + let recorder_handle = metrics::setup_metrics_recorder(); + let app = handlers::app(Some(recorder_handle)); + let http_server = Box::pin(listen(app, config.bind())); + + match select(http_server, cleanup_loop).await { + Either::Left((listen_result, _)) => match listen_result { + Ok(_) => {} + Err(e) => tracing::error!("failed to start hook-janitor http server, {}", e), + }, + Either::Right((cleanup_result, _)) => match cleanup_result { + Ok(_) => {} + Err(e) => tracing::error!("hook-janitor cleanup task exited, {}", e), + }, + }; +} diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs new file mode 100644 index 0000000000000..a6cd9ff803ea5 --- /dev/null +++ b/hook-janitor/src/webhooks.rs @@ -0,0 +1,54 @@ +use async_trait::async_trait; + +use rdkafka::producer::FutureProducer; +use sqlx::postgres::{PgPool, PgPoolOptions}; + +use crate::cleanup::{Cleaner, CleanerError}; +use crate::kafka_producer::KafkaContext; + +pub struct WebhookCleaner { + queue_name: String, + table_name: String, + pg_pool: PgPool, + batch_size: u32, + kafka_producer: FutureProducer, + app_metrics_topic: String, + plugin_log_entries_topic: String, +} + +impl WebhookCleaner { + pub fn new( + queue_name: &str, + table_name: &str, + database_url: &str, + batch_size: u32, + kafka_producer: FutureProducer, + app_metrics_topic: String, + plugin_log_entries_topic: String, + ) -> Result { + let queue_name = queue_name.to_owned(); + let table_name = table_name.to_owned(); + let pg_pool = PgPoolOptions::new() + .connect_lazy(database_url) + .map_err(|error| CleanerError::PoolCreationError { error })?; + + Ok(Self { + queue_name, + table_name, + pg_pool, + batch_size, + kafka_producer, + app_metrics_topic, + plugin_log_entries_topic, + }) + } +} + +#[async_trait] +impl Cleaner for WebhookCleaner { + async fn cleanup(&self) { + // TODO: collect stats on completed/failed rows + // TODO: push metrics about those rows into `app_metrics` + // TODO: delete those completed/failed rows + } +} diff --git a/hook-producer/Cargo.toml b/hook-producer/Cargo.toml index ef1a24b01820a..f4b116563fd12 100644 --- a/hook-producer/Cargo.toml +++ b/hook-producer/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = { version = "0.7.1", features = ["http2"] } +axum = { workspace = true } envconfig = { workspace = true } eyre = { workspace = true } hook-common = { path = "../hook-common" } diff --git a/hook-producer/src/handlers/app.rs b/hook-producer/src/handlers/app.rs index 911a04d397416..1666676fee1bb 100644 --- a/hook-producer/src/handlers/app.rs +++ b/hook-producer/src/handlers/app.rs @@ -1,6 +1,7 @@ use axum::{routing, Router}; use metrics_exporter_prometheus::PrometheusHandle; +use hook_common::metrics; use hook_common::pgqueue::PgQueue; use super::webhook; @@ -16,11 +17,11 @@ pub fn app(pg_pool: PgQueue, metrics: Option) -> Router { }), ) .route("/webhook", routing::post(webhook::post).with_state(pg_pool)) - .layer(axum::middleware::from_fn(crate::metrics::track_metrics)) + .layer(axum::middleware::from_fn(metrics::track_metrics)) } pub async fn index() -> &'static str { - "rusty hook" + "rusty-hook producer" } #[cfg(test)] @@ -55,6 +56,6 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); - assert_eq!(&body[..], b"rusty hook"); + assert_eq!(&body[..], b"rusty-hook producer"); } } diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index 29da8dd6efe0e..7c2b73c0f12bb 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -3,11 +3,11 @@ use config::Config; use envconfig::Envconfig; use eyre::Result; +use hook_common::metrics; use hook_common::pgqueue::{PgQueue, RetryPolicy}; mod config; mod handlers; -mod metrics; async fn listen(app: Router, bind: String) -> Result<()> { let listener = tokio::net::TcpListener::bind(bind).await?; @@ -35,7 +35,7 @@ async fn main() { .await .expect("failed to initialize queue"); - let recorder_handle = crate::metrics::setup_metrics_recorder(); + let recorder_handle = metrics::setup_metrics_recorder(); let app = handlers::app(pg_queue, Some(recorder_handle)); From d2d929d95f45c2e4f0649c70a0dcf95655bcc29c Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 14 Dec 2023 13:13:30 -0700 Subject: [PATCH 119/249] Add Kafka message types for app_metrics and plugin_log_entries --- Cargo.lock | 51 +++++++ Cargo.toml | 1 + hook-common/Cargo.toml | 3 + hook-common/src/kafka_messages/app_metrics.rs | 123 +++++++++++++++++ hook-common/src/kafka_messages/mod.rs | 20 +++ hook-common/src/kafka_messages/plugin_logs.rs | 130 ++++++++++++++++++ hook-common/src/lib.rs | 1 + 7 files changed, 329 insertions(+) create mode 100644 hook-common/src/kafka_messages/app_metrics.rs create mode 100644 hook-common/src/kafka_messages/mod.rs create mode 100644 hook-common/src/kafka_messages/plugin_logs.rs diff --git a/Cargo.lock b/Cargo.lock index c86f175f1c487..5b0aedcfa6038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.16" @@ -215,6 +224,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -999,11 +1014,14 @@ dependencies = [ "http 0.2.11", "metrics", "metrics-exporter-prometheus", + "regex", "serde", "serde_derive", + "serde_json", "sqlx", "thiserror", "tokio", + "uuid", ] [[package]] @@ -2016,6 +2034,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "reqwest" version = "0.11.22" @@ -2902,6 +2949,10 @@ name = "uuid" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "atomic", + "getrandom", +] [[package]] name = "valuable" diff --git a/Cargo.toml b/Cargo.toml index 535c2429412c7..faf8644a85dfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ tower = "0.4.13" tracing = "0.1.40" tracing-subscriber = "0.3.18" url = { version = "2.5.0 " } +uuid = { version = "1.6.1", features = ["v7"] } diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 213d2e98f7634..2c32d9d35c1b1 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -11,10 +11,13 @@ chrono = { workspace = true } http = { workspace = true } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } +regex = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } +serde_json = { workspace = true } sqlx = { workspace = true } thiserror = { workspace = true } +uuid = { workspace = true } [dev-dependencies] tokio = { workspace = true } # We need a runtime for async tests diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs new file mode 100644 index 0000000000000..a7530644280f6 --- /dev/null +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -0,0 +1,123 @@ +use chrono::{DateTime, Utc}; +use serde::{Serialize, Serializer}; +use uuid::Uuid; + +use super::{serialize_datetime, serialize_uuid}; + +#[derive(Serialize)] +pub enum AppMetricCategory { + ProcessEvent, + OnEvent, + ScheduledTask, + Webhook, + ComposeWebhook, +} + +#[derive(Serialize)] +pub enum ErrorType { + Timeout, + Connection, + HttpStatus(u16), +} + +#[derive(Serialize)] +pub struct ErrorDetails { + pub error: Error, + // TODO: The plugin-server sends the entire raw event with errors. In order to do this, we'll + // have to pass the entire event when we enqueue items, and store it in the Parameters JSONB + // column. We should see if it's possible to work around this before we commit to it. + // + // event: Value, +} + +#[derive(Serialize)] +pub struct Error { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + // TODO: Realistically, it doesn't seem likely that we'll generate Rust stack traces and put + // them here. I think this was more useful in plugin-server when the stack could come from + // plugin code. + #[serde(skip_serializing_if = "Option::is_none")] + pub stack: Option, +} + +#[derive(Serialize)] +pub struct AppMetric { + #[serde(serialize_with = "serialize_datetime")] + pub timestamp: DateTime, + pub team_id: u32, + pub plugin_config_id: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub job_id: Option, + #[serde(serialize_with = "serialize_category")] + pub category: AppMetricCategory, + pub successes: u32, + pub successes_on_retry: u32, + pub failures: u32, + #[serde(serialize_with = "serialize_uuid")] + pub error_uuid: Uuid, + #[serde(serialize_with = "serialize_error_type")] + pub error_type: ErrorType, + pub error_details: Error, +} + +fn serialize_category(category: &AppMetricCategory, serializer: S) -> Result +where + S: Serializer, +{ + let category_str = match category { + AppMetricCategory::ProcessEvent => "processEvent", + AppMetricCategory::OnEvent => "onEvent", + AppMetricCategory::ScheduledTask => "scheduledTask", + AppMetricCategory::Webhook => "webhook", + AppMetricCategory::ComposeWebhook => "composeWebhook", + }; + serializer.serialize_str(category_str) +} + +fn serialize_error_type(error_type: &ErrorType, serializer: S) -> Result +where + S: Serializer, +{ + let error_type = match error_type { + ErrorType::Connection => "Connection Error".to_owned(), + ErrorType::Timeout => "Timeout".to_owned(), + ErrorType::HttpStatus(s) => format!("HTTP Status: {}", s), + }; + serializer.serialize_str(&error_type) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_app_metric_serialization() { + use chrono::prelude::*; + + let app_metric = AppMetric { + timestamp: Utc.with_ymd_and_hms(2023, 12, 14, 12, 2, 0).unwrap(), + team_id: 123, + plugin_config_id: 456, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 10, + successes_on_retry: 0, + failures: 2, + error_uuid: Uuid::parse_str("550e8400-e29b-41d4-a716-446655447777").unwrap(), + error_type: ErrorType::Connection, + error_details: Error { + name: "FooError".to_owned(), + message: Some("Error Message".to_owned()), + stack: None, + }, + }; + + let serialized_json = serde_json::to_string(&app_metric).unwrap(); + + let expected_json = r#"{"timestamp":"2023-12-14 12:02:00","team_id":123,"plugin_config_id":456,"category":"webhook","successes":10,"successes_on_retry":0,"failures":2,"error_uuid":"550e8400-e29b-41d4-a716-446655447777","error_type":"Connection Error","error_details":{"name":"FooError","message":"Error Message"}}"#; + + assert_eq!(serialized_json, expected_json); + } +} diff --git a/hook-common/src/kafka_messages/mod.rs b/hook-common/src/kafka_messages/mod.rs new file mode 100644 index 0000000000000..1449f56faeaa7 --- /dev/null +++ b/hook-common/src/kafka_messages/mod.rs @@ -0,0 +1,20 @@ +pub mod app_metrics; +pub mod plugin_logs; + +use chrono::{DateTime, Utc}; +use serde::Serializer; +use uuid::Uuid; + +fn serialize_uuid(uuid: &Uuid, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&uuid.to_string()) +} + +fn serialize_datetime(datetime: &DateTime, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f").to_string()) +} diff --git a/hook-common/src/kafka_messages/plugin_logs.rs b/hook-common/src/kafka_messages/plugin_logs.rs new file mode 100644 index 0000000000000..8f8bb43efea96 --- /dev/null +++ b/hook-common/src/kafka_messages/plugin_logs.rs @@ -0,0 +1,130 @@ +use chrono::{DateTime, Utc}; +use serde::{Serialize, Serializer}; +use uuid::Uuid; + +use super::{serialize_datetime, serialize_uuid}; + +#[allow(dead_code)] +#[derive(Serialize)] +pub enum PluginLogEntrySource { + System, + Plugin, + Console, +} + +#[allow(dead_code)] +#[derive(Serialize)] +pub enum PluginLogEntryType { + Debug, + Log, + Info, + Warn, + Error, +} + +#[derive(Serialize)] +pub struct PluginLogEntry { + #[serde(serialize_with = "serialize_source")] + pub source: PluginLogEntrySource, + #[serde(rename = "type", serialize_with = "serialize_type")] + pub type_: PluginLogEntryType, + #[serde(serialize_with = "serialize_uuid")] + pub id: Uuid, + pub team_id: u32, + pub plugin_id: u32, + pub plugin_config_id: u32, + #[serde(serialize_with = "serialize_datetime")] + pub timestamp: DateTime, + #[serde(serialize_with = "serialize_message")] + pub message: String, + #[serde(serialize_with = "serialize_uuid")] + pub instance_id: Uuid, +} + +fn serialize_source(source: &PluginLogEntrySource, serializer: S) -> Result +where + S: Serializer, +{ + let source_str = match source { + PluginLogEntrySource::System => "SYSTEM", + PluginLogEntrySource::Plugin => "PLUGIN", + PluginLogEntrySource::Console => "CONSOLE", + }; + serializer.serialize_str(source_str) +} + +fn serialize_type(type_: &PluginLogEntryType, serializer: S) -> Result +where + S: Serializer, +{ + let type_str = match type_ { + PluginLogEntryType::Debug => "DEBUG", + PluginLogEntryType::Log => "LOG", + PluginLogEntryType::Info => "INFO", + PluginLogEntryType::Warn => "WARN", + PluginLogEntryType::Error => "ERROR", + }; + serializer.serialize_str(type_str) +} + +fn serialize_message(msg: &String, serializer: S) -> Result +where + S: Serializer, +{ + if msg.len() > 50_000 { + return Err(serde::ser::Error::custom( + "Message is too long for ClickHouse", + )); + } + + serializer.serialize_str(msg) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_plugin_log_entry_serialization() { + use chrono::prelude::*; + + let log_entry = PluginLogEntry { + source: PluginLogEntrySource::Plugin, + type_: PluginLogEntryType::Warn, + id: Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(), + team_id: 4, + plugin_id: 5, + plugin_config_id: 6, + timestamp: Utc.with_ymd_and_hms(2023, 12, 14, 12, 2, 0).unwrap(), + message: "My message!".to_string(), + instance_id: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(), + }; + + let serialized_json = serde_json::to_string(&log_entry).unwrap(); + + assert_eq!( + serialized_json, + r#"{"source":"PLUGIN","type":"WARN","id":"550e8400-e29b-41d4-a716-446655440000","team_id":4,"plugin_id":5,"plugin_config_id":6,"timestamp":"2023-12-14 12:02:00","message":"My message!","instance_id":"00000000-0000-0000-0000-000000000000"}"# + ); + } + + #[test] + fn test_plugin_log_entry_message_too_long() { + use chrono::prelude::*; + + let log_entry = PluginLogEntry { + source: PluginLogEntrySource::Plugin, + type_: PluginLogEntryType::Warn, + id: Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(), + team_id: 4, + plugin_id: 5, + plugin_config_id: 6, + timestamp: Utc.with_ymd_and_hms(2023, 12, 14, 12, 2, 0).unwrap(), + message: "My message!".repeat(10_000).to_string(), + instance_id: Uuid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(), + }; + + let err = serde_json::to_string(&log_entry).unwrap_err(); + assert_eq!(err.to_string(), "Message is too long for ClickHouse"); + } +} diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs index 3b154c834224a..7d9ef37e84606 100644 --- a/hook-common/src/lib.rs +++ b/hook-common/src/lib.rs @@ -1,3 +1,4 @@ +pub mod kafka_messages; pub mod metrics; pub mod pgqueue; pub mod webhook; From a938a107faf8f48cfa176887177b52971f81de14 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Mon, 18 Dec 2023 09:40:15 -0700 Subject: [PATCH 120/249] Squelch clippy complaints --- hook-janitor/src/kafka_producer.rs | 7 ++----- hook-janitor/src/webhooks.rs | 1 + 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/hook-janitor/src/kafka_producer.rs b/hook-janitor/src/kafka_producer.rs index 4e905b3e9bae2..4845e9410df56 100644 --- a/hook-janitor/src/kafka_producer.rs +++ b/hook-janitor/src/kafka_producer.rs @@ -1,11 +1,8 @@ use crate::config::KafkaConfig; -use rdkafka::error::{KafkaError, RDKafkaErrorCode}; -use rdkafka::producer::{DeliveryFuture, FutureProducer, FutureRecord, Producer}; -use rdkafka::util::Timeout; +use rdkafka::error::KafkaError; +use rdkafka::producer::FutureProducer; use rdkafka::ClientConfig; -use std::{str::FromStr, time::Duration}; -use tokio::sync::Semaphore; use tracing::debug; // TODO: Take stats recording pieces that we want from `capture-rs`. diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index a6cd9ff803ea5..e8895f1ba0781 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -6,6 +6,7 @@ use sqlx::postgres::{PgPool, PgPoolOptions}; use crate::cleanup::{Cleaner, CleanerError}; use crate::kafka_producer::KafkaContext; +#[allow(dead_code)] pub struct WebhookCleaner { queue_name: String, table_name: String, From 5a5c3bed8c3f6c78bbe869b8a83cbd288cd90eb2 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Mon, 18 Dec 2023 10:32:35 -0700 Subject: [PATCH 121/249] Remove pointless Result --- hook-janitor/src/main.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs index b487fda796690..46223aa7aafb3 100644 --- a/hook-janitor/src/main.rs +++ b/hook-janitor/src/main.rs @@ -25,7 +25,7 @@ async fn listen(app: Router, bind: String) -> Result<()> { Ok(()) } -async fn cleanup_loop(cleaner: Box, interval_secs: u64) -> Result<()> { +async fn cleanup_loop(cleaner: Box, interval_secs: u64) { let semaphore = Semaphore::new(1); let mut interval = tokio::time::interval(Duration::from_secs(interval_secs)); @@ -78,9 +78,8 @@ async fn main() { Ok(_) => {} Err(e) => tracing::error!("failed to start hook-janitor http server, {}", e), }, - Either::Right((cleanup_result, _)) => match cleanup_result { - Ok(_) => {} - Err(e) => tracing::error!("hook-janitor cleanup task exited, {}", e), - }, + Either::Right((_, _)) => { + tracing::error!("hook-janitor cleanup task exited") + } }; } From 769c8f22fc83f348ef44a805111b2395e4322e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Mon, 18 Dec 2023 11:34:38 +0100 Subject: [PATCH 122/249] refactor: Add a metadata field to job queue --- hook-common/src/pgqueue.rs | 110 ++++++++++++------ hook-common/src/webhook.rs | 14 +-- hook-consumer/src/consumer.rs | 41 ++++--- hook-producer/src/handlers/webhook.rs | 106 +++++++++++------ migrations/20231129172339_job_queue_table.sql | 5 +- 5 files changed, 179 insertions(+), 97 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 47938ecd83760..5288ade106f47 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -74,9 +74,12 @@ impl FromStr for JobStatus { /// JobParameters are stored and read to and from a JSONB field, so we accept anything that fits `sqlx::types::Json`. pub type JobParameters = sqlx::types::Json; +/// JobMetadata are stored and read to and from a JSONB field, so we accept anything that fits `sqlx::types::Json`. +pub type JobMetadata = sqlx::types::Json; + /// A Job to be executed by a worker dequeueing a PgQueue. #[derive(sqlx::FromRow, Debug)] -pub struct Job { +pub struct Job { /// A unique id identifying a job. pub id: i64, /// A number corresponding to the current job attempt. @@ -89,6 +92,8 @@ pub struct Job { pub created_at: chrono::DateTime, /// The current job's number of max attempts. pub max_attempts: i32, + /// Arbitrary job metadata stored as JSON. + pub metadata: JobMetadata, /// Arbitrary job parameters stored as JSON. pub parameters: JobParameters, /// The queue this job belongs to. @@ -99,7 +104,7 @@ pub struct Job { pub target: String, } -impl Job { +impl Job { /// Return true if this job attempt is greater or equal to the maximum number of possible attempts. pub fn is_gte_max_attempts(&self) -> bool { self.attempt >= self.max_attempts @@ -146,19 +151,19 @@ impl Job { /// A Job that can be updated in PostgreSQL. #[derive(Debug)] -pub struct PgJob { - pub job: Job, +pub struct PgJob { + pub job: Job, pub table: String, pub connection: sqlx::pool::PoolConnection, pub retry_policy: RetryPolicy, } -impl PgJob { +impl PgJob { pub async fn retry( mut self, error: E, preferred_retry_interval: Option, - ) -> Result, PgJobError>> { + ) -> Result, PgJobError>> { if self.job.is_gte_max_attempts() { return Err(PgJobError::RetryInvalidError { job: self, @@ -203,7 +208,7 @@ RETURNING Ok(retryable_job) } - pub async fn complete(mut self) -> Result>> { + pub async fn complete(mut self) -> Result>> { let completed_job = self.job.complete(); let base_query = format!( @@ -238,7 +243,7 @@ RETURNING pub async fn fail( mut self, error: E, - ) -> Result, PgJobError>> { + ) -> Result, PgJobError>> { let failed_job = self.job.fail(error); let base_query = format!( @@ -277,19 +282,19 @@ RETURNING /// A Job within an open PostgreSQL transaction. /// This implementation allows 'hiding' the job from any other workers running SKIP LOCKED queries. #[derive(Debug)] -pub struct PgTransactionJob<'c, J> { - pub job: Job, +pub struct PgTransactionJob<'c, J, M> { + pub job: Job, pub table: String, pub transaction: sqlx::Transaction<'c, sqlx::postgres::Postgres>, pub retry_policy: RetryPolicy, } -impl<'c, J> PgTransactionJob<'c, J> { +impl<'c, J, M> PgTransactionJob<'c, J, M> { pub async fn retry( mut self, error: E, preferred_retry_interval: Option, - ) -> Result, PgJobError>> { + ) -> Result, PgJobError>> { if self.job.is_gte_max_attempts() { return Err(PgJobError::RetryInvalidError { job: self, @@ -343,7 +348,9 @@ RETURNING Ok(retryable_job) } - pub async fn complete(mut self) -> Result>> { + pub async fn complete( + mut self, + ) -> Result>> { let completed_job = self.job.complete(); let base_query = format!( @@ -386,7 +393,7 @@ RETURNING pub async fn fail( mut self, error: E, - ) -> Result, PgJobError>> { + ) -> Result, PgJobError>> { let failed_job = self.job.fail(error); let base_query = format!( @@ -461,19 +468,22 @@ pub struct FailedJob { } /// A NewJob to be enqueued into a PgQueue. -pub struct NewJob { +pub struct NewJob { /// The maximum amount of attempts this NewJob has to complete. pub max_attempts: i32, /// The JSON-deserializable parameters for this NewJob. + pub metadata: JobMetadata, + /// The JSON-deserializable parameters for this NewJob. pub parameters: JobParameters, /// The target of the NewJob. E.g. an endpoint or service we are trying to reach. pub target: String, } -impl NewJob { - pub fn new(max_attempts: i32, parameters: J, target: &str) -> Self { +impl NewJob { + pub fn new(max_attempts: i32, metadata: M, parameters: J, target: &str) -> Self { Self { max_attempts, + metadata: sqlx::types::Json(metadata), parameters: sqlx::types::Json(parameters), target: target.to_owned(), } @@ -583,10 +593,11 @@ impl PgQueue { /// Dequeue a Job from this PgQueue to work on it. pub async fn dequeue< J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, + M: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, >( &self, attempted_by: &str, - ) -> PgQueueResult>> { + ) -> PgQueueResult>> { let mut connection = self .pool .acquire() @@ -628,7 +639,7 @@ RETURNING &self.table ); - let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) + let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) .bind(&self.name) .bind(attempted_by) .fetch_one(&mut *connection) @@ -662,10 +673,11 @@ RETURNING pub async fn dequeue_tx< 'a, J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, + M: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, >( &self, attempted_by: &str, - ) -> PgQueueResult>> { + ) -> PgQueueResult>> { let mut tx = self .pool .begin() @@ -707,7 +719,7 @@ RETURNING &self.table ); - let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) + let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) .bind(&self.name) .bind(attempted_by) .fetch_one(&mut *tx) @@ -732,23 +744,27 @@ RETURNING /// Enqueue a Job into this PgQueue. /// We take ownership of NewJob to enforce a specific NewJob is only enqueued once. - pub async fn enqueue( + pub async fn enqueue< + J: serde::Serialize + std::marker::Sync, + M: serde::Serialize + std::marker::Sync, + >( &self, - job: NewJob, + job: NewJob, ) -> PgQueueResult<()> { // TODO: Escaping. I think sqlx doesn't support identifiers. let base_query = format!( r#" INSERT INTO {} - (attempt, created_at, scheduled_at, max_attempts, parameters, queue, status, target) + (attempt, created_at, scheduled_at, max_attempts, metadata, parameters, queue, status, target) VALUES - (0, NOW(), NOW(), $1, $2, $3, 'available'::job_status, $4) + (0, NOW(), NOW(), $1, $2, $3, $4, 'available'::job_status, $5) "#, &self.table ); sqlx::query(&base_query) .bind(job.max_attempts) + .bind(&job.metadata) .bind(&job.parameters) .bind(&self.name) .bind(&job.target) @@ -767,6 +783,23 @@ VALUES mod tests { use super::*; + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] + struct JobMetadata { + team_id: u32, + plugin_config_id: u32, + plugin_id: u32, + } + + impl Default for JobMetadata { + fn default() -> Self { + Self { + team_id: 0, + plugin_config_id: 1, + plugin_id: 2, + } + } + } + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] struct JobParameters { method: String, @@ -798,8 +831,9 @@ mod tests { async fn test_can_dequeue_job() { let job_target = job_target(); let job_parameters = JobParameters::default(); + let job_metadata = JobMetadata::default(); let worker_id = worker_id(); - let new_job = NewJob::new(1, job_parameters, &job_target); + let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); let queue = PgQueue::new( "test_can_dequeue_job", @@ -812,7 +846,7 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); - let pg_job: PgJob = queue + let pg_job: PgJob = queue .dequeue(&worker_id) .await .expect("failed to dequeue job") @@ -839,7 +873,7 @@ mod tests { .await .expect("failed to connect to local test postgresql database"); - let pg_job: Option> = queue + let pg_job: Option> = queue .dequeue(&worker_id) .await .expect("failed to dequeue job"); @@ -850,9 +884,10 @@ mod tests { #[tokio::test] async fn test_can_dequeue_tx_job() { let job_target = job_target(); + let job_metadata = JobMetadata::default(); let job_parameters = JobParameters::default(); let worker_id = worker_id(); - let new_job = NewJob::new(1, job_parameters, &job_target); + let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); let queue = PgQueue::new( "test_can_dequeue_tx_job", @@ -865,7 +900,7 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); - let tx_job: PgTransactionJob<'_, JobParameters> = queue + let tx_job: PgTransactionJob<'_, JobParameters, JobMetadata> = queue .dequeue_tx(&worker_id) .await .expect("failed to dequeue job") @@ -875,6 +910,7 @@ mod tests { assert!(tx_job.job.attempted_by.contains(&worker_id)); assert_eq!(tx_job.job.attempted_by.len(), 1); assert_eq!(tx_job.job.max_attempts, 1); + assert_eq!(*tx_job.job.metadata.as_ref(), JobMetadata::default()); assert_eq!(*tx_job.job.parameters.as_ref(), JobParameters::default()); assert_eq!(tx_job.job.status, JobStatus::Running); assert_eq!(tx_job.job.target, job_target); @@ -892,7 +928,7 @@ mod tests { .await .expect("failed to connect to local test postgresql database"); - let tx_job: Option> = queue + let tx_job: Option> = queue .dequeue_tx(&worker_id) .await .expect("failed to dequeue job"); @@ -904,8 +940,9 @@ mod tests { async fn test_can_retry_job_with_remaining_attempts() { let job_target = job_target(); let job_parameters = JobParameters::default(); + let job_metadata = JobMetadata::default(); let worker_id = worker_id(); - let new_job = NewJob::new(2, job_parameters, &job_target); + let new_job = NewJob::new(2, job_metadata, job_parameters, &job_target); let retry_policy = RetryPolicy { backoff_coefficient: 0, initial_interval: time::Duration::from_secs(0), @@ -922,7 +959,7 @@ mod tests { .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: PgJob = queue + let job: PgJob = queue .dequeue(&worker_id) .await .expect("failed to dequeue job") @@ -931,7 +968,7 @@ mod tests { .retry("a very reasonable failure reason", None) .await .expect("failed to retry job"); - let retried_job: PgJob = queue + let retried_job: PgJob = queue .dequeue(&worker_id) .await .expect("failed to dequeue job") @@ -954,8 +991,9 @@ mod tests { async fn test_cannot_retry_job_without_remaining_attempts() { let job_target = job_target(); let job_parameters = JobParameters::default(); + let job_metadata = JobMetadata::default(); let worker_id = worker_id(); - let new_job = NewJob::new(1, job_parameters, &job_target); + let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); let retry_policy = RetryPolicy { backoff_coefficient: 0, initial_interval: time::Duration::from_secs(0), @@ -973,7 +1011,7 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: PgJob = queue + let job: PgJob = queue .dequeue(&worker_id) .await .expect("failed to dequeue job") diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index b17959ce0f6da..64968fc51deb3 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -124,16 +124,14 @@ pub struct WebhookJobParameters { pub headers: collections::HashMap, pub method: HttpMethod, pub url: String, +} - // These should be set if the Webhook is associated with a plugin `composeWebhook` invocation. +/// `JobParameters` required for the `WebhookConsumer` to execute a webhook. +/// These parameters should match the exported Webhook interface that PostHog plugins. +/// implement. See: https://github.com/PostHog/plugin-scaffold/blob/main/src/types.ts#L15. +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct WebhookJobMetadata { pub team_id: Option, pub plugin_id: Option, pub plugin_config_id: Option, - - #[serde(default = "default_max_attempts")] - pub max_attempts: i32, -} - -fn default_max_attempts() -> i32 { - 3 } diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 59b8e9f24d949..9e4b3b798405e 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -4,7 +4,7 @@ use std::time; use async_std::task; use hook_common::pgqueue::{PgJobError, PgQueue, PgQueueError, PgTransactionJob}; -use hook_common::webhook::{HttpMethod, WebhookJobParameters}; +use hook_common::webhook::{HttpMethod, WebhookJobMetadata, WebhookJobParameters}; use http::StatusCode; use reqwest::header; use tokio::sync; @@ -57,7 +57,8 @@ impl<'p> WebhookConsumer<'p> { /// Wait until a job becomes available in our queue. async fn wait_for_job<'a>( &self, - ) -> Result, WebhookConsumerError> { + ) -> Result, WebhookConsumerError> + { loop { if let Some(job) = self.queue.dequeue_tx(&self.name).await? { return Ok(job); @@ -102,7 +103,7 @@ impl<'p> WebhookConsumer<'p> { /// * `request_timeout`: A timeout for the HTTP request. async fn process_webhook_job( client: reqwest::Client, - webhook_job: PgTransactionJob<'_, WebhookJobParameters>, + webhook_job: PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata>, ) -> Result<(), WebhookConsumerError> { match send_webhook( client, @@ -261,9 +262,10 @@ mod tests { queue: &PgQueue, max_attempts: i32, job_parameters: WebhookJobParameters, + job_metadata: WebhookJobMetadata, ) -> Result<(), PgQueueError> { let job_target = job_parameters.url.to_owned(); - let new_job = NewJob::new(max_attempts, job_parameters, &job_target); + let new_job = NewJob::new(max_attempts, job_metadata, job_parameters, &job_target); queue.enqueue(new_job).await?; Ok(()) } @@ -308,25 +310,29 @@ mod tests { .await .expect("failed to connect to PG"); - let webhook_job = WebhookJobParameters { + let webhook_job_parameters = WebhookJobParameters { body: "a webhook job body. much wow.".to_owned(), headers: collections::HashMap::new(), method: HttpMethod::POST, url: "localhost".to_owned(), - - team_id: Some(1), - plugin_id: Some(2), - plugin_config_id: Some(3), - - max_attempts: 1, + }; + let webhook_job_metadata = WebhookJobMetadata { + team_id: None, + plugin_id: None, + plugin_config_id: None, }; // enqueue takes ownership of the job enqueued to avoid bugs that can cause duplicate jobs. // Normally, a separate application would be enqueueing jobs for us to consume, so no ownership // conflicts would arise. However, in this test we need to do the enqueueing ourselves. // So, we clone the job to keep it around and assert the values returned by wait_for_job. - enqueue_job(&queue, 1, webhook_job.clone()) - .await - .expect("failed to enqueue job"); + enqueue_job( + &queue, + 1, + webhook_job_parameters.clone(), + webhook_job_metadata, + ) + .await + .expect("failed to enqueue job"); let consumer = WebhookConsumer::new( &worker_id, &queue, @@ -344,9 +350,12 @@ mod tests { assert!(consumed_job.job.attempted_by.contains(&worker_id)); assert_eq!(consumed_job.job.attempted_by.len(), 1); assert_eq!(consumed_job.job.max_attempts, 1); - assert_eq!(*consumed_job.job.parameters.as_ref(), webhook_job); + assert_eq!( + *consumed_job.job.parameters.as_ref(), + webhook_job_parameters + ); assert_eq!(consumed_job.job.status, JobStatus::Running); - assert_eq!(consumed_job.job.target, webhook_job.url); + assert_eq!(consumed_job.job.target, webhook_job_parameters.url); consumed_job .complete() diff --git a/hook-producer/src/handlers/webhook.rs b/hook-producer/src/handlers/webhook.rs index 7de1126fe4db8..394732094a3c6 100644 --- a/hook-producer/src/handlers/webhook.rs +++ b/hook-producer/src/handlers/webhook.rs @@ -1,5 +1,5 @@ use axum::{extract::State, http::StatusCode, Json}; -use hook_common::webhook::WebhookJobParameters; +use hook_common::webhook::{WebhookJobMetadata, WebhookJobParameters}; use serde_derive::Deserialize; use url::Url; @@ -15,13 +15,27 @@ pub struct WebhookPostResponse { error: Option, } +/// The body of a request made to create a webhook Job. +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] +pub struct WebhookPostRequestBody { + parameters: WebhookJobParameters, + metadata: WebhookJobMetadata, + + #[serde(default = "default_max_attempts")] + max_attempts: u32, +} + +fn default_max_attempts() -> u32 { + 3 +} + pub async fn post( State(pg_queue): State, - Json(payload): Json, + Json(payload): Json, ) -> Result, (StatusCode, Json)> { debug!("received payload: {:?}", payload); - if payload.body.len() > MAX_BODY_SIZE { + if payload.parameters.body.len() > MAX_BODY_SIZE { return Err(( StatusCode::BAD_REQUEST, Json(WebhookPostResponse { @@ -30,8 +44,22 @@ pub async fn post( )); } - let url_hostname = get_hostname(&payload.url)?; - let job = NewJob::new(payload.max_attempts, payload, url_hostname.as_str()); + let url_hostname = get_hostname(&payload.parameters.url)?; + // We could cast to i32, but this ensures we are not wrapping. + let max_attempts = i32::try_from(payload.max_attempts).map_err(|_| { + ( + StatusCode::BAD_REQUEST, + Json(WebhookPostResponse { + error: Some("invalid number of max attempts".to_owned()), + }), + ) + })?; + let job = NewJob::new( + max_attempts, + payload.metadata, + payload.parameters, + url_hostname.as_str(), + ); pg_queue.enqueue(job).await.map_err(internal_error)?; @@ -74,6 +102,8 @@ fn get_hostname(url_str: &str) -> Result Date: Mon, 18 Dec 2023 11:43:11 +0100 Subject: [PATCH 123/249] fix: Update webhook.rs docs --- hook-common/src/webhook.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 64968fc51deb3..ce7d3ae2ad4d0 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -126,9 +126,8 @@ pub struct WebhookJobParameters { pub url: String, } -/// `JobParameters` required for the `WebhookConsumer` to execute a webhook. -/// These parameters should match the exported Webhook interface that PostHog plugins. -/// implement. See: https://github.com/PostHog/plugin-scaffold/blob/main/src/types.ts#L15. +/// `JobMetadata` required for the `WebhookConsumer` to execute a webhook. +/// These should be set if the Webhook is associated with a plugin `composeWebhook` invocation. #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct WebhookJobMetadata { pub team_id: Option, From 491c2697d29aa995e972478964b4d8cfc85bff20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Mon, 18 Dec 2023 12:08:08 +0100 Subject: [PATCH 124/249] feat: Use new WebhookJobError struct for error reporting --- Cargo.lock | 156 +++++++++--------- Cargo.toml | 1 + hook-common/Cargo.toml | 1 + hook-common/src/kafka_messages/app_metrics.rs | 10 +- hook-common/src/kafka_messages/mod.rs | 4 +- hook-common/src/webhook.rs | 116 +++++++++++++ hook-consumer/Cargo.toml | 2 +- hook-consumer/src/consumer.rs | 117 +++++++------ hook-consumer/src/error.rs | 27 +-- hook-consumer/src/main.rs | 4 +- 10 files changed, 294 insertions(+), 144 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b0aedcfa6038..ba9f4bb10b67d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,13 +100,13 @@ dependencies = [ [[package]] name = "async-global-executor" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4353121d5644cdf2beb5726ab752e79a8db1ebb52031770ec47db31d245526" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ "async-channel 2.1.1", "async-executor", - "async-io 2.2.1", + "async-io 2.2.2", "async-lock 3.2.0", "blocking", "futures-lite 2.1.0", @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6d3b15875ba253d1110c740755e246537483f152fa334f91abd7fe84c88b3ff" +checksum = "6afaa937395a620e33dc6a742c593c01aced20aa376ffb0f628121198578ccc7" dependencies = [ "async-lock 3.2.0", "cfg-if", @@ -146,7 +146,7 @@ dependencies = [ "futures-lite 2.1.0", "parking", "polling 3.3.1", - "rustix 0.38.27", + "rustix 0.38.28", "slab", "tracing", "windows-sys 0.52.0", @@ -200,9 +200,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.5.0" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4eb2cdb97421e01129ccb49169d8279ed21e829929144f4a22a6e54ac549ca1" +checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" [[package]] name = "async-trait" @@ -212,7 +212,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -265,7 +265,7 @@ dependencies = [ "http 1.0.0", "http-body 1.0.0", "http-body-util", - "hyper 1.0.1", + "hyper 1.1.0", "hyper-util", "itoa", "matchit", @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" @@ -485,22 +485,21 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-epoch" -version = "0.9.15" +version = "0.9.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" dependencies = [ "cfg-if", "crossbeam-utils", @@ -508,9 +507,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" dependencies = [ "cfg-if", ] @@ -648,9 +647,9 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bbb8258be8305fb0237d7b295f47bb24ff1b136a535f473baf40e70468515aa" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" dependencies = [ "indenter", "once_cell", @@ -813,7 +812,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -980,9 +979,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -998,11 +997,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1015,6 +1014,7 @@ dependencies = [ "metrics", "metrics-exporter-prometheus", "regex", + "reqwest", "serde", "serde_derive", "serde_json", @@ -1115,9 +1115,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.11", @@ -1161,9 +1161,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1171,12 +1171,12 @@ dependencies = [ "futures-util", "h2 0.3.22", "http 0.2.11", - "http-body 0.4.5", + "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1185,9 +1185,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" dependencies = [ "bytes", "futures-channel", @@ -1209,7 +1209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.27", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", @@ -1226,7 +1226,7 @@ dependencies = [ "futures-util", "http 1.0.0", "http-body 1.0.0", - "hyper 1.0.1", + "hyper 1.1.0", "pin-project-lite", "socket2 0.5.5", "tokio", @@ -1331,9 +1331,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" @@ -1364,9 +1364,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libm" @@ -1481,12 +1481,12 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4964177ddfdab1e3a2b37aec7cf320e14169abb0ed73999f558136409178d5" +checksum = "1d4fa7ce7c4862db464a37b0b31d89bca874562f034bd7993895572783d02950" dependencies = [ "base64", - "hyper 0.14.27", + "hyper 0.14.28", "indexmap 1.9.3", "ipnet", "metrics", @@ -1505,7 +1505,7 @@ checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -1721,7 +1721,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -1815,7 +1815,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -1893,7 +1893,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "pin-project-lite", - "rustix 0.38.27", + "rustix 0.38.28", "tracing", "windows-sys 0.52.0", ] @@ -2065,9 +2065,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64", "bytes", @@ -2076,8 +2076,8 @@ dependencies = [ "futures-util", "h2 0.3.22", "http 0.2.11", - "http-body 0.4.5", - "hyper 0.14.27", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-tls", "ipnet", "js-sys", @@ -2143,9 +2143,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.27" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfeae074e687625746172d639330f1de242a178bf3189b51e35a7a21573513ac" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", @@ -2162,9 +2162,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" @@ -2221,7 +2221,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -2620,9 +2620,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" dependencies = [ "proc-macro2", "quote", @@ -2665,28 +2665,28 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall", - "rustix 0.38.27", + "rustix 0.38.28", "windows-sys 0.48.0", ] [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -2716,9 +2716,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" dependencies = [ "backtrace", "bytes", @@ -2741,7 +2741,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -2844,7 +2844,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] @@ -3020,7 +3020,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", "wasm-bindgen-shared", ] @@ -3054,7 +3054,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3246,9 +3246,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.28" +version = "0.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" dependencies = [ "memchr", ] @@ -3265,22 +3265,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.29" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d075cf85bbb114e933343e087b92f2146bac0d55b534cbb8188becf0039948e" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.29" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86cd5ca076997b97ef09d3ad65efe811fa68c9e874cb636ccb211223a813b0c2" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.41", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index faf8644a85dfb..0a0a2f578560b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ http-body-util = "0.1.0" metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" rdkafka = { version = "0.35.0", features = ["cmake-build", "ssl"] } +reqwest = { version = "0.11" } regex = "1.10.2" serde = { version = "1.0" } serde_derive = { version = "1.0" } diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 2c32d9d35c1b1..6350ba4b8f7c1 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -11,6 +11,7 @@ chrono = { workspace = true } http = { workspace = true } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } +reqwest = { workspace = true } regex = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index a7530644280f6..327f4b89fb6a6 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -13,14 +13,16 @@ pub enum AppMetricCategory { ComposeWebhook, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub enum ErrorType { Timeout, Connection, HttpStatus(u16), + Parse, + MaxAttempts, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct ErrorDetails { pub error: Error, // TODO: The plugin-server sends the entire raw event with errors. In order to do this, we'll @@ -30,7 +32,7 @@ pub struct ErrorDetails { // event: Value, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct Error { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -84,6 +86,8 @@ where ErrorType::Connection => "Connection Error".to_owned(), ErrorType::Timeout => "Timeout".to_owned(), ErrorType::HttpStatus(s) => format!("HTTP Status: {}", s), + ErrorType::Parse => "Parse Error".to_owned(), + ErrorType::MaxAttempts => "Maximum attempts exceeded".to_owned(), }; serializer.serialize_str(&error_type) } diff --git a/hook-common/src/kafka_messages/mod.rs b/hook-common/src/kafka_messages/mod.rs index 1449f56faeaa7..a29e2193d327b 100644 --- a/hook-common/src/kafka_messages/mod.rs +++ b/hook-common/src/kafka_messages/mod.rs @@ -5,14 +5,14 @@ use chrono::{DateTime, Utc}; use serde::Serializer; use uuid::Uuid; -fn serialize_uuid(uuid: &Uuid, serializer: S) -> Result +pub fn serialize_uuid(uuid: &Uuid, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&uuid.to_string()) } -fn serialize_datetime(datetime: &DateTime, serializer: S) -> Result +pub fn serialize_datetime(datetime: &DateTime, serializer: S) -> Result where S: Serializer, { diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index ce7d3ae2ad4d0..ea7dfb4724159 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -1,9 +1,11 @@ use std::collections; +use std::convert::From; use std::fmt; use std::str::FromStr; use serde::{de::Visitor, Deserialize, Serialize}; +use crate::kafka_messages::{app_metrics, serialize_uuid}; use crate::pgqueue::PgQueueError; /// Supported HTTP methods for webhooks. @@ -134,3 +136,117 @@ pub struct WebhookJobMetadata { pub plugin_id: Option, pub plugin_config_id: Option, } + +/// An error originating during a Webhook Job invocation. +#[derive(Serialize, Debug)] +pub struct WebhookJobError { + pub r#type: app_metrics::ErrorType, + pub details: app_metrics::ErrorDetails, + #[serde(serialize_with = "serialize_uuid")] + pub uuid: uuid::Uuid, +} + +impl From for WebhookJobError { + fn from(error: reqwest::Error) -> Self { + if error.is_body() || error.is_decode() { + WebhookJobError::new_parse(&error.to_string()) + } else if error.is_timeout() { + WebhookJobError::new_timeout(&error.to_string()) + } else if error.is_status() { + WebhookJobError::new_http_status( + error.status().expect("status code is defined").into(), + &error.to_string(), + ) + } else if error.is_connect() + || error.is_builder() + || error.is_request() + || error.is_redirect() + { + // Builder errors seem to be related to unable to setup TLS, so I'm bundling them in connection. + WebhookJobError::new_connection(&error.to_string()) + } else { + // We can't match on Kind as some types do not have an associated variant in Kind (e.g. Timeout). + unreachable!("We have covered all reqwest::Error types.") + } + } +} + +impl WebhookJobError { + pub fn new_timeout(message: &str) -> Self { + let error_details = app_metrics::Error { + name: "timeout".to_owned(), + message: Some(message.to_owned()), + stack: None, + }; + Self { + r#type: app_metrics::ErrorType::Timeout, + details: app_metrics::ErrorDetails { + error: error_details, + }, + uuid: uuid::Uuid::now_v7(), + } + } + + pub fn new_connection(message: &str) -> Self { + let error_details = app_metrics::Error { + name: "connection error".to_owned(), + message: Some(message.to_owned()), + stack: None, + }; + Self { + r#type: app_metrics::ErrorType::Connection, + details: app_metrics::ErrorDetails { + error: error_details, + }, + uuid: uuid::Uuid::now_v7(), + } + } + + pub fn new_http_status(status_code: u16, message: &str) -> Self { + let error_details = app_metrics::Error { + name: "http status".to_owned(), + message: Some(message.to_owned()), + stack: None, + }; + Self { + r#type: app_metrics::ErrorType::HttpStatus(status_code), + details: app_metrics::ErrorDetails { + error: error_details, + }, + uuid: uuid::Uuid::now_v7(), + } + } + + pub fn new_parse(message: &str) -> Self { + let error_details = app_metrics::Error { + name: "parse error".to_owned(), + message: Some(message.to_owned()), + stack: None, + }; + Self { + r#type: app_metrics::ErrorType::Parse, + details: app_metrics::ErrorDetails { + error: error_details, + }, + uuid: uuid::Uuid::now_v7(), + } + } + + pub fn new_max_attempts(max_attempts: i32) -> Self { + let error_details = app_metrics::Error { + name: "maximum attempts exceeded".to_owned(), + message: Some(format!( + "Exceeded maximum number of attempts ({}) for webhook", + max_attempts + )), + stack: None, + }; + Self { + r#type: app_metrics::ErrorType::MaxAttempts, + details: app_metrics::ErrorDetails { + error: error_details, + }, + uuid: uuid::Uuid::now_v7(), + } + } +} diff --git a/hook-consumer/Cargo.toml b/hook-consumer/Cargo.toml index 5ff1eb08eb660..35c64b5e7c7a4 100644 --- a/hook-consumer/Cargo.toml +++ b/hook-consumer/Cargo.toml @@ -10,7 +10,7 @@ envconfig = { workspace = true } futures = "0.3" hook-common = { path = "../hook-common" } http = { version = "0.2" } -reqwest = { version = "0.11" } +reqwest = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } sqlx = { workspace = true } diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 9e4b3b798405e..bf7573fec4058 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -4,12 +4,12 @@ use std::time; use async_std::task; use hook_common::pgqueue::{PgJobError, PgQueue, PgQueueError, PgTransactionJob}; -use hook_common::webhook::{HttpMethod, WebhookJobMetadata, WebhookJobParameters}; +use hook_common::webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}; use http::StatusCode; use reqwest::header; use tokio::sync; -use crate::error::WebhookConsumerError; +use crate::error::{ConsumerError, WebhookError}; /// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. pub struct WebhookConsumer<'p> { @@ -57,8 +57,7 @@ impl<'p> WebhookConsumer<'p> { /// Wait until a job becomes available in our queue. async fn wait_for_job<'a>( &self, - ) -> Result, WebhookConsumerError> - { + ) -> Result, ConsumerError> { loop { if let Some(job) = self.queue.dequeue_tx(&self.name).await? { return Ok(job); @@ -69,7 +68,7 @@ impl<'p> WebhookConsumer<'p> { } /// Run this consumer to continuously process any jobs that become available. - pub async fn run(&self) -> Result<(), WebhookConsumerError> { + pub async fn run(&self) -> Result<(), ConsumerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); loop { @@ -104,7 +103,7 @@ impl<'p> WebhookConsumer<'p> { async fn process_webhook_job( client: reqwest::Client, webhook_job: PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata>, -) -> Result<(), WebhookConsumerError> { +) -> Result<(), ConsumerError> { match send_webhook( client, &webhook_job.job.parameters.method, @@ -118,31 +117,54 @@ async fn process_webhook_job( webhook_job .complete() .await - .map_err(|error| WebhookConsumerError::PgJobError(error.to_string()))?; + .map_err(|error| ConsumerError::PgJobError(error.to_string()))?; + Ok(()) + } + Err(WebhookError::ParseHeadersError(e)) => { + webhook_job + .fail(WebhookJobError::new_parse(&e.to_string())) + .await + .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; Ok(()) } - Err(WebhookConsumerError::RetryableWebhookError { - reason, - retry_after, - }) => match webhook_job.retry(reason.to_string(), retry_after).await { - Ok(_) => Ok(()), - Err(PgJobError::RetryInvalidError { - job: webhook_job, - error: fail_error, - }) => { - webhook_job - .fail(fail_error.to_string()) - .await - .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; - Ok(()) + Err(WebhookError::ParseHttpMethodError(e)) => { + webhook_job + .fail(WebhookJobError::new_parse(&e)) + .await + .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + Err(WebhookError::ParseUrlError(e)) => { + webhook_job + .fail(WebhookJobError::new_parse(&e.to_string())) + .await + .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + Err(WebhookError::RetryableRequestError { error, retry_after }) => { + match webhook_job + .retry(WebhookJobError::from(error), retry_after) + .await + { + Ok(_) => Ok(()), + Err(PgJobError::RetryInvalidError { + job: webhook_job, .. + }) => { + let max_attempts = webhook_job.job.max_attempts; + webhook_job + .fail(WebhookJobError::new_max_attempts(max_attempts)) + .await + .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + Err(job_error) => Err(ConsumerError::PgJobError(job_error.to_string())), } - Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), - }, - Err(error) => { + } + Err(WebhookError::NonRetryableRetryableRequestError(error)) => { webhook_job - .fail(error.to_string()) + .fail(WebhookJobError::from(error)) .await - .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; Ok(()) } } @@ -163,12 +185,12 @@ async fn send_webhook( url: &str, headers: &collections::HashMap, body: String, -) -> Result { +) -> Result<(), WebhookError> { let method: http::Method = method.into(); - let url: reqwest::Url = (url).parse().map_err(WebhookConsumerError::ParseUrlError)?; + let url: reqwest::Url = (url).parse().map_err(WebhookError::ParseUrlError)?; let headers: reqwest::header::HeaderMap = (headers) .try_into() - .map_err(WebhookConsumerError::ParseHeadersError)?; + .map_err(WebhookError::ParseHeadersError)?; let body = reqwest::Body::from(body); let response = client @@ -177,27 +199,28 @@ async fn send_webhook( .body(body) .send() .await - .map_err(|e| WebhookConsumerError::RetryableWebhookError { - reason: e.to_string(), + .map_err(|e| WebhookError::RetryableRequestError { + error: e, retry_after: None, })?; - let status = response.status(); - - if status.is_success() { - Ok(response) - } else if is_retryable_status(status) { - let retry_after = parse_retry_after_header(response.headers()); - - Err(WebhookConsumerError::RetryableWebhookError { - reason: format!("retryable status code {}", status), - retry_after, - }) - } else { - Err(WebhookConsumerError::NonRetryableWebhookError(format!( - "non-retryable status code {}", - status - ))) + match response.error_for_status_ref() { + Ok(_) => Ok(()), + Err(err) => { + if is_retryable_status( + err.status() + .expect("status code is set as error is generated from a response"), + ) { + let retry_after = parse_retry_after_header(response.headers()); + + Err(WebhookError::RetryableRequestError { + error: err, + retry_after, + }) + } else { + Err(WebhookError::NonRetryableRetryableRequestError(err)) + } + } } } diff --git a/hook-consumer/src/error.rs b/hook-consumer/src/error.rs index a19664359caf7..b05d476e22849 100644 --- a/hook-consumer/src/error.rs +++ b/hook-consumer/src/error.rs @@ -3,26 +3,31 @@ use std::time; use hook_common::pgqueue; use thiserror::Error; -/// Enumeration of errors for operations with WebhookConsumer. +/// Enumeration of errors related to webhook job processing in the WebhookConsumer. #[derive(Error, Debug)] -pub enum WebhookConsumerError { - #[error("timed out while waiting for jobs to be available")] - TimeoutError, +pub enum WebhookError { #[error("{0} is not a valid HttpMethod")] ParseHttpMethodError(String), #[error("error parsing webhook headers")] ParseHeadersError(http::Error), #[error("error parsing webhook url")] ParseUrlError(url::ParseError), + #[error("a webhook could not be delivered but it could be retried later: {error}")] + RetryableRequestError { + error: reqwest::Error, + retry_after: Option, + }, + #[error("a webhook could not be delivered and it cannot be retried further: {0}")] + NonRetryableRetryableRequestError(reqwest::Error), +} + +/// Enumeration of errors related to initialization and consumption of webhook jobs. +#[derive(Error, Debug)] +pub enum ConsumerError { + #[error("timed out while waiting for jobs to be available")] + TimeoutError, #[error("an error occurred in the underlying queue")] QueueError(#[from] pgqueue::PgQueueError), #[error("an error occurred in the underlying job")] PgJobError(String), - #[error("a webhook could not be delivered but it could be retried later: {reason}")] - RetryableWebhookError { - reason: String, - retry_after: Option, - }, - #[error("a webhook could not be delivered and it cannot be retried further: {0}")] - NonRetryableWebhookError(String), } diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index bf76503041d3a..bb02526c23b76 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -3,10 +3,10 @@ use envconfig::Envconfig; use hook_common::pgqueue::{PgQueue, RetryPolicy}; use hook_consumer::config::Config; use hook_consumer::consumer::WebhookConsumer; -use hook_consumer::error::WebhookConsumerError; +use hook_consumer::error::ConsumerError; #[tokio::main] -async fn main() -> Result<(), WebhookConsumerError> { +async fn main() -> Result<(), ConsumerError> { let config = Config::init_from_env().expect("Invalid configuration:"); let retry_policy = RetryPolicy::new( From c64c3d054c7f9701dfc170e79e6f77f9f36a995d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Tue, 19 Dec 2023 12:15:08 +0100 Subject: [PATCH 125/249] fix: Return response from send_webhook --- hook-consumer/src/consumer.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index bf7573fec4058..9430126f1f45a 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -185,7 +185,7 @@ async fn send_webhook( url: &str, headers: &collections::HashMap, body: String, -) -> Result<(), WebhookError> { +) -> Result { let method: http::Method = method.into(); let url: reqwest::Url = (url).parse().map_err(WebhookError::ParseUrlError)?; let headers: reqwest::header::HeaderMap = (headers) @@ -204,15 +204,15 @@ async fn send_webhook( retry_after: None, })?; - match response.error_for_status_ref() { - Ok(_) => Ok(()), + let retry_after = parse_retry_after_header(response.headers()); + + match response.error_for_status() { + Ok(response) => Ok(response), Err(err) => { if is_retryable_status( err.status() .expect("status code is set as error is generated from a response"), ) { - let retry_after = parse_retry_after_header(response.headers()); - Err(WebhookError::RetryableRequestError { error: err, retry_after, From d80644b2fa8325dcd121f71656d13a6860f0cb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Tue, 19 Dec 2023 15:02:18 +0100 Subject: [PATCH 126/249] refactor: Re-use underlying error when failing after retry --- hook-common/src/kafka_messages/app_metrics.rs | 2 -- hook-common/src/webhook.rs | 22 ++----------------- hook-consumer/src/consumer.rs | 7 +++--- 3 files changed, 5 insertions(+), 26 deletions(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 327f4b89fb6a6..8964144bf65b6 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -19,7 +19,6 @@ pub enum ErrorType { Connection, HttpStatus(u16), Parse, - MaxAttempts, } #[derive(Serialize, Debug)] @@ -87,7 +86,6 @@ where ErrorType::Timeout => "Timeout".to_owned(), ErrorType::HttpStatus(s) => format!("HTTP Status: {}", s), ErrorType::Parse => "Parse Error".to_owned(), - ErrorType::MaxAttempts => "Maximum attempts exceeded".to_owned(), }; serializer.serialize_str(&error_type) } diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index ea7dfb4724159..d8c174d3f6cd6 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -146,8 +146,8 @@ pub struct WebhookJobError { pub uuid: uuid::Uuid, } -impl From for WebhookJobError { - fn from(error: reqwest::Error) -> Self { +impl From<&reqwest::Error> for WebhookJobError { + fn from(error: &reqwest::Error) -> Self { if error.is_body() || error.is_decode() { WebhookJobError::new_parse(&error.to_string()) } else if error.is_timeout() { @@ -231,22 +231,4 @@ impl WebhookJobError { uuid: uuid::Uuid::now_v7(), } } - - pub fn new_max_attempts(max_attempts: i32) -> Self { - let error_details = app_metrics::Error { - name: "maximum attempts exceeded".to_owned(), - message: Some(format!( - "Exceeded maximum number of attempts ({}) for webhook", - max_attempts - )), - stack: None, - }; - Self { - r#type: app_metrics::ErrorType::MaxAttempts, - details: app_metrics::ErrorDetails { - error: error_details, - }, - uuid: uuid::Uuid::now_v7(), - } - } } diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 9430126f1f45a..8a2ecdc026f39 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -143,16 +143,15 @@ async fn process_webhook_job( } Err(WebhookError::RetryableRequestError { error, retry_after }) => { match webhook_job - .retry(WebhookJobError::from(error), retry_after) + .retry(WebhookJobError::from(&error), retry_after) .await { Ok(_) => Ok(()), Err(PgJobError::RetryInvalidError { job: webhook_job, .. }) => { - let max_attempts = webhook_job.job.max_attempts; webhook_job - .fail(WebhookJobError::new_max_attempts(max_attempts)) + .fail(WebhookJobError::from(&error)) .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; Ok(()) @@ -162,7 +161,7 @@ async fn process_webhook_job( } Err(WebhookError::NonRetryableRetryableRequestError(error)) => { webhook_job - .fail(WebhookJobError::from(error)) + .fail(WebhookJobError::from(&error)) .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; Ok(()) From e4ee369fb24c2f6ee410a8349da0b78953d90e86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Tue, 19 Dec 2023 16:45:58 +0100 Subject: [PATCH 127/249] chore: Update docs --- hook-common/src/webhook.rs | 4 ++++ hook-consumer/src/consumer.rs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index d8c174d3f6cd6..488d52786b125 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -138,6 +138,7 @@ pub struct WebhookJobMetadata { } /// An error originating during a Webhook Job invocation. +/// This is to be serialized to be stored as an error whenever retrying or failing a webhook job. #[derive(Serialize, Debug)] pub struct WebhookJobError { pub r#type: app_metrics::ErrorType, @@ -146,6 +147,9 @@ pub struct WebhookJobError { pub uuid: uuid::Uuid, } +/// Webhook jobs boil down to an HTTP request, so it's useful to have a way to convert from &reqwest::Error. +/// For the convertion we check all possible error types with the associated is_* methods provided by reqwest. +/// Some precision may be lost as our app_metrics::ErrorType does not support the same number of variants. impl From<&reqwest::Error> for WebhookJobError { fn from(error: &reqwest::Error) -> Self { if error.is_body() || error.is_decode() { diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 8a2ecdc026f39..12d4b38b9a56f 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -98,8 +98,8 @@ impl<'p> WebhookConsumer<'p> { /// /// # Arguments /// +/// * `client`: An HTTP client to execute the webhook job request. /// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. -/// * `request_timeout`: A timeout for the HTTP request. async fn process_webhook_job( client: reqwest::Client, webhook_job: PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata>, @@ -173,11 +173,11 @@ async fn process_webhook_job( /// /// # Arguments /// +/// * `client`: An HTTP client to execute the HTTP request. /// * `method`: The HTTP method to use in the HTTP request. /// * `url`: The URL we are targetting with our request. Parsing this URL fail. /// * `headers`: Key, value pairs of HTTP headers in a `std::collections::HashMap`. Can fail if headers are not valid. /// * `body`: The body of the request. Ownership is required. -/// * `timeout`: A timeout for the HTTP request. async fn send_webhook( client: reqwest::Client, method: &HttpMethod, From f4803070a5aca3803bb9d56ec5574341c85f8d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Tue, 19 Dec 2023 17:10:37 +0100 Subject: [PATCH 128/249] refactor: Have Connection Error catch all reqwest errors --- hook-common/src/webhook.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 488d52786b125..2bf8db34f9fd5 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -152,25 +152,18 @@ pub struct WebhookJobError { /// Some precision may be lost as our app_metrics::ErrorType does not support the same number of variants. impl From<&reqwest::Error> for WebhookJobError { fn from(error: &reqwest::Error) -> Self { - if error.is_body() || error.is_decode() { - WebhookJobError::new_parse(&error.to_string()) - } else if error.is_timeout() { + if error.is_timeout() { WebhookJobError::new_timeout(&error.to_string()) } else if error.is_status() { WebhookJobError::new_http_status( error.status().expect("status code is defined").into(), &error.to_string(), ) - } else if error.is_connect() - || error.is_builder() - || error.is_request() - || error.is_redirect() - { - // Builder errors seem to be related to unable to setup TLS, so I'm bundling them in connection. - WebhookJobError::new_connection(&error.to_string()) } else { - // We can't match on Kind as some types do not have an associated variant in Kind (e.g. Timeout). - unreachable!("We have covered all reqwest::Error types.") + // Catch all other errors as `app_metrics::ErrorType::Connection` errors. + // Not all of `reqwest::Error` may strictly be connection errors, so our supported error types may need an extension + // depending on how strict error reporting has to be. + WebhookJobError::new_connection(&error.to_string()) } } } From f9b32ae4c04add0281f7f0d5390a61dc8bcdfc27 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 19 Dec 2023 09:53:20 -0700 Subject: [PATCH 129/249] Implement Webhook cleanup --- hook-common/src/kafka_messages/app_metrics.rs | 66 ++-- hook-common/src/kafka_messages/mod.rs | 10 + hook-common/src/webhook.rs | 10 +- hook-janitor/src/cleanup.rs | 2 - hook-janitor/src/config.rs | 3 - hook-janitor/src/main.rs | 2 - hook-janitor/src/webhooks.rs | 328 +++++++++++++++++- 7 files changed, 361 insertions(+), 60 deletions(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 8964144bf65b6..439664342fa35 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -1,10 +1,10 @@ use chrono::{DateTime, Utc}; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer}; use uuid::Uuid; -use super::{serialize_datetime, serialize_uuid}; +use super::{serialize_datetime, serialize_optional_uuid}; -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub enum AppMetricCategory { ProcessEvent, OnEvent, @@ -13,7 +13,7 @@ pub enum AppMetricCategory { ComposeWebhook, } -#[derive(Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub enum ErrorType { Timeout, Connection, @@ -21,29 +21,23 @@ pub enum ErrorType { Parse, } -#[derive(Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct ErrorDetails { pub error: Error, - // TODO: The plugin-server sends the entire raw event with errors. In order to do this, we'll - // have to pass the entire event when we enqueue items, and store it in the Parameters JSONB - // column. We should see if it's possible to work around this before we commit to it. - // - // event: Value, } -#[derive(Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Error { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, - // TODO: Realistically, it doesn't seem likely that we'll generate Rust stack traces and put - // them here. I think this was more useful in plugin-server when the stack could come from - // plugin code. + // This field will only be useful if we start running plugins in Rust (via a WASM runtime or + // something) and want to provide the user with stack traces like we do for TypeScript plugins. #[serde(skip_serializing_if = "Option::is_none")] pub stack: Option, } -#[derive(Serialize)] +#[derive(Serialize, Debug)] pub struct AppMetric { #[serde(serialize_with = "serialize_datetime")] pub timestamp: DateTime, @@ -56,11 +50,18 @@ pub struct AppMetric { pub successes: u32, pub successes_on_retry: u32, pub failures: u32, - #[serde(serialize_with = "serialize_uuid")] - pub error_uuid: Uuid, - #[serde(serialize_with = "serialize_error_type")] - pub error_type: ErrorType, - pub error_details: Error, + #[serde( + serialize_with = "serialize_optional_uuid", + skip_serializing_if = "Option::is_none" + )] + pub error_uuid: Option, + #[serde( + serialize_with = "serialize_error_type", + skip_serializing_if = "Option::is_none" + )] + pub error_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_details: Option, } fn serialize_category(category: &AppMetricCategory, serializer: S) -> Result @@ -77,10 +78,15 @@ where serializer.serialize_str(category_str) } -fn serialize_error_type(error_type: &ErrorType, serializer: S) -> Result +fn serialize_error_type(error_type: &Option, serializer: S) -> Result where S: Serializer, { + let error_type = match error_type { + Some(error_type) => error_type, + None => return serializer.serialize_none(), + }; + let error_type = match error_type { ErrorType::Connection => "Connection Error".to_owned(), ErrorType::Timeout => "Timeout".to_owned(), @@ -107,18 +113,20 @@ mod tests { successes: 10, successes_on_retry: 0, failures: 2, - error_uuid: Uuid::parse_str("550e8400-e29b-41d4-a716-446655447777").unwrap(), - error_type: ErrorType::Connection, - error_details: Error { - name: "FooError".to_owned(), - message: Some("Error Message".to_owned()), - stack: None, - }, + error_uuid: Some(Uuid::parse_str("550e8400-e29b-41d4-a716-446655447777").unwrap()), + error_type: Some(ErrorType::Connection), + error_details: Some(ErrorDetails { + error: Error { + name: "FooError".to_owned(), + message: Some("Error Message".to_owned()), + stack: None, + }, + }), }; let serialized_json = serde_json::to_string(&app_metric).unwrap(); - let expected_json = r#"{"timestamp":"2023-12-14 12:02:00","team_id":123,"plugin_config_id":456,"category":"webhook","successes":10,"successes_on_retry":0,"failures":2,"error_uuid":"550e8400-e29b-41d4-a716-446655447777","error_type":"Connection Error","error_details":{"name":"FooError","message":"Error Message"}}"#; + let expected_json = r#"{"timestamp":"2023-12-14 12:02:00","team_id":123,"plugin_config_id":456,"category":"webhook","successes":10,"successes_on_retry":0,"failures":2,"error_uuid":"550e8400-e29b-41d4-a716-446655447777","error_type":"Connection Error","error_details":{"error":{"name":"FooError","message":"Error Message"}}}"#; assert_eq!(serialized_json, expected_json); } diff --git a/hook-common/src/kafka_messages/mod.rs b/hook-common/src/kafka_messages/mod.rs index a29e2193d327b..72b49e1e45059 100644 --- a/hook-common/src/kafka_messages/mod.rs +++ b/hook-common/src/kafka_messages/mod.rs @@ -12,6 +12,16 @@ where serializer.serialize_str(&uuid.to_string()) } +pub fn serialize_optional_uuid(uuid: &Option, serializer: S) -> Result +where + S: Serializer, +{ + match uuid { + Some(uuid) => serializer.serialize_str(&uuid.to_string()), + None => serializer.serialize_none(), + } +} + pub fn serialize_datetime(datetime: &DateTime, serializer: S) -> Result where S: Serializer, diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 2bf8db34f9fd5..d320ce0c4aac7 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use serde::{de::Visitor, Deserialize, Serialize}; -use crate::kafka_messages::{app_metrics, serialize_uuid}; +use crate::kafka_messages::app_metrics; use crate::pgqueue::PgQueueError; /// Supported HTTP methods for webhooks. @@ -139,12 +139,10 @@ pub struct WebhookJobMetadata { /// An error originating during a Webhook Job invocation. /// This is to be serialized to be stored as an error whenever retrying or failing a webhook job. -#[derive(Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct WebhookJobError { pub r#type: app_metrics::ErrorType, pub details: app_metrics::ErrorDetails, - #[serde(serialize_with = "serialize_uuid")] - pub uuid: uuid::Uuid, } /// Webhook jobs boil down to an HTTP request, so it's useful to have a way to convert from &reqwest::Error. @@ -180,7 +178,6 @@ impl WebhookJobError { details: app_metrics::ErrorDetails { error: error_details, }, - uuid: uuid::Uuid::now_v7(), } } @@ -195,7 +192,6 @@ impl WebhookJobError { details: app_metrics::ErrorDetails { error: error_details, }, - uuid: uuid::Uuid::now_v7(), } } @@ -210,7 +206,6 @@ impl WebhookJobError { details: app_metrics::ErrorDetails { error: error_details, }, - uuid: uuid::Uuid::now_v7(), } } @@ -225,7 +220,6 @@ impl WebhookJobError { details: app_metrics::ErrorDetails { error: error_details, }, - uuid: uuid::Uuid::now_v7(), } } } diff --git a/hook-janitor/src/cleanup.rs b/hook-janitor/src/cleanup.rs index e6e91e0be922f..82b91303a721b 100644 --- a/hook-janitor/src/cleanup.rs +++ b/hook-janitor/src/cleanup.rs @@ -5,8 +5,6 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum CleanerError { - #[error("pool creation failed with: {error}")] - PoolCreationError { error: sqlx::Error }, #[error("invalid cleaner mode")] InvalidCleanerMode, } diff --git a/hook-janitor/src/config.rs b/hook-janitor/src/config.rs index 89621a2354617..c1efb85d38ee5 100644 --- a/hook-janitor/src/config.rs +++ b/hook-janitor/src/config.rs @@ -20,9 +20,6 @@ pub struct Config { #[envconfig(default = "30")] pub cleanup_interval_secs: u64, - #[envconfig(default = "10000")] - pub cleanup_batch_size: u32, - // The cleanup task needs to have special knowledge of the queue it's cleaning up. This is so it // can do things like flush the proper app_metrics or plugin_log_entries, and so it knows what // to expect in the job's payload JSONB column. diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs index 46223aa7aafb3..5de3ec4d93978 100644 --- a/hook-janitor/src/main.rs +++ b/hook-janitor/src/main.rs @@ -57,10 +57,8 @@ async fn main() { &config.queue_name, &config.table_name, &config.database_url, - config.cleanup_batch_size, kafka_producer, config.kafka.app_metrics_topic.to_owned(), - config.kafka.plugin_log_entries_topic.to_owned(), ) .expect("unable to create webhook cleaner"), ) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index e8895f1ba0781..6b10ce0d322dd 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -1,20 +1,87 @@ -use async_trait::async_trait; +use std::time::Duration; -use rdkafka::producer::FutureProducer; -use sqlx::postgres::{PgPool, PgPoolOptions}; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use futures::future::join_all; +use hook_common::webhook::WebhookJobError; +use rdkafka::error::KafkaError; +use rdkafka::producer::{FutureProducer, FutureRecord}; +use serde_json::error::Error as SerdeError; +use sqlx::postgres::{PgPool, PgPoolOptions, Postgres}; +use sqlx::types::{chrono, Uuid}; +use sqlx::Transaction; +use thiserror::Error; +use tracing::{debug, error}; -use crate::cleanup::{Cleaner, CleanerError}; +use crate::cleanup::Cleaner; use crate::kafka_producer::KafkaContext; -#[allow(dead_code)] +use hook_common::kafka_messages::app_metrics::{AppMetric, AppMetricCategory}; + +#[derive(Error, Debug)] +pub enum WebhookCleanerError { + #[error("failed to create postgres pool: {error}")] + PoolCreationError { error: sqlx::Error }, + #[error("failed to acquire conn and start txn: {error}")] + StartTxnError { error: sqlx::Error }, + #[error("failed to get completed rows: {error}")] + GetCompletedRowsError { error: sqlx::Error }, + #[error("failed to get failed rows: {error}")] + GetFailedRowsError { error: sqlx::Error }, + #[error("failed to serialize rows: {error}")] + SerializeRowsError { error: SerdeError }, + #[error("failed to produce to kafka: {error}")] + KafkaProduceError { error: KafkaError }, + #[error("failed to produce to kafka (timeout)")] + KafkaProduceCanceled, + #[error("failed to delete rows: {error}")] + DeleteRowsError { error: sqlx::Error }, + #[error("failed to commit txn: {error}")] + CommitTxnError { error: sqlx::Error }, +} + +type Result = std::result::Result; + pub struct WebhookCleaner { queue_name: String, table_name: String, pg_pool: PgPool, - batch_size: u32, kafka_producer: FutureProducer, app_metrics_topic: String, - plugin_log_entries_topic: String, +} + +#[derive(sqlx::FromRow, Debug)] +struct CompletedRow { + // App Metrics truncates/aggregates rows on the hour, so we take advantage of that to GROUP BY + // and aggregate to select fewer rows. + hour: DateTime, + // A note about the `try_from`s: Postgres returns all of those types as `bigint` (i64), but + // we know their true sizes, and so we can convert them to the correct types here. If this + // ever fails then something has gone wrong. + #[sqlx(try_from = "i64")] + team_id: u32, + #[sqlx(try_from = "i64")] + plugin_config_id: u32, + #[sqlx(try_from = "i64")] + successes: u32, +} + +#[derive(sqlx::FromRow, Debug)] +struct FailedRow { + // App Metrics truncates/aggregates rows on the hour, so we take advantage of that to GROUP BY + // and aggregate to select fewer rows. + hour: DateTime, + // A note about the `try_from`s: Postgres returns all of those types as `bigint` (i64), but + // we know their true sizes, and so we can convert them to the correct types here. If this + // ever fails then something has gone wrong. + #[sqlx(try_from = "i64")] + team_id: u32, + #[sqlx(try_from = "i64")] + plugin_config_id: u32, + #[sqlx(json)] + last_error: WebhookJobError, + #[sqlx(try_from = "i64")] + failures: u32, } impl WebhookCleaner { @@ -22,34 +89,263 @@ impl WebhookCleaner { queue_name: &str, table_name: &str, database_url: &str, - batch_size: u32, kafka_producer: FutureProducer, app_metrics_topic: String, - plugin_log_entries_topic: String, - ) -> Result { + ) -> Result { let queue_name = queue_name.to_owned(); let table_name = table_name.to_owned(); let pg_pool = PgPoolOptions::new() + .acquire_timeout(Duration::from_secs(10)) .connect_lazy(database_url) - .map_err(|error| CleanerError::PoolCreationError { error })?; + .map_err(|error| WebhookCleanerError::PoolCreationError { error })?; Ok(Self { queue_name, table_name, pg_pool, - batch_size, kafka_producer, app_metrics_topic, - plugin_log_entries_topic, }) } + + async fn start_serializable_txn(&self) -> Result> { + let mut tx = self + .pg_pool + .begin() + .await + .map_err(|e| WebhookCleanerError::StartTxnError { error: e })?; + + // We use serializable isolation so that we observe a snapshot of the DB at the time we + // start the cleanup process. This prevents us from accidentally deleting rows that are + // added (or become 'completed' or 'failed') after we start the cleanup process. + sqlx::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") + .execute(&mut *tx) + .await + .map_err(|e| WebhookCleanerError::StartTxnError { error: e })?; + + Ok(tx) + } + + async fn get_completed_rows( + &self, + tx: &mut Transaction<'_, Postgres>, + ) -> Result> { + let base_query = format!( + r#" + SELECT DATE_TRUNC('hour', finished_at) AS hour, + metadata->>'team_id' AS team_id, + metadata->>'plugin_config_id' AS plugin_config_id, + count(*) as successes + FROM {0} + WHERE status = 'completed' + AND queue = $1 + GROUP BY hour, team_id, plugin_config_id + ORDER BY hour, team_id, plugin_config_id; + "#, + self.table_name + ); + + let rows = sqlx::query_as::<_, CompletedRow>(&base_query) + .bind(&self.queue_name) + .fetch_all(&mut **tx) + .await + .map_err(|e| WebhookCleanerError::GetCompletedRowsError { error: e })?; + + Ok(rows) + } + + async fn serialize_completed_rows( + &self, + completed_rows: Vec, + ) -> Result> { + let mut payloads = Vec::new(); + + for row in completed_rows { + let app_metric = AppMetric { + timestamp: row.hour, + team_id: row.team_id, + plugin_config_id: row.plugin_config_id, + job_id: None, + category: AppMetricCategory::Webhook, + successes: row.successes, + successes_on_retry: 0, + failures: 0, + error_uuid: None, + error_type: None, + error_details: None, + }; + + let payload = serde_json::to_string(&app_metric) + .map_err(|e| WebhookCleanerError::SerializeRowsError { error: e })?; + + payloads.push(payload) + } + + Ok(payloads) + } + + async fn get_failed_rows(&self, tx: &mut Transaction<'_, Postgres>) -> Result> { + let base_query = format!( + r#" + SELECT DATE_TRUNC('hour', finished_at) AS hour, + metadata->>'team_id' AS team_id, + metadata->>'plugin_config_id' AS plugin_config_id, + errors[-1] AS last_error, + count(*) as failures + FROM {0} + WHERE status = 'failed' + AND queue = $1 + GROUP BY hour, team_id, plugin_config_id, last_error + ORDER BY hour, team_id, plugin_config_id, last_error; + "#, + self.table_name + ); + + let rows = sqlx::query_as::<_, FailedRow>(&base_query) + .bind(&self.queue_name) + .fetch_all(&mut **tx) + .await + .map_err(|e| WebhookCleanerError::GetFailedRowsError { error: e })?; + + Ok(rows) + } + + async fn serialize_failed_rows(&self, failed_rows: Vec) -> Result> { + let mut payloads = Vec::new(); + + for row in failed_rows { + let app_metric = AppMetric { + timestamp: row.hour, + team_id: row.team_id, + plugin_config_id: row.plugin_config_id, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 0, + successes_on_retry: 0, + failures: row.failures, + error_uuid: Some(Uuid::now_v7()), + error_type: Some(row.last_error.r#type), + error_details: Some(row.last_error.details), + }; + + let payload = serde_json::to_string(&app_metric) + .map_err(|e| WebhookCleanerError::SerializeRowsError { error: e })?; + + payloads.push(payload) + } + + Ok(payloads) + } + + async fn send_messages_to_kafka(&self, payloads: Vec) -> Result<()> { + let mut delivery_futures = Vec::new(); + + for payload in payloads { + match self.kafka_producer.send_result(FutureRecord { + topic: self.app_metrics_topic.as_str(), + payload: Some(&payload), + partition: None, + key: None::<&str>, + timestamp: None, + headers: None, + }) { + Ok(future) => delivery_futures.push(future), + Err((error, _)) => return Err(WebhookCleanerError::KafkaProduceError { error }), + } + } + + for result in join_all(delivery_futures).await { + match result { + Ok(Ok(_)) => {} + Ok(Err((error, _))) => { + return Err(WebhookCleanerError::KafkaProduceError { error }) + } + Err(_) => { + // Cancelled due to timeout while retrying + return Err(WebhookCleanerError::KafkaProduceCanceled); + } + } + } + + Ok(()) + } + + async fn delete_observed_rows(&self, tx: &mut Transaction<'_, Postgres>) -> Result { + // This DELETE is only safe because we are in serializable isolation mode, see the note + // in `start_serializable_txn`. + let base_query = format!( + r#" + DELETE FROM {0} + WHERE status IN ('failed', 'completed') + AND queue = $1; + "#, + self.table_name + ); + + let result = sqlx::query(&base_query) + .bind(&self.queue_name) + .execute(&mut **tx) + .await + .map_err(|e| WebhookCleanerError::DeleteRowsError { error: e })?; + + Ok(result.rows_affected()) + } + + async fn commit_txn(&self, tx: Transaction<'_, Postgres>) -> Result<()> { + tx.commit() + .await + .map_err(|e| WebhookCleanerError::CommitTxnError { error: e })?; + + Ok(()) + } + + async fn cleanup_impl(&self) -> Result<()> { + debug!("WebhookCleaner starting cleanup"); + + // Note that we select all completed and failed rows without any pagination at the moment. + // We aggregrate as much as possible with GROUP BY, truncating the timestamp down to the + // hour just like App Metrics does. A completed row is 24 bytes (and aggregates an entire + // hour per `plugin_config_id`), and a failed row is 104 + the message length (and + // aggregates an entire hour per `plugin_config_id` per `error`), so we can fit a lot of + // rows in memory. It seems unlikely we'll need to paginate, but that can be added in the + // future if necessary. + + let mut tx = self.start_serializable_txn().await?; + let completed_rows = self.get_completed_rows(&mut tx).await?; + let mut payloads = self.serialize_completed_rows(completed_rows).await?; + let failed_rows = self.get_failed_rows(&mut tx).await?; + let mut failed_payloads = self.serialize_failed_rows(failed_rows).await?; + payloads.append(&mut failed_payloads); + let mut rows_deleted: u64 = 0; + if !payloads.is_empty() { + self.send_messages_to_kafka(payloads).await?; + rows_deleted = self.delete_observed_rows(&mut tx).await?; + self.commit_txn(tx).await?; + } + + debug!( + "WebhookCleaner finished cleanup, deleted {} rows", + rows_deleted + ); + + Ok(()) + } } #[async_trait] impl Cleaner for WebhookCleaner { async fn cleanup(&self) { - // TODO: collect stats on completed/failed rows - // TODO: push metrics about those rows into `app_metrics` - // TODO: delete those completed/failed rows + match self.cleanup_impl().await { + Ok(_) => {} + Err(error) => { + error!(error = ?error, "WebhookCleaner::cleanup failed"); + } + } } } + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn test() {} +} From 72aa509cccef0a6df87e5e69a63ce4e9db020d95 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 19 Dec 2023 09:59:14 -0700 Subject: [PATCH 130/249] Drop unnecessary asyncs --- hook-janitor/src/webhooks.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 6b10ce0d322dd..7e6f540837844 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -154,10 +154,7 @@ impl WebhookCleaner { Ok(rows) } - async fn serialize_completed_rows( - &self, - completed_rows: Vec, - ) -> Result> { + fn serialize_completed_rows(&self, completed_rows: Vec) -> Result> { let mut payloads = Vec::new(); for row in completed_rows { @@ -210,7 +207,7 @@ impl WebhookCleaner { Ok(rows) } - async fn serialize_failed_rows(&self, failed_rows: Vec) -> Result> { + fn serialize_failed_rows(&self, failed_rows: Vec) -> Result> { let mut payloads = Vec::new(); for row in failed_rows { @@ -312,9 +309,9 @@ impl WebhookCleaner { let mut tx = self.start_serializable_txn().await?; let completed_rows = self.get_completed_rows(&mut tx).await?; - let mut payloads = self.serialize_completed_rows(completed_rows).await?; + let mut payloads = self.serialize_completed_rows(completed_rows)?; let failed_rows = self.get_failed_rows(&mut tx).await?; - let mut failed_payloads = self.serialize_failed_rows(failed_rows).await?; + let mut failed_payloads = self.serialize_failed_rows(failed_rows)?; payloads.append(&mut failed_payloads); let mut rows_deleted: u64 = 0; if !payloads.is_empty() { From 4fe10877dc9a0345f7904bda99a32bd70bac773d Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 19 Dec 2023 12:39:25 -0700 Subject: [PATCH 131/249] Make WebhookJobMetadata fields required --- hook-common/src/webhook.rs | 6 +++--- hook-consumer/src/consumer.rs | 6 +++--- hook-producer/src/handlers/webhook.rs | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index d320ce0c4aac7..9a21b83cd713b 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -132,9 +132,9 @@ pub struct WebhookJobParameters { /// These should be set if the Webhook is associated with a plugin `composeWebhook` invocation. #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct WebhookJobMetadata { - pub team_id: Option, - pub plugin_id: Option, - pub plugin_config_id: Option, + pub team_id: u32, + pub plugin_id: u32, + pub plugin_config_id: u32, } /// An error originating during a Webhook Job invocation. diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 12d4b38b9a56f..633381a140def 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -339,9 +339,9 @@ mod tests { url: "localhost".to_owned(), }; let webhook_job_metadata = WebhookJobMetadata { - team_id: None, - plugin_id: None, - plugin_config_id: None, + team_id: 1, + plugin_id: 2, + plugin_config_id: 3, }; // enqueue takes ownership of the job enqueued to avoid bugs that can cause duplicate jobs. // Normally, a separate application would be enqueueing jobs for us to consume, so no ownership diff --git a/hook-producer/src/handlers/webhook.rs b/hook-producer/src/handlers/webhook.rs index 394732094a3c6..18aebf39ce991 100644 --- a/hook-producer/src/handlers/webhook.rs +++ b/hook-producer/src/handlers/webhook.rs @@ -146,9 +146,9 @@ mod tests { body: r#"{"a": "b"}"#.to_owned(), }, metadata: WebhookJobMetadata { - team_id: Some(1), - plugin_id: Some(2), - plugin_config_id: Some(3), + team_id: 1, + plugin_id: 2, + plugin_config_id: 3, }, max_attempts: 1, }) @@ -193,9 +193,9 @@ mod tests { body: r#"{"a": "b"}"#.to_owned(), }, metadata: WebhookJobMetadata { - team_id: Some(1), - plugin_id: Some(2), - plugin_config_id: Some(3), + team_id: 1, + plugin_id: 2, + plugin_config_id: 3, }, max_attempts: 1, }) @@ -296,9 +296,9 @@ mod tests { body: long_string.to_string(), }, metadata: WebhookJobMetadata { - team_id: Some(1), - plugin_id: Some(2), - plugin_config_id: Some(3), + team_id: 1, + plugin_id: 2, + plugin_config_id: 3, }, max_attempts: 1, }) From 9ad59d24504e58d13ba35503d1cd2ad1d46d26e3 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 19 Dec 2023 13:28:50 -0700 Subject: [PATCH 132/249] Switch to sqlx::test for per-test DBs and fixtures --- .env | 1 + Cargo.toml | 7 ++- hook-common/src/pgqueue.rs | 65 +++++++++++++-------- hook-consumer/src/consumer.rs | 13 +++-- hook-producer/src/handlers/app.rs | 17 +++--- hook-producer/src/handlers/webhook.rs | 83 +++++++++++---------------- 6 files changed, 92 insertions(+), 94 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000000000..43eda2a13040b --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database diff --git a/Cargo.toml b/Cargo.toml index 0a0a2f578560b..2481c1dfbbefa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,12 +21,13 @@ serde = { version = "1.0" } serde_derive = { version = "1.0" } serde_json = { version = "1.0" } sqlx = { version = "0.7", features = [ + "chrono", + "json", + "migrate", + "postgres", "runtime-tokio", "tls-native-tls", - "postgres", "uuid", - "json", - "chrono", ] } thiserror = { version = "1.0" } tokio = { version = "1.34.0", features = ["full"] } diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 5288ade106f47..8b36b6d5fb167 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -590,6 +590,23 @@ impl PgQueue { }) } + pub async fn new_from_pool( + queue_name: &str, + table_name: &str, + pool: PgPool, + retry_policy: RetryPolicy, + ) -> PgQueueResult { + let name = queue_name.to_owned(); + let table = table_name.to_owned(); + + Ok(Self { + name, + pool, + retry_policy, + table, + }) + } + /// Dequeue a Job from this PgQueue to work on it. pub async fn dequeue< J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, @@ -827,18 +844,18 @@ mod tests { "https://myhost/endpoint".to_owned() } - #[tokio::test] - async fn test_can_dequeue_job() { + #[sqlx::test(migrations = "../migrations")] + async fn test_can_dequeue_job(db: PgPool) { let job_target = job_target(); let job_parameters = JobParameters::default(); let job_metadata = JobMetadata::default(); let worker_id = worker_id(); let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); - let queue = PgQueue::new( + let queue = PgQueue::new_from_pool( "test_can_dequeue_job", "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", + db, RetryPolicy::default(), ) .await @@ -861,13 +878,13 @@ mod tests { assert_eq!(pg_job.job.target, job_target); } - #[tokio::test] - async fn test_dequeue_returns_none_on_no_jobs() { + #[sqlx::test(migrations = "../migrations")] + async fn test_dequeue_returns_none_on_no_jobs(db: PgPool) { let worker_id = worker_id(); - let queue = PgQueue::new( + let queue = PgQueue::new_from_pool( "test_dequeue_returns_none_on_no_jobs", "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", + db, RetryPolicy::default(), ) .await @@ -881,18 +898,18 @@ mod tests { assert!(pg_job.is_none()); } - #[tokio::test] - async fn test_can_dequeue_tx_job() { + #[sqlx::test(migrations = "../migrations")] + async fn test_can_dequeue_tx_job(db: PgPool) { let job_target = job_target(); let job_metadata = JobMetadata::default(); let job_parameters = JobParameters::default(); let worker_id = worker_id(); let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); - let queue = PgQueue::new( + let queue = PgQueue::new_from_pool( "test_can_dequeue_tx_job", "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", + db, RetryPolicy::default(), ) .await @@ -916,13 +933,13 @@ mod tests { assert_eq!(tx_job.job.target, job_target); } - #[tokio::test] - async fn test_dequeue_tx_returns_none_on_no_jobs() { + #[sqlx::test(migrations = "../migrations")] + async fn test_dequeue_tx_returns_none_on_no_jobs(db: PgPool) { let worker_id = worker_id(); - let queue = PgQueue::new( + let queue = PgQueue::new_from_pool( "test_dequeue_tx_returns_none_on_no_jobs", "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", + db, RetryPolicy::default(), ) .await @@ -936,8 +953,8 @@ mod tests { assert!(tx_job.is_none()); } - #[tokio::test] - async fn test_can_retry_job_with_remaining_attempts() { + #[sqlx::test(migrations = "../migrations")] + async fn test_can_retry_job_with_remaining_attempts(db: PgPool) { let job_target = job_target(); let job_parameters = JobParameters::default(); let job_metadata = JobMetadata::default(); @@ -949,10 +966,10 @@ mod tests { maximum_interval: None, }; - let queue = PgQueue::new( + let queue = PgQueue::new_from_pool( "test_can_retry_job_with_remaining_attempts", "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", + db, retry_policy, ) .await @@ -986,9 +1003,9 @@ mod tests { assert_eq!(retried_job.job.target, job_target); } - #[tokio::test] + #[sqlx::test(migrations = "../migrations")] #[should_panic(expected = "failed to retry job")] - async fn test_cannot_retry_job_without_remaining_attempts() { + async fn test_cannot_retry_job_without_remaining_attempts(db: PgPool) { let job_target = job_target(); let job_parameters = JobParameters::default(); let job_metadata = JobMetadata::default(); @@ -1000,10 +1017,10 @@ mod tests { maximum_interval: None, }; - let queue = PgQueue::new( + let queue = PgQueue::new_from_pool( "test_cannot_retry_job_without_remaining_attempts", "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", + db, retry_policy, ) .await diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 633381a140def..d17578b5afe55 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -272,6 +272,8 @@ mod tests { // See: https://github.com/rust-lang/rust/issues/46379. #[allow(unused_imports)] use hook_common::pgqueue::{JobStatus, NewJob, RetryPolicy}; + #[allow(unused_imports)] + use sqlx::PgPool; /// Use process id as a worker id for tests. #[allow(dead_code)] @@ -322,13 +324,12 @@ mod tests { assert_eq!(duration, None); } - #[tokio::test] - async fn test_wait_for_job() { + #[sqlx::test(migrations = "../migrations")] + async fn test_wait_for_job(db: PgPool) { let worker_id = worker_id(); let queue_name = "test_wait_for_job".to_string(); let table_name = "job_queue".to_string(); - let db_url = "postgres://posthog:posthog@localhost:15432/test_database".to_string(); - let queue = PgQueue::new(&queue_name, &table_name, &db_url, RetryPolicy::default()) + let queue = PgQueue::new_from_pool(&queue_name, &table_name, db, RetryPolicy::default()) .await .expect("failed to connect to PG"); @@ -385,8 +386,8 @@ mod tests { .expect("job not successfully completed"); } - #[tokio::test] - async fn test_send_webhook() { + #[sqlx::test(migrations = "../migrations")] + async fn test_send_webhook(_: PgPool) { let method = HttpMethod::POST; let url = "http://localhost:18081/echo"; let headers = collections::HashMap::new(); diff --git a/hook-producer/src/handlers/app.rs b/hook-producer/src/handlers/app.rs index 1666676fee1bb..c3309dea9efb3 100644 --- a/hook-producer/src/handlers/app.rs +++ b/hook-producer/src/handlers/app.rs @@ -33,18 +33,15 @@ mod tests { }; use hook_common::pgqueue::{PgQueue, RetryPolicy}; use http_body_util::BodyExt; // for `collect` + use sqlx::PgPool; use tower::ServiceExt; // for `call`, `oneshot`, and `ready` - #[tokio::test] - async fn index() { - let pg_queue = PgQueue::new( - "test_index", - "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), - ) - .await - .expect("failed to construct pg_queue"); + #[sqlx::test(migrations = "../migrations")] + async fn index(db: PgPool) { + let pg_queue = + PgQueue::new_from_pool("test_index", "job_queue", db, RetryPolicy::default()) + .await + .expect("failed to construct pg_queue"); let app = app(pg_queue, None); diff --git a/hook-producer/src/handlers/webhook.rs b/hook-producer/src/handlers/webhook.rs index 18aebf39ce991..e2864e8aa0d43 100644 --- a/hook-producer/src/handlers/webhook.rs +++ b/hook-producer/src/handlers/webhook.rs @@ -110,22 +110,19 @@ mod tests { }; use hook_common::pgqueue::{PgQueue, RetryPolicy}; use hook_common::webhook::{HttpMethod, WebhookJobParameters}; - use http_body_util::BodyExt; // for `collect` + use http_body_util::BodyExt; + use sqlx::PgPool; // for `collect` use std::collections; use tower::ServiceExt; // for `call`, `oneshot`, and `ready` use crate::handlers::app; - #[tokio::test] - async fn webhook_success() { - let pg_queue = PgQueue::new( - "test_index", - "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), - ) - .await - .expect("failed to construct pg_queue"); + #[sqlx::test(migrations = "../migrations")] + async fn webhook_success(db: PgPool) { + let pg_queue = + PgQueue::new_from_pool("test_index", "job_queue", db, RetryPolicy::default()) + .await + .expect("failed to construct pg_queue"); let app = app(pg_queue, None); @@ -165,16 +162,12 @@ mod tests { assert_eq!(&body[..], b"{}"); } - #[tokio::test] - async fn webhook_bad_url() { - let pg_queue = PgQueue::new( - "test_index", - "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), - ) - .await - .expect("failed to construct pg_queue"); + #[sqlx::test(migrations = "../migrations")] + async fn webhook_bad_url(db: PgPool) { + let pg_queue = + PgQueue::new_from_pool("test_index", "job_queue", db, RetryPolicy::default()) + .await + .expect("failed to construct pg_queue"); let app = app(pg_queue, None); @@ -209,16 +202,12 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } - #[tokio::test] - async fn webhook_payload_missing_fields() { - let pg_queue = PgQueue::new( - "test_index", - "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), - ) - .await - .expect("failed to construct pg_queue"); + #[sqlx::test(migrations = "../migrations")] + async fn webhook_payload_missing_fields(db: PgPool) { + let pg_queue = + PgQueue::new_from_pool("test_index", "job_queue", db, RetryPolicy::default()) + .await + .expect("failed to construct pg_queue"); let app = app(pg_queue, None); @@ -237,16 +226,12 @@ mod tests { assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); } - #[tokio::test] - async fn webhook_payload_not_json() { - let pg_queue = PgQueue::new( - "test_index", - "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), - ) - .await - .expect("failed to construct pg_queue"); + #[sqlx::test(migrations = "../migrations")] + async fn webhook_payload_not_json(db: PgPool) { + let pg_queue = + PgQueue::new_from_pool("test_index", "job_queue", db, RetryPolicy::default()) + .await + .expect("failed to construct pg_queue"); let app = app(pg_queue, None); @@ -265,16 +250,12 @@ mod tests { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } - #[tokio::test] - async fn webhook_payload_body_too_large() { - let pg_queue = PgQueue::new( - "test_index", - "job_queue", - "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), - ) - .await - .expect("failed to construct pg_queue"); + #[sqlx::test(migrations = "../migrations")] + async fn webhook_payload_body_too_large(db: PgPool) { + let pg_queue = + PgQueue::new_from_pool("test_index", "job_queue", db, RetryPolicy::default()) + .await + .expect("failed to construct pg_queue"); let app = app(pg_queue, None); From 9359e7c846152aec9a43cf0c2734fabb7a8a9f0f Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 19 Dec 2023 15:55:21 -0700 Subject: [PATCH 133/249] Add tests, fix some bugs found by said tests --- Cargo.lock | 1 + Cargo.toml | 2 +- hook-janitor/src/fixtures/webhook_cleanup.sql | 81 ++++++++++ hook-janitor/src/webhooks.rs | 140 +++++++++++++++--- 4 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 hook-janitor/src/fixtures/webhook_cleanup.sql diff --git a/Cargo.lock b/Cargo.lock index ba9f4bb10b67d..fbfcc50d98ee8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2009,6 +2009,7 @@ dependencies = [ "serde_json", "slab", "tokio", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2481c1dfbbefa..a29806a2fa730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ http = { version = "0.2" } http-body-util = "0.1.0" metrics = "0.21.1" metrics-exporter-prometheus = "0.12.1" -rdkafka = { version = "0.35.0", features = ["cmake-build", "ssl"] } +rdkafka = { version = "0.35.0", features = ["cmake-build", "ssl", "tracing"] } reqwest = { version = "0.11" } regex = "1.10.2" serde = { version = "1.0" } diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql new file mode 100644 index 0000000000000..6f29d302b8462 --- /dev/null +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -0,0 +1,81 @@ +INSERT INTO + job_queue ( + errors, + metadata, + finished_at, + parameters, + queue, + status, + target + ) +VALUES + -- team:1, plugin_config:2, completed in hour 20 + ( + NULL, + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'completed', + 'https://myhost/endpoint' + ), + -- another team:1, plugin_config:2, completed in hour 20 + ( + NULL, + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'completed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, completed in hour 21 (different hour) + ( + NULL, + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 21:01:18.799371+00', + '{}', + 'webhooks', + 'completed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:3, completed in hour 20 (different plugin_config) + ( + NULL, + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', + '2023-12-19 20:01:18.80335+00', + '{}', + 'webhooks', + 'completed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, completed but in a different queue + ( + NULL, + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'not-webhooks', + 'completed', + 'https://myhost/endpoint' + ), + -- team:2, plugin_config:4, completed in hour 20 (different team) + ( + NULL, + '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'completed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, failed in hour 20 + ( + ARRAY ['{"type":"Timeout","details":{"error":{"name":"timeout"}}}'::jsonb], + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'failed', + 'https://myhost/endpoint' + ); \ No newline at end of file diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 7e6f540837844..e30f71ac861b3 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -108,6 +108,26 @@ impl WebhookCleaner { }) } + #[allow(dead_code)] // This is used in tests. + pub fn new_from_pool( + queue_name: &str, + table_name: &str, + pg_pool: PgPool, + kafka_producer: FutureProducer, + app_metrics_topic: String, + ) -> Result { + let queue_name = queue_name.to_owned(); + let table_name = table_name.to_owned(); + + Ok(Self { + queue_name, + table_name, + pg_pool, + kafka_producer, + app_metrics_topic, + }) + } + async fn start_serializable_txn(&self) -> Result> { let mut tx = self .pg_pool @@ -118,6 +138,9 @@ impl WebhookCleaner { // We use serializable isolation so that we observe a snapshot of the DB at the time we // start the cleanup process. This prevents us from accidentally deleting rows that are // added (or become 'completed' or 'failed') after we start the cleanup process. + // + // If we find that this has a significant performance impact, we could instead move + // rows to a temporary table for processing and then deletion. sqlx::query("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") .execute(&mut *tx) .await @@ -133,8 +156,8 @@ impl WebhookCleaner { let base_query = format!( r#" SELECT DATE_TRUNC('hour', finished_at) AS hour, - metadata->>'team_id' AS team_id, - metadata->>'plugin_config_id' AS plugin_config_id, + (metadata->>'team_id')::bigint AS team_id, + (metadata->>'plugin_config_id')::bigint AS plugin_config_id, count(*) as successes FROM {0} WHERE status = 'completed' @@ -185,9 +208,13 @@ impl WebhookCleaner { let base_query = format!( r#" SELECT DATE_TRUNC('hour', finished_at) AS hour, - metadata->>'team_id' AS team_id, - metadata->>'plugin_config_id' AS plugin_config_id, - errors[-1] AS last_error, + (metadata->>'team_id')::bigint AS team_id, + (metadata->>'plugin_config_id')::bigint AS plugin_config_id, + CASE + WHEN array_length(errors, 1) > 1 + THEN errors[array_length(errors, 1)] + ELSE errors[1] + END AS last_error, count(*) as failures FROM {0} WHERE status = 'failed' @@ -302,27 +329,34 @@ impl WebhookCleaner { // Note that we select all completed and failed rows without any pagination at the moment. // We aggregrate as much as possible with GROUP BY, truncating the timestamp down to the // hour just like App Metrics does. A completed row is 24 bytes (and aggregates an entire - // hour per `plugin_config_id`), and a failed row is 104 + the message length (and - // aggregates an entire hour per `plugin_config_id` per `error`), so we can fit a lot of - // rows in memory. It seems unlikely we'll need to paginate, but that can be added in the + // hour per `plugin_config_id`), and a failed row is 104 bytes + the error message length + // (and aggregates an entire hour per `plugin_config_id` per `error`), so we can fit a lot + // of rows in memory. It seems unlikely we'll need to paginate, but that can be added in the // future if necessary. let mut tx = self.start_serializable_txn().await?; + let completed_rows = self.get_completed_rows(&mut tx).await?; - let mut payloads = self.serialize_completed_rows(completed_rows)?; + let completed_agg_row_count = completed_rows.len(); + let completed_kafka_payloads = self.serialize_completed_rows(completed_rows)?; + let failed_rows = self.get_failed_rows(&mut tx).await?; - let mut failed_payloads = self.serialize_failed_rows(failed_rows)?; - payloads.append(&mut failed_payloads); + let failed_agg_row_count = failed_rows.len(); + let mut failed_kafka_payloads = self.serialize_failed_rows(failed_rows)?; + + let mut all_kafka_payloads = completed_kafka_payloads; + all_kafka_payloads.append(&mut failed_kafka_payloads); + let mut rows_deleted: u64 = 0; - if !payloads.is_empty() { - self.send_messages_to_kafka(payloads).await?; + if !all_kafka_payloads.is_empty() { + self.send_messages_to_kafka(all_kafka_payloads).await?; rows_deleted = self.delete_observed_rows(&mut tx).await?; self.commit_txn(tx).await?; } debug!( - "WebhookCleaner finished cleanup, deleted {} rows", - rows_deleted + "WebhookCleaner finished cleanup, deleted {} rows ({} completed+aggregated, {} failed+aggregated)", + rows_deleted, completed_agg_row_count, failed_agg_row_count ); Ok(()) @@ -343,6 +377,78 @@ impl Cleaner for WebhookCleaner { #[cfg(test)] mod tests { - #[tokio::test] - async fn test() {} + use super::*; + use crate::config; + use crate::kafka_producer::{create_kafka_producer, KafkaContext}; + use rdkafka::mocking::MockCluster; + use rdkafka::producer::{DefaultProducerContext, FutureProducer}; + use sqlx::PgPool; + + const APP_METRICS_TOPIC: &str = "app_metrics"; + + async fn create_mock_kafka() -> ( + MockCluster<'static, DefaultProducerContext>, + FutureProducer, + ) { + let cluster = MockCluster::new(1).expect("failed to create mock brokers"); + + let config = config::KafkaConfig { + kafka_producer_linger_ms: 0, + kafka_producer_queue_mib: 50, + kafka_message_timeout_ms: 5000, + kafka_compression_codec: "none".to_string(), + kafka_hosts: cluster.bootstrap_servers(), + app_metrics_topic: APP_METRICS_TOPIC.to_string(), + plugin_log_entries_topic: "plugin_log_entries".to_string(), + kafka_tls: false, + }; + + ( + cluster, + create_kafka_producer(&config) + .await + .expect("failed to create mocked kafka producer"), + ) + } + + #[sqlx::test(migrations = "../migrations", fixtures("webhook_cleanup"))] + async fn test_cleanup_impl(db: PgPool) { + let (mock_cluster, mock_producer) = create_mock_kafka().await; + mock_cluster + .create_topic(APP_METRICS_TOPIC, 1, 1) + .expect("failed to create mock app_metrics topic"); + + let table_name = "job_queue"; + let queue_name = "webhooks"; + + let webhook_cleaner = WebhookCleaner::new_from_pool( + &queue_name, + &table_name, + db, + mock_producer, + APP_METRICS_TOPIC.to_owned(), + ) + .expect("unable to create webhook cleaner"); + + let _ = webhook_cleaner + .cleanup_impl() + .await + .expect("webbook cleanup_impl failed"); + + // TODO: I spent a lot of time trying to get the mock Kafka consumer to work, but I think + // I've identified an issue with the rust-rdkafka library: + // https://github.com/fede1024/rust-rdkafka/issues/629#issuecomment-1863555417 + // + // I wanted to test the messages put on the AppMetrics topic, but I think we need to figure + // out that issue about in order to do so. (Capture uses the MockProducer but not a + // Consumer, fwiw.) + // + // For now, I'll probably have to make `cleanup_impl` return the row information so at + // least we can inspect that for correctness. + } + + // #[sqlx::test] + // async fn test_serializable_isolation() { + // TODO: I'm going to add a test that verifies new rows aren't visible during the txn. + // } } From cc206a54216cf30d72900cedb5904ec1d2685cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 16:50:39 +0100 Subject: [PATCH 134/249] feat: Give non-transactional consumer a chance --- hook-consumer/src/config.rs | 3 +++ hook-consumer/src/consumer.rs | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/hook-consumer/src/config.rs b/hook-consumer/src/config.rs index 8e4bde9d956be..36c120ad421cc 100644 --- a/hook-consumer/src/config.rs +++ b/hook-consumer/src/config.rs @@ -26,6 +26,9 @@ pub struct Config { #[envconfig(nested = true)] pub retry_policy: RetryPolicyConfig, + #[envconfig(default = "true")] + pub transactional: bool, + #[envconfig(default = "job_queue")] pub table_name: String, } diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 12d4b38b9a56f..04600f138e590 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -23,6 +23,8 @@ pub struct WebhookConsumer<'p> { client: reqwest::Client, /// Maximum number of concurrent jobs being processed. max_concurrent_jobs: usize, + /// Indicates whether we are holding an open transaction while processing or not. + transactional: bool, } impl<'p> WebhookConsumer<'p> { @@ -54,6 +56,19 @@ impl<'p> WebhookConsumer<'p> { } } + /// Wait until a job becomes available in our queue. + async fn wait_for_job_tx<'a>( + &self, + ) -> Result, WebhookConsumerError> { + loop { + if let Some(job) = self.queue.dequeue_tx(&self.name).await? { + return Ok(job); + } else { + task::sleep(self.poll_interval).await; + } + } + } + /// Wait until a job becomes available in our queue. async fn wait_for_job<'a>( &self, @@ -72,7 +87,10 @@ impl<'p> WebhookConsumer<'p> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); loop { - let webhook_job = self.wait_for_job().await?; + let webhook_job = match self.transactional { + true => self.wait_for_job_tx().await, + false => self.wait_for_job().await, + }?; // reqwest::Client internally wraps with Arc, so this allocation is cheap. let client = self.client.clone(); From bd4dc4bdc5b01d52047cc439d4e1723d9c05e0f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 14 Dec 2023 12:30:31 +0100 Subject: [PATCH 135/249] refactor: Two clients one for each mode --- hook-consumer/src/consumer.rs | 137 +++++++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 11 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 04600f138e590..83ea319d2c555 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -23,8 +23,6 @@ pub struct WebhookConsumer<'p> { client: reqwest::Client, /// Maximum number of concurrent jobs being processed. max_concurrent_jobs: usize, - /// Indicates whether we are holding an open transaction while processing or not. - transactional: bool, } impl<'p> WebhookConsumer<'p> { @@ -57,11 +55,9 @@ impl<'p> WebhookConsumer<'p> { } /// Wait until a job becomes available in our queue. - async fn wait_for_job_tx<'a>( - &self, - ) -> Result, WebhookConsumerError> { + async fn wait_for_job<'a>(&self) -> Result, WebhookConsumerError> { loop { - if let Some(job) = self.queue.dequeue_tx(&self.name).await? { + if let Some(job) = self.queue.dequeue(&self.name).await? { return Ok(job); } else { task::sleep(self.poll_interval).await; @@ -69,6 +65,68 @@ impl<'p> WebhookConsumer<'p> { } } + /// Run this consumer to continuously process any jobs that become available. + pub async fn run(&self) -> Result<(), WebhookConsumerError> { + let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); + + loop { + let webhook_job = self.wait_for_job().await?; + + // reqwest::Client internally wraps with Arc, so this allocation is cheap. + let client = self.client.clone(); + let permit = semaphore.clone().acquire_owned().await.unwrap(); + + tokio::spawn(async move { + let result = process_webhook_job(client, webhook_job).await; + drop(permit); + result + }); + } + } +} + +/// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. +pub struct WebhookTransactionConsumer<'p> { + /// An identifier for this consumer. Used to mark jobs we have consumed. + name: String, + /// The queue we will be dequeuing jobs from. + queue: &'p PgQueue, + /// The interval for polling the queue. + poll_interval: time::Duration, + /// The client used for HTTP requests. + client: reqwest::Client, + /// Maximum number of concurrent jobs being processed. + max_concurrent_jobs: usize, +} + +impl<'p> WebhookTransactionConsumer<'p> { + pub fn new( + name: &str, + queue: &'p PgQueue, + poll_interval: time::Duration, + request_timeout: time::Duration, + max_concurrent_jobs: usize, + ) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + + let client = reqwest::Client::builder() + .default_headers(headers) + .timeout(request_timeout) + .build()?; + + Ok(Self { + name: name.to_owned(), + queue, + poll_interval, + client, + max_concurrent_jobs, + }) + } + /// Wait until a job becomes available in our queue. async fn wait_for_job<'a>( &self, @@ -87,17 +145,14 @@ impl<'p> WebhookConsumer<'p> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); loop { - let webhook_job = match self.transactional { - true => self.wait_for_job_tx().await, - false => self.wait_for_job().await, - }?; + let webhook_job = self.wait_for_job().await?; // reqwest::Client internally wraps with Arc, so this allocation is cheap. let client = self.client.clone(); let permit = semaphore.clone().acquire_owned().await.unwrap(); tokio::spawn(async move { - let result = process_webhook_job(client, webhook_job).await; + let result = process_webhook_job_tx(client, webhook_job).await; drop(permit); result }); @@ -187,6 +242,66 @@ async fn process_webhook_job( } } +/// Process a webhook job by transitioning it to its appropriate state after its request is sent. +/// After we finish, the webhook job will be set as completed (if the request was successful), retryable (if the request +/// was unsuccessful but we can still attempt a retry), or failed (if the request was unsuccessful and no more retries +/// may be attempted). +/// +/// A webhook job is considered retryable after a failing request if: +/// 1. The job has attempts remaining (i.e. hasn't reached `max_attempts`), and... +/// 2. The status code indicates retrying at a later point could resolve the issue. This means: 429 and any 5XX. +/// +/// # Arguments +/// +/// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. +/// * `request_timeout`: A timeout for the HTTP request. +async fn process_webhook_job( + client: reqwest::Client, + webhook_job: PgJob, +) -> Result<(), WebhookConsumerError> { + match send_webhook( + client, + &webhook_job.job.parameters.method, + &webhook_job.job.parameters.url, + &webhook_job.job.parameters.headers, + webhook_job.job.parameters.body.clone(), + ) + .await + { + Ok(_) => { + webhook_job + .complete() + .await + .map_err(|error| WebhookConsumerError::PgJobError(error.to_string()))?; + Ok(()) + } + Err(WebhookConsumerError::RetryableWebhookError { + reason, + retry_after, + }) => match webhook_job.retry(reason.to_string(), retry_after).await { + Ok(_) => Ok(()), + Err(PgJobError::RetryInvalidError { + job: webhook_job, + error: fail_error, + }) => { + webhook_job + .fail(fail_error.to_string()) + .await + .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), + }, + Err(error) => { + webhook_job + .fail(error.to_string()) + .await + .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + } +} + /// Make an HTTP request to a webhook endpoint. /// /// # Arguments From 3747c3450bc1a87240ed16a8d6d4ac20a3f216f1 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 20 Dec 2023 07:04:59 -0700 Subject: [PATCH 136/249] Handle some feedback --- README.md | 3 +++ hook-janitor/src/webhooks.rs | 10 +++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a3a674c28ff38..f579cd34fed49 100644 --- a/README.md +++ b/README.md @@ -15,5 +15,8 @@ docker compose -f docker-compose.yml up -d --wait 2. Test: ```bash +# Note that tests require a DATABASE_URL environment variable to be set, e.g.: +# export DATABASE_URL=postgres://posthog:posthog@localhost:15432/test_database +# But there is an .env file in the project root that should be used automatically. cargo test ``` diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index e30f71ac861b3..a9b05e560e737 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -210,11 +210,7 @@ impl WebhookCleaner { SELECT DATE_TRUNC('hour', finished_at) AS hour, (metadata->>'team_id')::bigint AS team_id, (metadata->>'plugin_config_id')::bigint AS plugin_config_id, - CASE - WHEN array_length(errors, 1) > 1 - THEN errors[array_length(errors, 1)] - ELSE errors[1] - END AS last_error, + errors[array_upper(errors, 1)] AS last_error, count(*) as failures FROM {0} WHERE status = 'failed' @@ -342,10 +338,10 @@ impl WebhookCleaner { let failed_rows = self.get_failed_rows(&mut tx).await?; let failed_agg_row_count = failed_rows.len(); - let mut failed_kafka_payloads = self.serialize_failed_rows(failed_rows)?; + let failed_kafka_payloads = self.serialize_failed_rows(failed_rows)?; let mut all_kafka_payloads = completed_kafka_payloads; - all_kafka_payloads.append(&mut failed_kafka_payloads); + all_kafka_payloads.extend(failed_kafka_payloads.into_iter()); let mut rows_deleted: u64 = 0; if !all_kafka_payloads.is_empty() { From d28dfef201c40d2cfe4f708f5230bd83b418b123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 20 Dec 2023 15:25:56 +0100 Subject: [PATCH 137/249] refactor: Support both modes in single client with WebhookJob trait --- Cargo.lock | 1 + hook-common/Cargo.toml | 1 + hook-common/src/pgqueue.rs | 179 +++++++++++++++++--------------- hook-consumer/src/consumer.rs | 187 +++++++++++----------------------- hook-consumer/src/main.rs | 6 +- 5 files changed, 164 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba9f4bb10b67d..130f765cff81d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1008,6 +1008,7 @@ dependencies = [ name = "hook-common" version = "0.1.0" dependencies = [ + "async-trait", "axum", "chrono", "http 0.2.11", diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 6350ba4b8f7c1..9b20396a388c9 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = { workspace = true } axum = { workspace = true, features = ["http2"] } chrono = { workspace = true } http = { workspace = true } diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 5288ade106f47..78fe2fc21c3f7 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -6,6 +6,7 @@ use std::default::Default; use std::str::FromStr; use std::time; +use async_trait::async_trait; use chrono; use serde; use sqlx::postgres::{PgPool, PgPoolOptions}; @@ -149,6 +150,22 @@ impl Job { } } +#[async_trait] +pub trait PgQueueJob { + async fn complete(mut self) -> Result>>; + + async fn fail( + mut self, + error: E, + ) -> Result, PgJobError>>; + + async fn retry( + mut self, + error: E, + preferred_retry_interval: Option, + ) -> Result, PgJobError>>; +} + /// A Job that can be updated in PostgreSQL. #[derive(Debug)] pub struct PgJob { @@ -158,22 +175,10 @@ pub struct PgJob { pub retry_policy: RetryPolicy, } -impl PgJob { - pub async fn retry( - mut self, - error: E, - preferred_retry_interval: Option, - ) -> Result, PgJobError>> { - if self.job.is_gte_max_attempts() { - return Err(PgJobError::RetryInvalidError { - job: self, - error: "Maximum attempts reached".to_owned(), - }); - } - let retryable_job = self.job.retry(error); - let retry_interval = self - .retry_policy - .time_until_next_retry(&retryable_job, preferred_retry_interval); +#[async_trait] +impl PgQueueJob for PgJob { + async fn complete(mut self) -> Result>>> { + let completed_job = self.job.complete(); let base_query = format!( r#" @@ -181,9 +186,7 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'available'::job_status, - scheduled_at = NOW() + $3, - errors = array_append("{0}".errors, $4) + status = 'completed'::job_status WHERE "{0}".id = $2 AND queue = $1 @@ -194,10 +197,8 @@ RETURNING ); sqlx::query(&base_query) - .bind(&retryable_job.queue) - .bind(retryable_job.id) - .bind(retry_interval) - .bind(&retryable_job.error) + .bind(&completed_job.queue) + .bind(completed_job.id) .execute(&mut *self.connection) .await .map_err(|error| PgJobError::QueryError { @@ -205,11 +206,14 @@ RETURNING error, })?; - Ok(retryable_job) + Ok(completed_job) } - pub async fn complete(mut self) -> Result>> { - let completed_job = self.job.complete(); + async fn fail( + mut self, + error: E, + ) -> Result, PgJobError>>> { + let failed_job = self.job.fail(error); let base_query = format!( r#" @@ -217,19 +221,22 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'completed'::job_status + status = 'failed'::job_status + errors = array_append("{0}".errors, $3) WHERE "{0}".id = $2 AND queue = $1 RETURNING "{0}".* + "#, &self.table ); sqlx::query(&base_query) - .bind(&completed_job.queue) - .bind(completed_job.id) + .bind(&failed_job.queue) + .bind(failed_job.id) + .bind(&failed_job.error) .execute(&mut *self.connection) .await .map_err(|error| PgJobError::QueryError { @@ -237,14 +244,24 @@ RETURNING error, })?; - Ok(completed_job) + Ok(failed_job) } - pub async fn fail( + async fn retry( mut self, error: E, - ) -> Result, PgJobError>> { - let failed_job = self.job.fail(error); + preferred_retry_interval: Option, + ) -> Result, PgJobError>>> { + if self.job.is_gte_max_attempts() { + return Err(PgJobError::RetryInvalidError { + job: Box::new(self), + error: "Maximum attempts reached".to_owned(), + }); + } + let retryable_job = self.job.retry(error); + let retry_interval = self + .retry_policy + .time_until_next_retry(&retryable_job, preferred_retry_interval); let base_query = format!( r#" @@ -252,22 +269,23 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'failed'::job_status - errors = array_append("{0}".errors, $3) + status = 'available'::job_status, + scheduled_at = NOW() + $3, + errors = array_append("{0}".errors, $4) WHERE "{0}".id = $2 AND queue = $1 RETURNING "{0}".* - "#, &self.table ); sqlx::query(&base_query) - .bind(&failed_job.queue) - .bind(failed_job.id) - .bind(&failed_job.error) + .bind(&retryable_job.queue) + .bind(retryable_job.id) + .bind(retry_interval) + .bind(&retryable_job.error) .execute(&mut *self.connection) .await .map_err(|error| PgJobError::QueryError { @@ -275,7 +293,7 @@ RETURNING error, })?; - Ok(failed_job) + Ok(retryable_job) } } @@ -289,22 +307,12 @@ pub struct PgTransactionJob<'c, J, M> { pub retry_policy: RetryPolicy, } -impl<'c, J, M> PgTransactionJob<'c, J, M> { - pub async fn retry( +#[async_trait] +impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactionJob<'c, J, M> { + async fn complete( mut self, - error: E, - preferred_retry_interval: Option, - ) -> Result, PgJobError>> { - if self.job.is_gte_max_attempts() { - return Err(PgJobError::RetryInvalidError { - job: self, - error: "Maximum attempts reached".to_owned(), - }); - } - let retryable_job = self.job.retry(error); - let retry_interval = self - .retry_policy - .time_until_next_retry(&retryable_job, preferred_retry_interval); + ) -> Result>>> { + let completed_job = self.job.complete(); let base_query = format!( r#" @@ -312,24 +320,19 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'available'::job_status, - scheduled_at = NOW() + $3, - errors = array_append("{0}".errors, $4) + status = 'completed'::job_status WHERE "{0}".id = $2 AND queue = $1 RETURNING "{0}".* - "#, &self.table ); sqlx::query(&base_query) - .bind(&retryable_job.queue) - .bind(retryable_job.id) - .bind(retry_interval) - .bind(&retryable_job.error) + .bind(&completed_job.queue) + .bind(completed_job.id) .execute(&mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { @@ -345,13 +348,14 @@ RETURNING error, })?; - Ok(retryable_job) + Ok(completed_job) } - pub async fn complete( + async fn fail( mut self, - ) -> Result>> { - let completed_job = self.job.complete(); + error: E, + ) -> Result, PgJobError>>> { + let failed_job = self.job.fail(error); let base_query = format!( r#" @@ -359,7 +363,8 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'completed'::job_status + status = 'failed'::job_status + errors = array_append("{0}".errors, $3) WHERE "{0}".id = $2 AND queue = $1 @@ -370,8 +375,9 @@ RETURNING ); sqlx::query(&base_query) - .bind(&completed_job.queue) - .bind(completed_job.id) + .bind(&failed_job.queue) + .bind(failed_job.id) + .bind(&failed_job.error) .execute(&mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { @@ -387,14 +393,24 @@ RETURNING error, })?; - Ok(completed_job) + Ok(failed_job) } - pub async fn fail( + async fn retry( mut self, error: E, - ) -> Result, PgJobError>> { - let failed_job = self.job.fail(error); + preferred_retry_interval: Option, + ) -> Result, PgJobError>>> { + if self.job.is_gte_max_attempts() { + return Err(PgJobError::RetryInvalidError { + job: Box::new(self), + error: "Maximum attempts reached".to_owned(), + }); + } + let retryable_job = self.job.retry(error); + let retry_interval = self + .retry_policy + .time_until_next_retry(&retryable_job, preferred_retry_interval); let base_query = format!( r#" @@ -402,21 +418,24 @@ UPDATE "{0}" SET finished_at = NOW(), - status = 'failed'::job_status - errors = array_append("{0}".errors, $3) + status = 'available'::job_status, + scheduled_at = NOW() + $3, + errors = array_append("{0}".errors, $4) WHERE "{0}".id = $2 AND queue = $1 RETURNING "{0}".* + "#, &self.table ); sqlx::query(&base_query) - .bind(&failed_job.queue) - .bind(failed_job.id) - .bind(&failed_job.error) + .bind(&retryable_job.queue) + .bind(retryable_job.id) + .bind(retry_interval) + .bind(&retryable_job.error) .execute(&mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { @@ -432,7 +451,7 @@ RETURNING error, })?; - Ok(failed_job) + Ok(retryable_job) } } diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 83ea319d2c555..4fdc8e476d745 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use std::time; use async_std::task; -use hook_common::pgqueue::{PgJobError, PgQueue, PgQueueError, PgTransactionJob}; +use hook_common::pgqueue::{ + PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob, +}; use hook_common::webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}; use http::StatusCode; use reqwest::header; @@ -11,6 +13,32 @@ use tokio::sync; use crate::error::{ConsumerError, WebhookError}; +/// A WebhookJob is any PgQueueJob that returns webhook required parameters and metadata. +trait WebhookJob: PgQueueJob + std::marker::Send { + fn parameters<'a>(&'a self) -> &'a WebhookJobParameters; + fn metadata<'a>(&'a self) -> &'a WebhookJobMetadata; +} + +impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata> { + fn parameters<'a>(&'a self) -> &'a WebhookJobParameters { + &self.job.parameters + } + + fn metadata<'a>(&'a self) -> &'a WebhookJobMetadata { + &self.job.metadata + } +} + +impl WebhookJob for PgJob { + fn parameters<'a>(&'a self) -> &'a WebhookJobParameters { + &self.job.parameters + } + + fn metadata<'a>(&'a self) -> &'a WebhookJobMetadata { + &self.job.metadata + } +} + /// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. pub struct WebhookConsumer<'p> { /// An identifier for this consumer. Used to mark jobs we have consumed. @@ -55,7 +83,9 @@ impl<'p> WebhookConsumer<'p> { } /// Wait until a job becomes available in our queue. - async fn wait_for_job<'a>(&self) -> Result, WebhookConsumerError> { + async fn wait_for_job<'a>( + &self, + ) -> Result, ConsumerError> { loop { if let Some(job) = self.queue.dequeue(&self.name).await? { return Ok(job); @@ -65,8 +95,21 @@ impl<'p> WebhookConsumer<'p> { } } + /// Wait until a job becomes available in our queue in transactional mode. + async fn wait_for_job_tx<'a>( + &self, + ) -> Result, ConsumerError> { + loop { + if let Some(job) = self.queue.dequeue_tx(&self.name).await? { + return Ok(job); + } else { + task::sleep(self.poll_interval).await; + } + } + } + /// Run this consumer to continuously process any jobs that become available. - pub async fn run(&self) -> Result<(), WebhookConsumerError> { + pub async fn run(&self) -> Result<(), ConsumerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); loop { @@ -83,76 +126,20 @@ impl<'p> WebhookConsumer<'p> { }); } } -} - -/// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. -pub struct WebhookTransactionConsumer<'p> { - /// An identifier for this consumer. Used to mark jobs we have consumed. - name: String, - /// The queue we will be dequeuing jobs from. - queue: &'p PgQueue, - /// The interval for polling the queue. - poll_interval: time::Duration, - /// The client used for HTTP requests. - client: reqwest::Client, - /// Maximum number of concurrent jobs being processed. - max_concurrent_jobs: usize, -} - -impl<'p> WebhookTransactionConsumer<'p> { - pub fn new( - name: &str, - queue: &'p PgQueue, - poll_interval: time::Duration, - request_timeout: time::Duration, - max_concurrent_jobs: usize, - ) -> Result { - let mut headers = header::HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ); - - let client = reqwest::Client::builder() - .default_headers(headers) - .timeout(request_timeout) - .build()?; - - Ok(Self { - name: name.to_owned(), - queue, - poll_interval, - client, - max_concurrent_jobs, - }) - } - - /// Wait until a job becomes available in our queue. - async fn wait_for_job<'a>( - &self, - ) -> Result, ConsumerError> { - loop { - if let Some(job) = self.queue.dequeue_tx(&self.name).await? { - return Ok(job); - } else { - task::sleep(self.poll_interval).await; - } - } - } - /// Run this consumer to continuously process any jobs that become available. - pub async fn run(&self) -> Result<(), ConsumerError> { + /// Run this consumer to continuously process any jobs that become available in transactional mode. + pub async fn run_tx(&self) -> Result<(), ConsumerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); loop { - let webhook_job = self.wait_for_job().await?; + let webhook_job = self.wait_for_job_tx().await?; // reqwest::Client internally wraps with Arc, so this allocation is cheap. let client = self.client.clone(); let permit = semaphore.clone().acquire_owned().await.unwrap(); tokio::spawn(async move { - let result = process_webhook_job_tx(client, webhook_job).await; + let result = process_webhook_job(client, webhook_job).await; drop(permit); result }); @@ -173,16 +160,18 @@ impl<'p> WebhookTransactionConsumer<'p> { /// /// * `client`: An HTTP client to execute the webhook job request. /// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. -async fn process_webhook_job( +async fn process_webhook_job( client: reqwest::Client, - webhook_job: PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata>, + webhook_job: W, ) -> Result<(), ConsumerError> { + let parameters = webhook_job.parameters(); + match send_webhook( client, - &webhook_job.job.parameters.method, - &webhook_job.job.parameters.url, - &webhook_job.job.parameters.headers, - webhook_job.job.parameters.body.clone(), + ¶meters.method, + ¶meters.url, + ¶meters.headers, + parameters.body.clone(), ) .await { @@ -242,66 +231,6 @@ async fn process_webhook_job( } } -/// Process a webhook job by transitioning it to its appropriate state after its request is sent. -/// After we finish, the webhook job will be set as completed (if the request was successful), retryable (if the request -/// was unsuccessful but we can still attempt a retry), or failed (if the request was unsuccessful and no more retries -/// may be attempted). -/// -/// A webhook job is considered retryable after a failing request if: -/// 1. The job has attempts remaining (i.e. hasn't reached `max_attempts`), and... -/// 2. The status code indicates retrying at a later point could resolve the issue. This means: 429 and any 5XX. -/// -/// # Arguments -/// -/// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. -/// * `request_timeout`: A timeout for the HTTP request. -async fn process_webhook_job( - client: reqwest::Client, - webhook_job: PgJob, -) -> Result<(), WebhookConsumerError> { - match send_webhook( - client, - &webhook_job.job.parameters.method, - &webhook_job.job.parameters.url, - &webhook_job.job.parameters.headers, - webhook_job.job.parameters.body.clone(), - ) - .await - { - Ok(_) => { - webhook_job - .complete() - .await - .map_err(|error| WebhookConsumerError::PgJobError(error.to_string()))?; - Ok(()) - } - Err(WebhookConsumerError::RetryableWebhookError { - reason, - retry_after, - }) => match webhook_job.retry(reason.to_string(), retry_after).await { - Ok(_) => Ok(()), - Err(PgJobError::RetryInvalidError { - job: webhook_job, - error: fail_error, - }) => { - webhook_job - .fail(fail_error.to_string()) - .await - .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; - Ok(()) - } - Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), - }, - Err(error) => { - webhook_job - .fail(error.to_string()) - .await - .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; - Ok(()) - } - } -} - /// Make an HTTP request to a webhook endpoint. /// /// # Arguments diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index bb02526c23b76..49c2e76a8610f 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -31,7 +31,11 @@ async fn main() -> Result<(), ConsumerError> { config.max_concurrent_jobs, ); - let _ = consumer.run().await; + if config.transactional { + consumer.run_tx().await?; + } else { + consumer.run().await?; + } Ok(()) } From b747d4c242b5abd0b959190a99797c717b3c7239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 20 Dec 2023 15:31:21 +0100 Subject: [PATCH 138/249] fix: Elide lifetimes --- hook-consumer/src/consumer.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 4fdc8e476d745..73bac1cffba87 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -13,28 +13,28 @@ use tokio::sync; use crate::error::{ConsumerError, WebhookError}; -/// A WebhookJob is any PgQueueJob that returns webhook required parameters and metadata. +/// A WebhookJob is any PgQueueJob that returns a reference to webhook parameters and metadata. trait WebhookJob: PgQueueJob + std::marker::Send { - fn parameters<'a>(&'a self) -> &'a WebhookJobParameters; - fn metadata<'a>(&'a self) -> &'a WebhookJobMetadata; + fn parameters(&self) -> &WebhookJobParameters; + fn metadata(&self) -> &WebhookJobMetadata; } impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata> { - fn parameters<'a>(&'a self) -> &'a WebhookJobParameters { + fn parameters(&self) -> &WebhookJobParameters { &self.job.parameters } - fn metadata<'a>(&'a self) -> &'a WebhookJobMetadata { + fn metadata(&self) -> &WebhookJobMetadata { &self.job.metadata } } impl WebhookJob for PgJob { - fn parameters<'a>(&'a self) -> &'a WebhookJobParameters { + fn parameters(&self) -> &WebhookJobParameters { &self.job.parameters } - fn metadata<'a>(&'a self) -> &'a WebhookJobMetadata { + fn metadata(&self) -> &WebhookJobMetadata { &self.job.metadata } } From 059fc99cb9bc4e381534a5cdcd7361b269c4b87a Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 20 Dec 2023 07:54:54 -0700 Subject: [PATCH 139/249] Add SerializableTxn for a little safety --- hook-janitor/src/webhooks.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index a9b05e560e737..71fe567dd30e3 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -84,6 +84,10 @@ struct FailedRow { failures: u32, } +// A simple wrapper type that ensures we don't use any old Transaction object when we need one +// that has set the isolation level to serializable. +struct SerializableTxn<'a>(Transaction<'a, Postgres>); + impl WebhookCleaner { pub fn new( queue_name: &str, @@ -128,7 +132,7 @@ impl WebhookCleaner { }) } - async fn start_serializable_txn(&self) -> Result> { + async fn start_serializable_txn(&self) -> Result { let mut tx = self .pg_pool .begin() @@ -146,13 +150,10 @@ impl WebhookCleaner { .await .map_err(|e| WebhookCleanerError::StartTxnError { error: e })?; - Ok(tx) + Ok(SerializableTxn(tx)) } - async fn get_completed_rows( - &self, - tx: &mut Transaction<'_, Postgres>, - ) -> Result> { + async fn get_completed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { let base_query = format!( r#" SELECT DATE_TRUNC('hour', finished_at) AS hour, @@ -170,7 +171,7 @@ impl WebhookCleaner { let rows = sqlx::query_as::<_, CompletedRow>(&base_query) .bind(&self.queue_name) - .fetch_all(&mut **tx) + .fetch_all(&mut *tx.0) .await .map_err(|e| WebhookCleanerError::GetCompletedRowsError { error: e })?; @@ -204,7 +205,7 @@ impl WebhookCleaner { Ok(payloads) } - async fn get_failed_rows(&self, tx: &mut Transaction<'_, Postgres>) -> Result> { + async fn get_failed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { let base_query = format!( r#" SELECT DATE_TRUNC('hour', finished_at) AS hour, @@ -223,7 +224,7 @@ impl WebhookCleaner { let rows = sqlx::query_as::<_, FailedRow>(&base_query) .bind(&self.queue_name) - .fetch_all(&mut **tx) + .fetch_all(&mut *tx.0) .await .map_err(|e| WebhookCleanerError::GetFailedRowsError { error: e })?; @@ -290,7 +291,7 @@ impl WebhookCleaner { Ok(()) } - async fn delete_observed_rows(&self, tx: &mut Transaction<'_, Postgres>) -> Result { + async fn delete_observed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result { // This DELETE is only safe because we are in serializable isolation mode, see the note // in `start_serializable_txn`. let base_query = format!( @@ -304,15 +305,15 @@ impl WebhookCleaner { let result = sqlx::query(&base_query) .bind(&self.queue_name) - .execute(&mut **tx) + .execute(&mut *tx.0) .await .map_err(|e| WebhookCleanerError::DeleteRowsError { error: e })?; Ok(result.rows_affected()) } - async fn commit_txn(&self, tx: Transaction<'_, Postgres>) -> Result<()> { - tx.commit() + async fn commit_txn(&self, tx: SerializableTxn<'_>) -> Result<()> { + tx.0.commit() .await .map_err(|e| WebhookCleanerError::CommitTxnError { error: e })?; From 8aeaad7e474e5592f6be47a53a1044c5314005f3 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 20 Dec 2023 08:18:04 -0700 Subject: [PATCH 140/249] Cleanup AppMetric creation and serialization --- hook-janitor/src/webhooks.rs | 139 +++++++++++++++++------------------ 1 file changed, 66 insertions(+), 73 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 71fe567dd30e3..d4b62550d1e61 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -66,6 +66,24 @@ struct CompletedRow { successes: u32, } +impl From for AppMetric { + fn from(row: CompletedRow) -> Self { + AppMetric { + timestamp: row.hour, + team_id: row.team_id, + plugin_config_id: row.plugin_config_id, + job_id: None, + category: AppMetricCategory::Webhook, + successes: row.successes, + successes_on_retry: 0, + failures: 0, + error_uuid: None, + error_type: None, + error_details: None, + } + } +} + #[derive(sqlx::FromRow, Debug)] struct FailedRow { // App Metrics truncates/aggregates rows on the hour, so we take advantage of that to GROUP BY @@ -84,6 +102,24 @@ struct FailedRow { failures: u32, } +impl From for AppMetric { + fn from(row: FailedRow) -> Self { + AppMetric { + timestamp: row.hour, + team_id: row.team_id, + plugin_config_id: row.plugin_config_id, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 0, + successes_on_retry: 0, + failures: row.failures, + error_uuid: Some(Uuid::now_v7()), + error_type: Some(row.last_error.r#type), + error_details: Some(row.last_error.details), + } + } +} + // A simple wrapper type that ensures we don't use any old Transaction object when we need one // that has set the isolation level to serializable. struct SerializableTxn<'a>(Transaction<'a, Postgres>); @@ -178,33 +214,6 @@ impl WebhookCleaner { Ok(rows) } - fn serialize_completed_rows(&self, completed_rows: Vec) -> Result> { - let mut payloads = Vec::new(); - - for row in completed_rows { - let app_metric = AppMetric { - timestamp: row.hour, - team_id: row.team_id, - plugin_config_id: row.plugin_config_id, - job_id: None, - category: AppMetricCategory::Webhook, - successes: row.successes, - successes_on_retry: 0, - failures: 0, - error_uuid: None, - error_type: None, - error_details: None, - }; - - let payload = serde_json::to_string(&app_metric) - .map_err(|e| WebhookCleanerError::SerializeRowsError { error: e })?; - - payloads.push(payload) - } - - Ok(payloads) - } - async fn get_failed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { let base_query = format!( r#" @@ -231,34 +240,13 @@ impl WebhookCleaner { Ok(rows) } - fn serialize_failed_rows(&self, failed_rows: Vec) -> Result> { - let mut payloads = Vec::new(); - - for row in failed_rows { - let app_metric = AppMetric { - timestamp: row.hour, - team_id: row.team_id, - plugin_config_id: row.plugin_config_id, - job_id: None, - category: AppMetricCategory::Webhook, - successes: 0, - successes_on_retry: 0, - failures: row.failures, - error_uuid: Some(Uuid::now_v7()), - error_type: Some(row.last_error.r#type), - error_details: Some(row.last_error.details), - }; - - let payload = serde_json::to_string(&app_metric) - .map_err(|e| WebhookCleanerError::SerializeRowsError { error: e })?; - - payloads.push(payload) - } + async fn send_metrics_to_kafka(&self, metrics: Vec) -> Result<()> { + let payloads: Vec = metrics + .into_iter() + .map(|metric| serde_json::to_string(&metric)) + .collect::, SerdeError>>() + .map_err(|e| WebhookCleanerError::SerializeRowsError { error: e })?; - Ok(payloads) - } - - async fn send_messages_to_kafka(&self, payloads: Vec) -> Result<()> { let mut delivery_futures = Vec::new(); for payload in payloads { @@ -332,30 +320,35 @@ impl WebhookCleaner { // future if necessary. let mut tx = self.start_serializable_txn().await?; + let mut rows_processed = 0; + + { + let completed_rows = self.get_completed_rows(&mut tx).await?; + rows_processed += completed_rows.len(); + let completed_app_metrics: Vec = + completed_rows.into_iter().map(Into::into).collect(); + self.send_metrics_to_kafka(completed_app_metrics).await?; + } - let completed_rows = self.get_completed_rows(&mut tx).await?; - let completed_agg_row_count = completed_rows.len(); - let completed_kafka_payloads = self.serialize_completed_rows(completed_rows)?; - - let failed_rows = self.get_failed_rows(&mut tx).await?; - let failed_agg_row_count = failed_rows.len(); - let failed_kafka_payloads = self.serialize_failed_rows(failed_rows)?; - - let mut all_kafka_payloads = completed_kafka_payloads; - all_kafka_payloads.extend(failed_kafka_payloads.into_iter()); + { + let failed_rows = self.get_failed_rows(&mut tx).await?; + rows_processed += failed_rows.len(); + let failed_app_metrics: Vec = + failed_rows.into_iter().map(Into::into).collect(); + self.send_metrics_to_kafka(failed_app_metrics).await?; + } - let mut rows_deleted: u64 = 0; - if !all_kafka_payloads.is_empty() { - self.send_messages_to_kafka(all_kafka_payloads).await?; - rows_deleted = self.delete_observed_rows(&mut tx).await?; + if rows_processed != 0 { + let rows_deleted = self.delete_observed_rows(&mut tx).await?; self.commit_txn(tx).await?; + debug!( + "WebhookCleaner finished cleanup, processed and deleted {} rows", + rows_deleted + ); + } else { + debug!("WebhookCleaner finished cleanup, no-op"); } - debug!( - "WebhookCleaner finished cleanup, deleted {} rows ({} completed+aggregated, {} failed+aggregated)", - rows_deleted, completed_agg_row_count, failed_agg_row_count - ); - Ok(()) } } From a94ee2f6dec7fdac47a524b9caf56c01ed362584 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 20 Dec 2023 08:31:13 -0700 Subject: [PATCH 141/249] Clean up row counts --- hook-janitor/src/webhooks.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index d4b62550d1e61..5ccf6dca701c3 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -241,6 +241,10 @@ impl WebhookCleaner { } async fn send_metrics_to_kafka(&self, metrics: Vec) -> Result<()> { + if metrics.is_empty() { + return Ok(()); + } + let payloads: Vec = metrics .into_iter() .map(|metric| serde_json::to_string(&metric)) @@ -320,33 +324,34 @@ impl WebhookCleaner { // future if necessary. let mut tx = self.start_serializable_txn().await?; - let mut rows_processed = 0; - { + let completed_agg_row_count = { let completed_rows = self.get_completed_rows(&mut tx).await?; - rows_processed += completed_rows.len(); + let row_count = completed_rows.len(); let completed_app_metrics: Vec = completed_rows.into_iter().map(Into::into).collect(); self.send_metrics_to_kafka(completed_app_metrics).await?; - } + row_count + }; - { + let failed_agg_row_count = { let failed_rows = self.get_failed_rows(&mut tx).await?; - rows_processed += failed_rows.len(); + let row_count = failed_rows.len(); let failed_app_metrics: Vec = failed_rows.into_iter().map(Into::into).collect(); self.send_metrics_to_kafka(failed_app_metrics).await?; - } + row_count + }; - if rows_processed != 0 { + if completed_agg_row_count + failed_agg_row_count != 0 { let rows_deleted = self.delete_observed_rows(&mut tx).await?; self.commit_txn(tx).await?; debug!( - "WebhookCleaner finished cleanup, processed and deleted {} rows", - rows_deleted + "WebhookCleaner finished cleanup, processed and deleted {} rows ({}/{} aggregated completed/failed rows)", + rows_deleted, completed_agg_row_count, failed_agg_row_count ); } else { - debug!("WebhookCleaner finished cleanup, no-op"); + debug!("WebhookCleaner finished cleanup, there were no rows to process"); } Ok(()) From 66ea83428f7bd5502475a21e8d0992d868a7645f Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 20 Dec 2023 08:53:47 -0700 Subject: [PATCH 142/249] Tweak ErrorType, add some notes about stability of serialized types --- hook-common/src/kafka_messages/app_metrics.rs | 24 ++++++++++++------- hook-common/src/webhook.rs | 16 ++++++------- hook-janitor/src/fixtures/webhook_cleanup.sql | 14 +++++++++-- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 439664342fa35..cecf6049a8637 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -13,14 +13,20 @@ pub enum AppMetricCategory { ComposeWebhook, } +// NOTE: These are stored in Postgres and deserialized by the cleanup/janitor process, so these +// names need to remain stable, or new variants need to be deployed to the cleanup/janitor +// process before they are used. #[derive(Deserialize, Serialize, Debug)] pub enum ErrorType { - Timeout, - Connection, - HttpStatus(u16), - Parse, + TimeoutError, + ConnectionError, + BadHttpStatus(u16), + ParseError, } +// NOTE: This is stored in Postgres and deserialized by the cleanup/janitor process, so this +// shouldn't change. It is intended to replicate the shape of `error_details` used in the +// plugin-server and by the frontend. #[derive(Deserialize, Serialize, Debug)] pub struct ErrorDetails { pub error: Error, @@ -88,10 +94,10 @@ where }; let error_type = match error_type { - ErrorType::Connection => "Connection Error".to_owned(), - ErrorType::Timeout => "Timeout".to_owned(), - ErrorType::HttpStatus(s) => format!("HTTP Status: {}", s), - ErrorType::Parse => "Parse Error".to_owned(), + ErrorType::ConnectionError => "Connection Error".to_owned(), + ErrorType::TimeoutError => "Timeout".to_owned(), + ErrorType::BadHttpStatus(s) => format!("HTTP Status: {}", s), + ErrorType::ParseError => "Parse Error".to_owned(), }; serializer.serialize_str(&error_type) } @@ -114,7 +120,7 @@ mod tests { successes_on_retry: 0, failures: 2, error_uuid: Some(Uuid::parse_str("550e8400-e29b-41d4-a716-446655447777").unwrap()), - error_type: Some(ErrorType::Connection), + error_type: Some(ErrorType::ConnectionError), error_details: Some(ErrorDetails { error: Error { name: "FooError".to_owned(), diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 9a21b83cd713b..bb1b5be04390a 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -169,12 +169,12 @@ impl From<&reqwest::Error> for WebhookJobError { impl WebhookJobError { pub fn new_timeout(message: &str) -> Self { let error_details = app_metrics::Error { - name: "timeout".to_owned(), + name: "Timeout Error".to_owned(), message: Some(message.to_owned()), stack: None, }; Self { - r#type: app_metrics::ErrorType::Timeout, + r#type: app_metrics::ErrorType::TimeoutError, details: app_metrics::ErrorDetails { error: error_details, }, @@ -183,12 +183,12 @@ impl WebhookJobError { pub fn new_connection(message: &str) -> Self { let error_details = app_metrics::Error { - name: "connection error".to_owned(), + name: "Connection Error".to_owned(), message: Some(message.to_owned()), stack: None, }; Self { - r#type: app_metrics::ErrorType::Connection, + r#type: app_metrics::ErrorType::ConnectionError, details: app_metrics::ErrorDetails { error: error_details, }, @@ -197,12 +197,12 @@ impl WebhookJobError { pub fn new_http_status(status_code: u16, message: &str) -> Self { let error_details = app_metrics::Error { - name: "http status".to_owned(), + name: "Bad Http Status".to_owned(), message: Some(message.to_owned()), stack: None, }; Self { - r#type: app_metrics::ErrorType::HttpStatus(status_code), + r#type: app_metrics::ErrorType::BadHttpStatus(status_code), details: app_metrics::ErrorDetails { error: error_details, }, @@ -211,12 +211,12 @@ impl WebhookJobError { pub fn new_parse(message: &str) -> Self { let error_details = app_metrics::Error { - name: "parse error".to_owned(), + name: "Parse Error".to_owned(), message: Some(message.to_owned()), stack: None, }; Self { - r#type: app_metrics::ErrorType::Parse, + r#type: app_metrics::ErrorType::ParseError, details: app_metrics::ErrorDetails { error: error_details, }, diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql index 6f29d302b8462..e4ea082ebf188 100644 --- a/hook-janitor/src/fixtures/webhook_cleanup.sql +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -19,7 +19,7 @@ VALUES 'completed', 'https://myhost/endpoint' ), - -- another team:1, plugin_config:2, completed in hour 20 + -- team:1, plugin_config:2, completed in hour 20 (purposeful duplicate) ( NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', @@ -71,7 +71,17 @@ VALUES ), -- team:1, plugin_config:2, failed in hour 20 ( - ARRAY ['{"type":"Timeout","details":{"error":{"name":"timeout"}}}'::jsonb], + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"timeout"}}}'::jsonb], + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'failed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, failed in hour 20 (purposeful duplicate) + ( + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '{}', From dd8faee798efe8796d2fb17df5f054c1f7f75c9d Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 20 Dec 2023 15:41:25 -0700 Subject: [PATCH 143/249] Finish up tests --- Cargo.lock | 1 + Cargo.toml | 2 +- hook-common/src/kafka_messages/app_metrics.rs | 94 ++++- hook-common/src/kafka_messages/mod.rs | 29 +- hook-common/src/kafka_messages/plugin_logs.rs | 4 +- hook-janitor/src/fixtures/webhook_cleanup.sql | 74 +++- hook-janitor/src/webhooks.rs | 372 ++++++++++++++++-- 7 files changed, 510 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbfcc50d98ee8..c36b1da60cae4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2953,6 +2953,7 @@ checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "atomic", "getrandom", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a29806a2fa730..1f6a38b6e3ed1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,4 +35,4 @@ tower = "0.4.13" tracing = "0.1.40" tracing-subscriber = "0.3.18" url = { version = "2.5.0 " } -uuid = { version = "1.6.1", features = ["v7"] } +uuid = { version = "1.6.1", features = ["v7", "serde"] } diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index cecf6049a8637..5aff62cd9fedd 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -1,10 +1,10 @@ use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use uuid::Uuid; -use super::{serialize_datetime, serialize_optional_uuid}; +use super::{deserialize_datetime, serialize_datetime}; -#[derive(Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub enum AppMetricCategory { ProcessEvent, OnEvent, @@ -16,7 +16,7 @@ pub enum AppMetricCategory { // NOTE: These are stored in Postgres and deserialized by the cleanup/janitor process, so these // names need to remain stable, or new variants need to be deployed to the cleanup/janitor // process before they are used. -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub enum ErrorType { TimeoutError, ConnectionError, @@ -27,12 +27,12 @@ pub enum ErrorType { // NOTE: This is stored in Postgres and deserialized by the cleanup/janitor process, so this // shouldn't change. It is intended to replicate the shape of `error_details` used in the // plugin-server and by the frontend. -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct ErrorDetails { pub error: Error, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct Error { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -43,26 +43,30 @@ pub struct Error { pub stack: Option, } -#[derive(Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct AppMetric { - #[serde(serialize_with = "serialize_datetime")] + #[serde( + serialize_with = "serialize_datetime", + deserialize_with = "deserialize_datetime" + )] pub timestamp: DateTime, pub team_id: u32, pub plugin_config_id: u32, #[serde(skip_serializing_if = "Option::is_none")] pub job_id: Option, - #[serde(serialize_with = "serialize_category")] + #[serde( + serialize_with = "serialize_category", + deserialize_with = "deserialize_category" + )] pub category: AppMetricCategory, pub successes: u32, pub successes_on_retry: u32, pub failures: u32, - #[serde( - serialize_with = "serialize_optional_uuid", - skip_serializing_if = "Option::is_none" - )] pub error_uuid: Option, #[serde( serialize_with = "serialize_error_type", + deserialize_with = "deserialize_error_type", + default, skip_serializing_if = "Option::is_none" )] pub error_type: Option, @@ -84,6 +88,35 @@ where serializer.serialize_str(category_str) } +fn deserialize_category<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + + let category = match &s[..] { + "processEvent" => AppMetricCategory::ProcessEvent, + "onEvent" => AppMetricCategory::OnEvent, + "scheduledTask" => AppMetricCategory::ScheduledTask, + "webhook" => AppMetricCategory::Webhook, + "composeWebhook" => AppMetricCategory::ComposeWebhook, + _ => { + return Err(serde::de::Error::unknown_variant( + &s, + &[ + "processEvent", + "onEvent", + "scheduledTask", + "webhook", + "composeWebhook", + ], + )) + } + }; + + Ok(category) +} + fn serialize_error_type(error_type: &Option, serializer: S) -> Result where S: Serializer, @@ -102,6 +135,41 @@ where serializer.serialize_str(&error_type) } +fn deserialize_error_type<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + let error_type = match opt { + Some(s) => { + let error_type = match &s[..] { + "Connection Error" => ErrorType::ConnectionError, + "Timeout" => ErrorType::TimeoutError, + _ if s.starts_with("HTTP Status:") => { + let status = &s["HTTP Status:".len()..]; + ErrorType::BadHttpStatus(status.parse().map_err(serde::de::Error::custom)?) + } + "Parse Error" => ErrorType::ParseError, + _ => { + return Err(serde::de::Error::unknown_variant( + &s, + &[ + "Connection Error", + "Timeout", + "HTTP Status: ", + "Parse Error", + ], + )) + } + }; + Some(error_type) + } + None => None, + }; + + Ok(error_type) +} + #[cfg(test)] mod tests { use super::*; diff --git a/hook-common/src/kafka_messages/mod.rs b/hook-common/src/kafka_messages/mod.rs index 72b49e1e45059..f548563af5ba1 100644 --- a/hook-common/src/kafka_messages/mod.rs +++ b/hook-common/src/kafka_messages/mod.rs @@ -1,30 +1,25 @@ pub mod app_metrics; pub mod plugin_logs; -use chrono::{DateTime, Utc}; -use serde::Serializer; -use uuid::Uuid; +use chrono::{DateTime, NaiveDateTime, Utc}; +use serde::{Deserialize, Deserializer, Serializer}; -pub fn serialize_uuid(uuid: &Uuid, serializer: S) -> Result +pub fn serialize_datetime(datetime: &DateTime, serializer: S) -> Result where S: Serializer, { - serializer.serialize_str(&uuid.to_string()) + serializer.serialize_str(&datetime.format("%Y-%m-%d %H:%M:%S").to_string()) } -pub fn serialize_optional_uuid(uuid: &Option, serializer: S) -> Result +pub fn deserialize_datetime<'de, D>(deserializer: D) -> Result, D::Error> where - S: Serializer, + D: Deserializer<'de>, { - match uuid { - Some(uuid) => serializer.serialize_str(&uuid.to_string()), - None => serializer.serialize_none(), - } -} + let formatted: String = Deserialize::deserialize(deserializer)?; + let datetime = match NaiveDateTime::parse_from_str(&formatted, "%Y-%m-%d %H:%M:%S") { + Ok(d) => d.and_utc(), + Err(_) => return Err(serde::de::Error::custom("Invalid datetime format")), + }; -pub fn serialize_datetime(datetime: &DateTime, serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_str(&datetime.format("%Y-%m-%d %H:%M:%S%.f").to_string()) + Ok(datetime) } diff --git a/hook-common/src/kafka_messages/plugin_logs.rs b/hook-common/src/kafka_messages/plugin_logs.rs index 8f8bb43efea96..e761fa40799ea 100644 --- a/hook-common/src/kafka_messages/plugin_logs.rs +++ b/hook-common/src/kafka_messages/plugin_logs.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Utc}; use serde::{Serialize, Serializer}; use uuid::Uuid; -use super::{serialize_datetime, serialize_uuid}; +use super::serialize_datetime; #[allow(dead_code)] #[derive(Serialize)] @@ -28,7 +28,6 @@ pub struct PluginLogEntry { pub source: PluginLogEntrySource, #[serde(rename = "type", serialize_with = "serialize_type")] pub type_: PluginLogEntryType, - #[serde(serialize_with = "serialize_uuid")] pub id: Uuid, pub team_id: u32, pub plugin_id: u32, @@ -37,7 +36,6 @@ pub struct PluginLogEntry { pub timestamp: DateTime, #[serde(serialize_with = "serialize_message")] pub message: String, - #[serde(serialize_with = "serialize_uuid")] pub instance_id: Uuid, } diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql index e4ea082ebf188..4aeb231febbd3 100644 --- a/hook-janitor/src/fixtures/webhook_cleanup.sql +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -71,7 +71,7 @@ VALUES ), -- team:1, plugin_config:2, failed in hour 20 ( - ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"timeout"}}}'::jsonb], + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '{}', @@ -81,11 +81,81 @@ VALUES ), -- team:1, plugin_config:2, failed in hour 20 (purposeful duplicate) ( - ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"timeout"}}}'::jsonb], + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'failed', 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, failed in hour 20 (different error) + ( + ARRAY ['{"type":"ConnectionError","details":{"error":{"name":"Connection Error"}}}'::jsonb], + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'failed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, failed in hour 21 (different hour) + ( + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 21:01:18.799371+00', + '{}', + 'webhooks', + 'failed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:3, failed in hour 20 (different plugin_config) + ( + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'failed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, failed but in a different queue + ( + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'not-webhooks', + 'failed', + 'https://myhost/endpoint' + ), + -- team:2, plugin_config:4, failed in hour 20 (purposeful duplicate) + ( + ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], + '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'failed', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, available + ( + NULL, + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{"body": "hello world", "headers": {}, "method": "POST", "url": "https://myhost/endpoint"}', + 'webhooks', + 'available', + 'https://myhost/endpoint' + ), + -- team:1, plugin_config:2, running + ( + NULL, + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '2023-12-19 20:01:18.799371+00', + '{}', + 'webhooks', + 'running', + 'https://myhost/endpoint' ); \ No newline at end of file diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 5ccf6dca701c3..fb48a57ea2421 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -124,6 +124,12 @@ impl From for AppMetric { // that has set the isolation level to serializable. struct SerializableTxn<'a>(Transaction<'a, Postgres>); +struct CleanupStats { + rows_processed: u64, + completed_agg_row_count: usize, + failed_agg_row_count: usize, +} + impl WebhookCleaner { pub fn new( queue_name: &str, @@ -312,7 +318,7 @@ impl WebhookCleaner { Ok(()) } - async fn cleanup_impl(&self) -> Result<()> { + async fn cleanup_impl(&self) -> Result { debug!("WebhookCleaner starting cleanup"); // Note that we select all completed and failed rows without any pagination at the moment. @@ -343,18 +349,17 @@ impl WebhookCleaner { row_count }; + let mut rows_processed = 0; if completed_agg_row_count + failed_agg_row_count != 0 { - let rows_deleted = self.delete_observed_rows(&mut tx).await?; + rows_processed = self.delete_observed_rows(&mut tx).await?; self.commit_txn(tx).await?; - debug!( - "WebhookCleaner finished cleanup, processed and deleted {} rows ({}/{} aggregated completed/failed rows)", - rows_deleted, completed_agg_row_count, failed_agg_row_count - ); - } else { - debug!("WebhookCleaner finished cleanup, there were no rows to process"); } - Ok(()) + Ok(CleanupStats { + rows_processed, + completed_agg_row_count, + failed_agg_row_count, + }) } } @@ -362,7 +367,18 @@ impl WebhookCleaner { impl Cleaner for WebhookCleaner { async fn cleanup(&self) { match self.cleanup_impl().await { - Ok(_) => {} + Ok(stats) => { + if stats.rows_processed > 0 { + debug!( + rows_processed = stats.rows_processed, + completed_agg_row_count = stats.completed_agg_row_count, + failed_agg_row_count = stats.failed_agg_row_count, + "WebhookCleaner::cleanup finished" + ); + } else { + debug!("WebhookCleaner finished cleanup, there were no rows to process"); + } + } Err(error) => { error!(error = ?error, "WebhookCleaner::cleanup failed"); } @@ -375,9 +391,18 @@ mod tests { use super::*; use crate::config; use crate::kafka_producer::{create_kafka_producer, KafkaContext}; + use hook_common::kafka_messages::app_metrics::{ + Error as WebhookError, ErrorDetails, ErrorType, + }; + use hook_common::pgqueue::{NewJob, PgJob, PgQueue, RetryPolicy}; + use hook_common::webhook::{HttpMethod, WebhookJobMetadata, WebhookJobParameters}; + use rdkafka::consumer::{Consumer, StreamConsumer}; use rdkafka::mocking::MockCluster; use rdkafka::producer::{DefaultProducerContext, FutureProducer}; - use sqlx::PgPool; + use rdkafka::{ClientConfig, Message}; + use sqlx::{PgPool, Row}; + use std::collections::HashMap; + use std::str::FromStr; const APP_METRICS_TOPIC: &str = "app_metrics"; @@ -406,6 +431,18 @@ mod tests { ) } + fn check_app_metric_vector_equality(v1: &[AppMetric], v2: &[AppMetric]) { + // Ignores `error_uuid`s. + assert_eq!(v1.len(), v2.len()); + for (item1, item2) in v1.iter().zip(v2) { + let mut item1 = item1.clone(); + item1.error_uuid = None; + let mut item2 = item2.clone(); + item2.error_uuid = None; + assert_eq!(item1, item2); + } + } + #[sqlx::test(migrations = "../migrations", fixtures("webhook_cleanup"))] async fn test_cleanup_impl(db: PgPool) { let (mock_cluster, mock_producer) = create_mock_kafka().await; @@ -413,37 +450,312 @@ mod tests { .create_topic(APP_METRICS_TOPIC, 1, 1) .expect("failed to create mock app_metrics topic"); - let table_name = "job_queue"; - let queue_name = "webhooks"; + let consumer: StreamConsumer = ClientConfig::new() + .set("bootstrap.servers", mock_cluster.bootstrap_servers()) + .set("group.id", "mock") + .set("auto.offset.reset", "earliest") + .create() + .expect("failed to create mock consumer"); + consumer.subscribe(&[APP_METRICS_TOPIC]).unwrap(); let webhook_cleaner = WebhookCleaner::new_from_pool( - &queue_name, - &table_name, + &"webhooks", + &"job_queue", db, mock_producer, APP_METRICS_TOPIC.to_owned(), ) .expect("unable to create webhook cleaner"); - let _ = webhook_cleaner + let cleanup_stats = webhook_cleaner .cleanup_impl() .await .expect("webbook cleanup_impl failed"); - // TODO: I spent a lot of time trying to get the mock Kafka consumer to work, but I think - // I've identified an issue with the rust-rdkafka library: - // https://github.com/fede1024/rust-rdkafka/issues/629#issuecomment-1863555417 - // - // I wanted to test the messages put on the AppMetrics topic, but I think we need to figure - // out that issue about in order to do so. (Capture uses the MockProducer but not a - // Consumer, fwiw.) - // - // For now, I'll probably have to make `cleanup_impl` return the row information so at - // least we can inspect that for correctness. + // Rows from other queues and rows that are not 'completed' or 'failed' should not be + // processed. + assert_eq!(cleanup_stats.rows_processed, 11); + + let mut received_app_metrics = Vec::new(); + for _ in 0..(cleanup_stats.completed_agg_row_count + cleanup_stats.failed_agg_row_count) { + let kafka_msg = consumer.recv().await.unwrap(); + let payload_str = String::from_utf8(kafka_msg.payload().unwrap().to_vec()).unwrap(); + let app_metric: AppMetric = serde_json::from_str(&payload_str).unwrap(); + received_app_metrics.push(app_metric); + } + + let expected_app_metrics = vec![ + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T20:00:00Z").unwrap(), + team_id: 1, + plugin_config_id: 2, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 2, + successes_on_retry: 0, + failures: 0, + error_uuid: None, + error_type: None, + error_details: None, + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T20:00:00Z").unwrap(), + team_id: 1, + plugin_config_id: 3, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 1, + successes_on_retry: 0, + failures: 0, + error_uuid: None, + error_type: None, + error_details: None, + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T20:00:00Z").unwrap(), + team_id: 2, + plugin_config_id: 4, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 1, + successes_on_retry: 0, + failures: 0, + error_uuid: None, + error_type: None, + error_details: None, + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T21:00:00Z").unwrap(), + team_id: 1, + plugin_config_id: 2, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 1, + successes_on_retry: 0, + failures: 0, + error_uuid: None, + error_type: None, + error_details: None, + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T20:00:00Z").unwrap(), + team_id: 1, + plugin_config_id: 2, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 0, + successes_on_retry: 0, + failures: 1, + error_uuid: Some(Uuid::parse_str("018c8935-d038-714a-957c-0df43d42e377").unwrap()), + error_type: Some(ErrorType::ConnectionError), + error_details: Some(ErrorDetails { + error: WebhookError { + name: "Connection Error".to_owned(), + message: None, + stack: None, + }, + }), + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T20:00:00Z").unwrap(), + team_id: 1, + plugin_config_id: 2, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 0, + successes_on_retry: 0, + failures: 2, + error_uuid: Some(Uuid::parse_str("018c8935-d038-714a-957c-0df43d42e377").unwrap()), + error_type: Some(ErrorType::TimeoutError), + error_details: Some(ErrorDetails { + error: WebhookError { + name: "Timeout".to_owned(), + message: None, + stack: None, + }, + }), + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T20:00:00Z").unwrap(), + team_id: 1, + plugin_config_id: 3, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 0, + successes_on_retry: 0, + failures: 1, + error_uuid: Some(Uuid::parse_str("018c8935-d038-714a-957c-0df43d42e377").unwrap()), + error_type: Some(ErrorType::TimeoutError), + error_details: Some(ErrorDetails { + error: WebhookError { + name: "Timeout".to_owned(), + message: None, + stack: None, + }, + }), + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T20:00:00Z").unwrap(), + team_id: 2, + plugin_config_id: 4, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 0, + successes_on_retry: 0, + failures: 1, + error_uuid: Some(Uuid::parse_str("018c8935-d038-714a-957c-0df43d42e377").unwrap()), + error_type: Some(ErrorType::TimeoutError), + error_details: Some(ErrorDetails { + error: WebhookError { + name: "Timeout".to_owned(), + message: None, + stack: None, + }, + }), + }, + AppMetric { + timestamp: DateTime::::from_str("2023-12-19T21:00:00Z").unwrap(), + team_id: 1, + plugin_config_id: 2, + job_id: None, + category: AppMetricCategory::Webhook, + successes: 0, + successes_on_retry: 0, + failures: 1, + error_uuid: Some(Uuid::parse_str("018c8935-d038-714a-957c-0df43d42e377").unwrap()), + error_type: Some(ErrorType::TimeoutError), + error_details: Some(ErrorDetails { + error: WebhookError { + name: "Timeout".to_owned(), + message: None, + stack: None, + }, + }), + }, + ]; + + check_app_metric_vector_equality(&expected_app_metrics, &received_app_metrics); } - // #[sqlx::test] - // async fn test_serializable_isolation() { - // TODO: I'm going to add a test that verifies new rows aren't visible during the txn. - // } + #[sqlx::test(migrations = "../migrations", fixtures("webhook_cleanup"))] + async fn test_serializable_isolation(db: PgPool) { + let (_, mock_producer) = create_mock_kafka().await; + let webhook_cleaner = WebhookCleaner::new_from_pool( + &"webhooks", + &"job_queue", + db.clone(), + mock_producer, + APP_METRICS_TOPIC.to_owned(), + ) + .expect("unable to create webhook cleaner"); + + let queue = + PgQueue::new_from_pool("webhooks", "job_queue", db.clone(), RetryPolicy::default()) + .await + .expect("failed to connect to local test postgresql database"); + + async fn get_count_from_new_conn(db: &PgPool, status: &str) -> i64 { + let mut conn = db.acquire().await.unwrap(); + let count: i64 = sqlx::query( + "SELECT count(*) FROM job_queue WHERE queue = 'webhooks' AND status = $1::job_status", + ) + .bind(&status) + .fetch_one(&mut *conn) + .await + .unwrap() + .get(0); + count + } + + // Important! Serializable txn is started here. + let mut tx = webhook_cleaner.start_serializable_txn().await.unwrap(); + webhook_cleaner.get_completed_rows(&mut tx).await.unwrap(); + webhook_cleaner.get_failed_rows(&mut tx).await.unwrap(); + + // All 13 rows in the queue are visible from outside the txn. + // The 11 the cleaner will process, plus 1 available and 1 running. + assert_eq!(get_count_from_new_conn(&db, "completed").await, 5); + assert_eq!(get_count_from_new_conn(&db, "failed").await, 6); + assert_eq!(get_count_from_new_conn(&db, "available").await, 1); + assert_eq!(get_count_from_new_conn(&db, "running").await, 1); + + { + // The fixtures include an available job, so let's complete it while the txn is open. + let webhook_job: PgJob = queue + .dequeue(&"worker_id") + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + webhook_job + .complete() + .await + .expect("failed to complete job"); + } + + { + // Enqueue and complete another job while the txn is open. + let job_parameters = WebhookJobParameters { + body: "foo".to_owned(), + headers: HashMap::new(), + method: HttpMethod::POST, + url: "http://example.com".to_owned(), + }; + let job_metadata = WebhookJobMetadata { + team_id: 1, + plugin_id: 2, + plugin_config_id: 3, + }; + let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); + queue.enqueue(new_job).await.expect("failed to enqueue job"); + let webhook_job: PgJob = queue + .dequeue(&"worker_id") + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + webhook_job + .complete() + .await + .expect("failed to complete job"); + } + + { + // Enqueue another available job while the txn is open. + let job_parameters = WebhookJobParameters { + body: "foo".to_owned(), + headers: HashMap::new(), + method: HttpMethod::POST, + url: "http://example.com".to_owned(), + }; + let job_metadata = WebhookJobMetadata { + team_id: 1, + plugin_id: 2, + plugin_config_id: 3, + }; + let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); + queue.enqueue(new_job).await.expect("failed to enqueue job"); + } + + // There are now 2 more completed rows (jobs added above) than before, visible from outside the txn. + assert_eq!(get_count_from_new_conn(&db, "completed").await, 7); + assert_eq!(get_count_from_new_conn(&db, "available").await, 1); + + let rows_processed = webhook_cleaner.delete_observed_rows(&mut tx).await.unwrap(); + // The 11 rows that were in the queue when the txn started should be deleted. + assert_eq!(rows_processed, 11); + + // We haven't committed, so the rows are still visible from outside the txn. + assert_eq!(get_count_from_new_conn(&db, "completed").await, 7); + assert_eq!(get_count_from_new_conn(&db, "available").await, 1); + + webhook_cleaner.commit_txn(tx).await.unwrap(); + + // We have committed, what remains are: + // * The 1 available job we completed while the txn was open. + // * The 2 brand new jobs we added while the txn was open. + // * The 1 running job that didn't change. + assert_eq!(get_count_from_new_conn(&db, "completed").await, 2); + assert_eq!(get_count_from_new_conn(&db, "failed").await, 0); + assert_eq!(get_count_from_new_conn(&db, "available").await, 1); + assert_eq!(get_count_from_new_conn(&db, "running").await, 1); + } } From d6035105e36ac604dad7bfa8805ed5608c92be22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 21 Dec 2023 12:10:26 +0100 Subject: [PATCH 144/249] refactor: Only one run method --- hook-consumer/src/consumer.rs | 73 +++++++++++++++++++++-------------- hook-consumer/src/main.rs | 6 +-- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 73bac1cffba87..921a0651bc2ba 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -109,42 +109,55 @@ impl<'p> WebhookConsumer<'p> { } /// Run this consumer to continuously process any jobs that become available. - pub async fn run(&self) -> Result<(), ConsumerError> { + pub async fn run(&self, transactional: bool) -> Result<(), ConsumerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); - loop { - let webhook_job = self.wait_for_job().await?; - - // reqwest::Client internally wraps with Arc, so this allocation is cheap. - let client = self.client.clone(); - let permit = semaphore.clone().acquire_owned().await.unwrap(); - - tokio::spawn(async move { - let result = process_webhook_job(client, webhook_job).await; - drop(permit); - result - }); + if transactional { + loop { + let webhook_job = self.wait_for_job_tx().await?; + spawn_webhook_job_processing_task( + self.client.clone(), + semaphore.clone(), + webhook_job, + ) + .await; + } + } else { + loop { + let webhook_job = self.wait_for_job().await?; + spawn_webhook_job_processing_task( + self.client.clone(), + semaphore.clone(), + webhook_job, + ) + .await; + } } } +} - /// Run this consumer to continuously process any jobs that become available in transactional mode. - pub async fn run_tx(&self) -> Result<(), ConsumerError> { - let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); - - loop { - let webhook_job = self.wait_for_job_tx().await?; - - // reqwest::Client internally wraps with Arc, so this allocation is cheap. - let client = self.client.clone(); - let permit = semaphore.clone().acquire_owned().await.unwrap(); +/// Spawn a Tokio task to process a Webhook Job once we successfully acquire a permit. +/// +/// # Arguments +/// +/// * `client`: An HTTP client to execute the webhook job request. +/// * `semaphore`: A semaphore used for rate limiting purposes. This function will panic if this semaphore is closed. +/// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. +async fn spawn_webhook_job_processing_task( + client: reqwest::Client, + semaphore: Arc, + webhook_job: W, +) -> tokio::task::JoinHandle> { + let permit = semaphore + .acquire_owned() + .await + .expect("semaphore has been closed"); - tokio::spawn(async move { - let result = process_webhook_job(client, webhook_job).await; - drop(permit); - result - }); - } - } + tokio::spawn(async move { + let result = process_webhook_job(client, webhook_job).await; + drop(permit); + result + }) } /// Process a webhook job by transitioning it to its appropriate state after its request is sent. diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index 49c2e76a8610f..86b8094a36218 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -31,11 +31,7 @@ async fn main() -> Result<(), ConsumerError> { config.max_concurrent_jobs, ); - if config.transactional { - consumer.run_tx().await?; - } else { - consumer.run().await?; - } + consumer.run(config.transactional).await?; Ok(()) } From fe972c7a65208bea3412f9d0b59d2e467897a3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 8 Dec 2023 16:50:39 +0100 Subject: [PATCH 145/249] feat: Give non-transactional consumer a chance --- hook-consumer/src/consumer.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 921a0651bc2ba..19fa0a831300e 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -51,6 +51,8 @@ pub struct WebhookConsumer<'p> { client: reqwest::Client, /// Maximum number of concurrent jobs being processed. max_concurrent_jobs: usize, + /// Indicates whether we are holding an open transaction while processing or not. + transactional: bool, } impl<'p> WebhookConsumer<'p> { @@ -82,6 +84,19 @@ impl<'p> WebhookConsumer<'p> { } } + /// Wait until a job becomes available in our queue. + async fn wait_for_job_tx<'a>( + &self, + ) -> Result, WebhookConsumerError> { + loop { + if let Some(job) = self.queue.dequeue_tx(&self.name).await? { + return Ok(job); + } else { + task::sleep(self.poll_interval).await; + } + } + } + /// Wait until a job becomes available in our queue. async fn wait_for_job<'a>( &self, From dc97d8c168924ac372d487489d843fbdddaf2e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 14 Dec 2023 12:30:31 +0100 Subject: [PATCH 146/249] refactor: Two clients one for each mode --- hook-consumer/src/consumer.rs | 130 ++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 6 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 19fa0a831300e..0127d25a8284c 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -51,8 +51,6 @@ pub struct WebhookConsumer<'p> { client: reqwest::Client, /// Maximum number of concurrent jobs being processed. max_concurrent_jobs: usize, - /// Indicates whether we are holding an open transaction while processing or not. - transactional: bool, } impl<'p> WebhookConsumer<'p> { @@ -85,11 +83,9 @@ impl<'p> WebhookConsumer<'p> { } /// Wait until a job becomes available in our queue. - async fn wait_for_job_tx<'a>( - &self, - ) -> Result, WebhookConsumerError> { + async fn wait_for_job<'a>(&self) -> Result, WebhookConsumerError> { loop { - if let Some(job) = self.queue.dequeue_tx(&self.name).await? { + if let Some(job) = self.queue.dequeue(&self.name).await? { return Ok(job); } else { task::sleep(self.poll_interval).await; @@ -97,6 +93,68 @@ impl<'p> WebhookConsumer<'p> { } } + /// Run this consumer to continuously process any jobs that become available. + pub async fn run(&self) -> Result<(), WebhookConsumerError> { + let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); + + loop { + let webhook_job = self.wait_for_job().await?; + + // reqwest::Client internally wraps with Arc, so this allocation is cheap. + let client = self.client.clone(); + let permit = semaphore.clone().acquire_owned().await.unwrap(); + + tokio::spawn(async move { + let result = process_webhook_job(client, webhook_job).await; + drop(permit); + result + }); + } + } +} + +/// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. +pub struct WebhookTransactionConsumer<'p> { + /// An identifier for this consumer. Used to mark jobs we have consumed. + name: String, + /// The queue we will be dequeuing jobs from. + queue: &'p PgQueue, + /// The interval for polling the queue. + poll_interval: time::Duration, + /// The client used for HTTP requests. + client: reqwest::Client, + /// Maximum number of concurrent jobs being processed. + max_concurrent_jobs: usize, +} + +impl<'p> WebhookTransactionConsumer<'p> { + pub fn new( + name: &str, + queue: &'p PgQueue, + poll_interval: time::Duration, + request_timeout: time::Duration, + max_concurrent_jobs: usize, + ) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + + let client = reqwest::Client::builder() + .default_headers(headers) + .timeout(request_timeout) + .build()?; + + Ok(Self { + name: name.to_owned(), + queue, + poll_interval, + client, + max_concurrent_jobs, + }) + } + /// Wait until a job becomes available in our queue. async fn wait_for_job<'a>( &self, @@ -259,6 +317,66 @@ async fn process_webhook_job( } } +/// Process a webhook job by transitioning it to its appropriate state after its request is sent. +/// After we finish, the webhook job will be set as completed (if the request was successful), retryable (if the request +/// was unsuccessful but we can still attempt a retry), or failed (if the request was unsuccessful and no more retries +/// may be attempted). +/// +/// A webhook job is considered retryable after a failing request if: +/// 1. The job has attempts remaining (i.e. hasn't reached `max_attempts`), and... +/// 2. The status code indicates retrying at a later point could resolve the issue. This means: 429 and any 5XX. +/// +/// # Arguments +/// +/// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. +/// * `request_timeout`: A timeout for the HTTP request. +async fn process_webhook_job( + client: reqwest::Client, + webhook_job: PgJob, +) -> Result<(), WebhookConsumerError> { + match send_webhook( + client, + &webhook_job.job.parameters.method, + &webhook_job.job.parameters.url, + &webhook_job.job.parameters.headers, + webhook_job.job.parameters.body.clone(), + ) + .await + { + Ok(_) => { + webhook_job + .complete() + .await + .map_err(|error| WebhookConsumerError::PgJobError(error.to_string()))?; + Ok(()) + } + Err(WebhookConsumerError::RetryableWebhookError { + reason, + retry_after, + }) => match webhook_job.retry(reason.to_string(), retry_after).await { + Ok(_) => Ok(()), + Err(PgJobError::RetryInvalidError { + job: webhook_job, + error: fail_error, + }) => { + webhook_job + .fail(fail_error.to_string()) + .await + .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), + }, + Err(error) => { + webhook_job + .fail(error.to_string()) + .await + .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; + Ok(()) + } + } +} + /// Make an HTTP request to a webhook endpoint. /// /// # Arguments From acd0d1379768fa035baa9fb73fadecca149a6c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 14 Dec 2023 14:49:50 +0100 Subject: [PATCH 147/249] refactor: PgQueue no longer takes a RetryPolicy --- hook-common/src/pgqueue.rs | 97 +++-------------------------------- hook-common/src/retry.rs | 55 ++++++++++++++++++++ hook-consumer/src/consumer.rs | 48 +++++++++++------ hook-consumer/src/main.rs | 13 ++--- 4 files changed, 98 insertions(+), 115 deletions(-) create mode 100644 hook-common/src/retry.rs diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 78fe2fc21c3f7..509d84e35efb2 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -1,8 +1,6 @@ //! # PgQueue //! //! A job queue implementation backed by a PostgreSQL table. - -use std::default::Default; use std::str::FromStr; use std::time; @@ -172,7 +170,6 @@ pub struct PgJob { pub job: Job, pub table: String, pub connection: sqlx::pool::PoolConnection, - pub retry_policy: RetryPolicy, } #[async_trait] @@ -259,9 +256,6 @@ RETURNING }); } let retryable_job = self.job.retry(error); - let retry_interval = self - .retry_policy - .time_until_next_retry(&retryable_job, preferred_retry_interval); let base_query = format!( r#" @@ -304,7 +298,6 @@ pub struct PgTransactionJob<'c, J, M> { pub job: Job, pub table: String, pub transaction: sqlx::Transaction<'c, sqlx::postgres::Postgres>, - pub retry_policy: RetryPolicy, } #[async_trait] @@ -408,9 +401,6 @@ RETURNING }); } let retryable_job = self.job.retry(error); - let retry_interval = self - .retry_policy - .time_until_next_retry(&retryable_job, preferred_retry_interval); let base_query = format!( r#" @@ -509,61 +499,6 @@ impl NewJob { } } -#[derive(Copy, Clone, Debug)] -/// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. -pub struct RetryPolicy { - /// Coefficient to multiply initial_interval with for every past attempt. - backoff_coefficient: u32, - /// The backoff interval for the first retry. - initial_interval: time::Duration, - /// The maximum possible backoff between retries. - maximum_interval: Option, -} - -impl RetryPolicy { - pub fn new( - backoff_coefficient: u32, - initial_interval: time::Duration, - maximum_interval: Option, - ) -> Self { - Self { - backoff_coefficient, - initial_interval, - maximum_interval, - } - } - - /// Calculate the time until the next retry for a given RetryableJob. - pub fn time_until_next_retry( - &self, - job: &RetryableJob, - preferred_retry_interval: Option, - ) -> time::Duration { - let candidate_interval = - self.initial_interval * self.backoff_coefficient.pow(job.attempt as u32); - - match (preferred_retry_interval, self.maximum_interval) { - (Some(duration), Some(max_interval)) => std::cmp::min( - std::cmp::max(std::cmp::min(candidate_interval, max_interval), duration), - max_interval, - ), - (Some(duration), None) => std::cmp::max(candidate_interval, duration), - (None, Some(max_interval)) => std::cmp::min(candidate_interval, max_interval), - (None, None) => candidate_interval, - } - } -} - -impl Default for RetryPolicy { - fn default() -> Self { - Self { - backoff_coefficient: 2, - initial_interval: time::Duration::from_secs(1), - maximum_interval: None, - } - } -} - /// A queue implemented on top of a PostgreSQL table. #[derive(Clone)] pub struct PgQueue { @@ -571,8 +506,6 @@ pub struct PgQueue { name: String, /// A connection pool used to connect to the PostgreSQL database. pool: PgPool, - /// The retry policy to be assigned to Jobs in this PgQueue. - retry_policy: RetryPolicy, /// The identifier of the PostgreSQL table this queue runs on. table: String, } @@ -588,25 +521,14 @@ impl PgQueue { /// * `table_name`: The name for the table the queue will use in PostgreSQL. /// * `url`: A URL pointing to where the PostgreSQL database is hosted. /// * `worker_name`: The name of the worker that is operating with this queue. - /// * `retry_policy`: A retry policy to pass to jobs from this queue. - pub async fn new( - queue_name: &str, - table_name: &str, - url: &str, - retry_policy: RetryPolicy, - ) -> PgQueueResult { + pub async fn new(queue_name: &str, table_name: &str, url: &str) -> PgQueueResult { let name = queue_name.to_owned(); let table = table_name.to_owned(); let pool = PgPoolOptions::new() .connect_lazy(url) .map_err(|error| PgQueueError::PoolCreationError { error })?; - Ok(Self { - name, - pool, - retry_policy, - table, - }) + Ok(Self { name, pool, table }) } /// Dequeue a Job from this PgQueue to work on it. @@ -669,7 +591,6 @@ RETURNING job, table: self.table.to_owned(), connection, - retry_policy: self.retry_policy, })), // Although connection would be closed once it goes out of scope, sqlx recommends explicitly calling close(). @@ -749,7 +670,6 @@ RETURNING job, table: self.table.to_owned(), transaction: tx, - retry_policy: self.retry_policy, })), // Transaction is rolledback on drop. @@ -801,6 +721,7 @@ VALUES #[cfg(test)] mod tests { use super::*; + use crate::retry::RetryPolicy; #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] struct JobMetadata { @@ -858,7 +779,6 @@ mod tests { "test_can_dequeue_job", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); @@ -887,7 +807,6 @@ mod tests { "test_dequeue_returns_none_on_no_jobs", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); @@ -912,7 +831,6 @@ mod tests { "test_can_dequeue_tx_job", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); @@ -942,7 +860,6 @@ mod tests { "test_dequeue_tx_returns_none_on_no_jobs", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to connect to local test postgresql database"); @@ -972,7 +889,6 @@ mod tests { "test_can_retry_job_with_remaining_attempts", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - retry_policy, ) .await .expect("failed to connect to local test postgresql database"); @@ -983,8 +899,9 @@ mod tests { .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); + let retry_interval = retry_policy.time_until_next_retry(job.job.attempt as u32, None); let _ = job - .retry("a very reasonable failure reason", None) + .retry("a very reasonable failure reason", retry_interval) .await .expect("failed to retry job"); let retried_job: PgJob = queue @@ -1023,7 +940,6 @@ mod tests { "test_cannot_retry_job_without_remaining_attempts", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - retry_policy, ) .await .expect("failed to connect to local test postgresql database"); @@ -1035,7 +951,8 @@ mod tests { .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); - job.retry("a very reasonable failure reason", None) + let retry_interval = retry_policy.time_until_next_retry(job.job.attempt as u32, None); + job.retry("a very reasonable failure reason", retry_interval) .await .expect("failed to retry job"); } diff --git a/hook-common/src/retry.rs b/hook-common/src/retry.rs new file mode 100644 index 0000000000000..140da192a8b32 --- /dev/null +++ b/hook-common/src/retry.rs @@ -0,0 +1,55 @@ +use std::time; + +#[derive(Copy, Clone, Debug)] +/// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. +pub struct RetryPolicy { + /// Coefficient to multiply initial_interval with for every past attempt. + backoff_coefficient: u32, + /// The backoff interval for the first retry. + initial_interval: time::Duration, + /// The maximum possible backoff between retries. + maximum_interval: Option, +} + +impl RetryPolicy { + pub fn new( + backoff_coefficient: u32, + initial_interval: time::Duration, + maximum_interval: Option, + ) -> Self { + Self { + backoff_coefficient, + initial_interval, + maximum_interval, + } + } + + /// Calculate the time until the next retry for a given RetryableJob. + pub fn time_until_next_retry( + &self, + attempt: u32, + preferred_retry_interval: Option, + ) -> time::Duration { + let candidate_interval = self.initial_interval * self.backoff_coefficient.pow(attempt); + + match (preferred_retry_interval, self.maximum_interval) { + (Some(duration), Some(max_interval)) => std::cmp::min( + std::cmp::max(std::cmp::min(candidate_interval, max_interval), duration), + max_interval, + ), + (Some(duration), None) => std::cmp::max(candidate_interval, duration), + (None, Some(max_interval)) => std::cmp::min(candidate_interval, max_interval), + (None, None) => candidate_interval, + } + } +} + +impl Default for RetryPolicy { + fn default() -> Self { + Self { + backoff_coefficient: 2, + initial_interval: time::Duration::from_secs(1), + maximum_interval: None, + } + } +} diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 0127d25a8284c..d6ebac7517e06 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -51,6 +51,8 @@ pub struct WebhookConsumer<'p> { client: reqwest::Client, /// Maximum number of concurrent jobs being processed. max_concurrent_jobs: usize, + /// The retry policy used to calculate retry intervals when a job fails with a retryable error. + retry_policy: RetryPolicy, } impl<'p> WebhookConsumer<'p> { @@ -96,6 +98,7 @@ impl<'p> WebhookConsumer<'p> { /// Run this consumer to continuously process any jobs that become available. pub async fn run(&self) -> Result<(), WebhookConsumerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); + let retry_policy = self.retry_policy.clone(); loop { let webhook_job = self.wait_for_job().await?; @@ -105,7 +108,7 @@ impl<'p> WebhookConsumer<'p> { let permit = semaphore.clone().acquire_owned().await.unwrap(); tokio::spawn(async move { - let result = process_webhook_job(client, webhook_job).await; + let result = process_webhook_job(client, webhook_job, &retry_policy).await; drop(permit); result }); @@ -125,6 +128,8 @@ pub struct WebhookTransactionConsumer<'p> { client: reqwest::Client, /// Maximum number of concurrent jobs being processed. max_concurrent_jobs: usize, + /// The retry policy used to calculate retry intervals when a job fails with a retryable error. + retry_policy: RetryPolicy, } impl<'p> WebhookTransactionConsumer<'p> { @@ -134,6 +139,7 @@ impl<'p> WebhookTransactionConsumer<'p> { poll_interval: time::Duration, request_timeout: time::Duration, max_concurrent_jobs: usize, + retry_policy: RetryPolicy, ) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( @@ -152,6 +158,7 @@ impl<'p> WebhookTransactionConsumer<'p> { poll_interval, client, max_concurrent_jobs, + retry_policy, }) } @@ -184,6 +191,7 @@ impl<'p> WebhookTransactionConsumer<'p> { /// Run this consumer to continuously process any jobs that become available. pub async fn run(&self, transactional: bool) -> Result<(), ConsumerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); + let retry_policy = self.retry_policy.clone(); if transactional { loop { @@ -333,6 +341,7 @@ async fn process_webhook_job( async fn process_webhook_job( client: reqwest::Client, webhook_job: PgJob, + retry_policy: &RetryPolicy, ) -> Result<(), WebhookConsumerError> { match send_webhook( client, @@ -353,20 +362,27 @@ async fn process_webhook_job( Err(WebhookConsumerError::RetryableWebhookError { reason, retry_after, - }) => match webhook_job.retry(reason.to_string(), retry_after).await { - Ok(_) => Ok(()), - Err(PgJobError::RetryInvalidError { - job: webhook_job, - error: fail_error, - }) => { - webhook_job - .fail(fail_error.to_string()) - .await - .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; - Ok(()) + }) => { + let retry_interval = + retry_policy.time_until_next_retry(webhook_job.job.attempt as u32, retry_after); + + match webhook_job.retry(reason.to_string(), retry_interval).await { + Ok(_) => Ok(()), + Err(PgJobError::RetryInvalidError { + job: webhook_job, + error: fail_error, + }) => { + webhook_job + .fail(fail_error.to_string()) + .await + .map_err(|job_error| { + WebhookConsumerError::PgJobError(job_error.to_string()) + })?; + Ok(()) + } + Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), } - Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), - }, + } Err(error) => { webhook_job .fail(error.to_string()) @@ -479,7 +495,7 @@ mod tests { // This is due to a long-standing cargo bug that reports imports and helper functions as unused. // See: https://github.com/rust-lang/rust/issues/46379. #[allow(unused_imports)] - use hook_common::pgqueue::{JobStatus, NewJob, RetryPolicy}; + use hook_common::pgqueue::{JobStatus, NewJob}; /// Use process id as a worker id for tests. #[allow(dead_code)] @@ -536,7 +552,7 @@ mod tests { let queue_name = "test_wait_for_job".to_string(); let table_name = "job_queue".to_string(); let db_url = "postgres://posthog:posthog@localhost:15432/test_database".to_string(); - let queue = PgQueue::new(&queue_name, &table_name, &db_url, RetryPolicy::default()) + let queue = PgQueue::new(&queue_name, &table_name, &db_url) .await .expect("failed to connect to PG"); diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index 86b8094a36218..dd9d3e7979e14 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -1,6 +1,6 @@ use envconfig::Envconfig; -use hook_common::pgqueue::{PgQueue, RetryPolicy}; +use hook_common::{pgqueue::PgQueue, retry::RetryPolicy}; use hook_consumer::config::Config; use hook_consumer::consumer::WebhookConsumer; use hook_consumer::error::ConsumerError; @@ -14,14 +14,9 @@ async fn main() -> Result<(), ConsumerError> { config.retry_policy.initial_interval.0, Some(config.retry_policy.maximum_interval.0), ); - let queue = PgQueue::new( - &config.queue_name, - &config.table_name, - &config.database_url, - retry_policy, - ) - .await - .expect("failed to initialize queue"); + let queue = PgQueue::new(&config.queue_name, &config.table_name, &config.database_url) + .await + .expect("failed to initialize queue"); let consumer = WebhookConsumer::new( &config.consumer_name, From 0f61af3ccda26f905dcf44c9b30cb35f4965e764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 21 Dec 2023 12:36:40 +0100 Subject: [PATCH 148/249] fix: Rebase on changes and fix all conflicts --- hook-common/src/lib.rs | 1 + hook-common/src/pgqueue.rs | 6 +- hook-common/src/retry.rs | 6 +- hook-consumer/src/consumer.rs | 177 ++++---------------------- hook-consumer/src/main.rs | 1 + hook-producer/src/handlers/app.rs | 3 +- hook-producer/src/handlers/webhook.rs | 7 +- hook-producer/src/main.rs | 4 +- 8 files changed, 36 insertions(+), 169 deletions(-) diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs index 7d9ef37e84606..8e63ded5a7bf2 100644 --- a/hook-common/src/lib.rs +++ b/hook-common/src/lib.rs @@ -1,4 +1,5 @@ pub mod kafka_messages; pub mod metrics; pub mod pgqueue; +pub mod retry; pub mod webhook; diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 509d84e35efb2..00ce57f0c35b4 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -160,7 +160,7 @@ pub trait PgQueueJob { async fn retry( mut self, error: E, - preferred_retry_interval: Option, + retry_interval: time::Duration, ) -> Result, PgJobError>>; } @@ -247,7 +247,7 @@ RETURNING async fn retry( mut self, error: E, - preferred_retry_interval: Option, + retry_interval: time::Duration, ) -> Result, PgJobError>>> { if self.job.is_gte_max_attempts() { return Err(PgJobError::RetryInvalidError { @@ -392,7 +392,7 @@ RETURNING async fn retry( mut self, error: E, - preferred_retry_interval: Option, + retry_interval: time::Duration, ) -> Result, PgJobError>>> { if self.job.is_gte_max_attempts() { return Err(PgJobError::RetryInvalidError { diff --git a/hook-common/src/retry.rs b/hook-common/src/retry.rs index 140da192a8b32..f72b0d166fdc8 100644 --- a/hook-common/src/retry.rs +++ b/hook-common/src/retry.rs @@ -4,11 +4,11 @@ use std::time; /// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. pub struct RetryPolicy { /// Coefficient to multiply initial_interval with for every past attempt. - backoff_coefficient: u32, + pub backoff_coefficient: u32, /// The backoff interval for the first retry. - initial_interval: time::Duration, + pub initial_interval: time::Duration, /// The maximum possible backoff between retries. - maximum_interval: Option, + pub maximum_interval: Option, } impl RetryPolicy { diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index d6ebac7517e06..b1e507177a924 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -3,10 +3,11 @@ use std::sync::Arc; use std::time; use async_std::task; -use hook_common::pgqueue::{ - PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob, +use hook_common::{ + pgqueue::{PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, + retry::RetryPolicy, + webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}, }; -use hook_common::webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}; use http::StatusCode; use reqwest::header; use tokio::sync; @@ -17,6 +18,7 @@ use crate::error::{ConsumerError, WebhookError}; trait WebhookJob: PgQueueJob + std::marker::Send { fn parameters(&self) -> &WebhookJobParameters; fn metadata(&self) -> &WebhookJobMetadata; + fn attempt(&self) -> i32; } impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata> { @@ -27,6 +29,10 @@ impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadat fn metadata(&self) -> &WebhookJobMetadata { &self.job.metadata } + + fn attempt(&self) -> i32 { + self.job.attempt + } } impl WebhookJob for PgJob { @@ -37,6 +43,10 @@ impl WebhookJob for PgJob { fn metadata(&self) -> &WebhookJobMetadata { &self.job.metadata } + + fn attempt(&self) -> i32 { + self.job.attempt + } } /// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. @@ -62,6 +72,7 @@ impl<'p> WebhookConsumer<'p> { poll_interval: time::Duration, request_timeout: time::Duration, max_concurrent_jobs: usize, + retry_policy: RetryPolicy, ) -> Self { let mut headers = header::HeaderMap::new(); headers.insert( @@ -81,85 +92,8 @@ impl<'p> WebhookConsumer<'p> { poll_interval, client, max_concurrent_jobs, - } - } - - /// Wait until a job becomes available in our queue. - async fn wait_for_job<'a>(&self) -> Result, WebhookConsumerError> { - loop { - if let Some(job) = self.queue.dequeue(&self.name).await? { - return Ok(job); - } else { - task::sleep(self.poll_interval).await; - } - } - } - - /// Run this consumer to continuously process any jobs that become available. - pub async fn run(&self) -> Result<(), WebhookConsumerError> { - let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); - let retry_policy = self.retry_policy.clone(); - - loop { - let webhook_job = self.wait_for_job().await?; - - // reqwest::Client internally wraps with Arc, so this allocation is cheap. - let client = self.client.clone(); - let permit = semaphore.clone().acquire_owned().await.unwrap(); - - tokio::spawn(async move { - let result = process_webhook_job(client, webhook_job, &retry_policy).await; - drop(permit); - result - }); - } - } -} - -/// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. -pub struct WebhookTransactionConsumer<'p> { - /// An identifier for this consumer. Used to mark jobs we have consumed. - name: String, - /// The queue we will be dequeuing jobs from. - queue: &'p PgQueue, - /// The interval for polling the queue. - poll_interval: time::Duration, - /// The client used for HTTP requests. - client: reqwest::Client, - /// Maximum number of concurrent jobs being processed. - max_concurrent_jobs: usize, - /// The retry policy used to calculate retry intervals when a job fails with a retryable error. - retry_policy: RetryPolicy, -} - -impl<'p> WebhookTransactionConsumer<'p> { - pub fn new( - name: &str, - queue: &'p PgQueue, - poll_interval: time::Duration, - request_timeout: time::Duration, - max_concurrent_jobs: usize, - retry_policy: RetryPolicy, - ) -> Result { - let mut headers = header::HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ); - - let client = reqwest::Client::builder() - .default_headers(headers) - .timeout(request_timeout) - .build()?; - - Ok(Self { - name: name.to_owned(), - queue, - poll_interval, - client, - max_concurrent_jobs, retry_policy, - }) + } } /// Wait until a job becomes available in our queue. @@ -191,7 +125,6 @@ impl<'p> WebhookTransactionConsumer<'p> { /// Run this consumer to continuously process any jobs that become available. pub async fn run(&self, transactional: bool) -> Result<(), ConsumerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); - let retry_policy = self.retry_policy.clone(); if transactional { loop { @@ -199,6 +132,7 @@ impl<'p> WebhookTransactionConsumer<'p> { spawn_webhook_job_processing_task( self.client.clone(), semaphore.clone(), + self.retry_policy, webhook_job, ) .await; @@ -209,6 +143,7 @@ impl<'p> WebhookTransactionConsumer<'p> { spawn_webhook_job_processing_task( self.client.clone(), semaphore.clone(), + self.retry_policy, webhook_job, ) .await; @@ -227,6 +162,7 @@ impl<'p> WebhookTransactionConsumer<'p> { async fn spawn_webhook_job_processing_task( client: reqwest::Client, semaphore: Arc, + retry_policy: RetryPolicy, webhook_job: W, ) -> tokio::task::JoinHandle> { let permit = semaphore @@ -235,7 +171,7 @@ async fn spawn_webhook_job_processing_task( .expect("semaphore has been closed"); tokio::spawn(async move { - let result = process_webhook_job(client, webhook_job).await; + let result = process_webhook_job(client, webhook_job, &retry_policy).await; drop(permit); result }) @@ -257,6 +193,7 @@ async fn spawn_webhook_job_processing_task( async fn process_webhook_job( client: reqwest::Client, webhook_job: W, + retry_policy: &RetryPolicy, ) -> Result<(), ConsumerError> { let parameters = webhook_job.parameters(); @@ -298,8 +235,11 @@ async fn process_webhook_job( Ok(()) } Err(WebhookError::RetryableRequestError { error, retry_after }) => { + let retry_interval = + retry_policy.time_until_next_retry(webhook_job.attempt() as u32, retry_after); + match webhook_job - .retry(WebhookJobError::from(&error), retry_after) + .retry(WebhookJobError::from(&error), retry_interval) .await { Ok(_) => Ok(()), @@ -325,74 +265,6 @@ async fn process_webhook_job( } } -/// Process a webhook job by transitioning it to its appropriate state after its request is sent. -/// After we finish, the webhook job will be set as completed (if the request was successful), retryable (if the request -/// was unsuccessful but we can still attempt a retry), or failed (if the request was unsuccessful and no more retries -/// may be attempted). -/// -/// A webhook job is considered retryable after a failing request if: -/// 1. The job has attempts remaining (i.e. hasn't reached `max_attempts`), and... -/// 2. The status code indicates retrying at a later point could resolve the issue. This means: 429 and any 5XX. -/// -/// # Arguments -/// -/// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. -/// * `request_timeout`: A timeout for the HTTP request. -async fn process_webhook_job( - client: reqwest::Client, - webhook_job: PgJob, - retry_policy: &RetryPolicy, -) -> Result<(), WebhookConsumerError> { - match send_webhook( - client, - &webhook_job.job.parameters.method, - &webhook_job.job.parameters.url, - &webhook_job.job.parameters.headers, - webhook_job.job.parameters.body.clone(), - ) - .await - { - Ok(_) => { - webhook_job - .complete() - .await - .map_err(|error| WebhookConsumerError::PgJobError(error.to_string()))?; - Ok(()) - } - Err(WebhookConsumerError::RetryableWebhookError { - reason, - retry_after, - }) => { - let retry_interval = - retry_policy.time_until_next_retry(webhook_job.job.attempt as u32, retry_after); - - match webhook_job.retry(reason.to_string(), retry_interval).await { - Ok(_) => Ok(()), - Err(PgJobError::RetryInvalidError { - job: webhook_job, - error: fail_error, - }) => { - webhook_job - .fail(fail_error.to_string()) - .await - .map_err(|job_error| { - WebhookConsumerError::PgJobError(job_error.to_string()) - })?; - Ok(()) - } - Err(job_error) => Err(WebhookConsumerError::PgJobError(job_error.to_string())), - } - } - Err(error) => { - webhook_job - .fail(error.to_string()) - .await - .map_err(|job_error| WebhookConsumerError::PgJobError(job_error.to_string()))?; - Ok(()) - } - } -} - /// Make an HTTP request to a webhook endpoint. /// /// # Arguments @@ -585,6 +457,7 @@ mod tests { time::Duration::from_millis(100), time::Duration::from_millis(5000), 10, + RetryPolicy::default(), ); let consumed_job = consumer diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index dd9d3e7979e14..3cefc1d04f42d 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -24,6 +24,7 @@ async fn main() -> Result<(), ConsumerError> { config.poll_interval.0, config.request_timeout.0, config.max_concurrent_jobs, + retry_policy, ); consumer.run(config.transactional).await?; diff --git a/hook-producer/src/handlers/app.rs b/hook-producer/src/handlers/app.rs index 1666676fee1bb..78d4dc5663fc7 100644 --- a/hook-producer/src/handlers/app.rs +++ b/hook-producer/src/handlers/app.rs @@ -31,7 +31,7 @@ mod tests { body::Body, http::{Request, StatusCode}, }; - use hook_common::pgqueue::{PgQueue, RetryPolicy}; + use hook_common::pgqueue::PgQueue; use http_body_util::BodyExt; // for `collect` use tower::ServiceExt; // for `call`, `oneshot`, and `ready` @@ -41,7 +41,6 @@ mod tests { "test_index", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to construct pg_queue"); diff --git a/hook-producer/src/handlers/webhook.rs b/hook-producer/src/handlers/webhook.rs index 394732094a3c6..ca0cc3792bef5 100644 --- a/hook-producer/src/handlers/webhook.rs +++ b/hook-producer/src/handlers/webhook.rs @@ -108,7 +108,7 @@ mod tests { body::Body, http::{self, Request, StatusCode}, }; - use hook_common::pgqueue::{PgQueue, RetryPolicy}; + use hook_common::pgqueue::PgQueue; use hook_common::webhook::{HttpMethod, WebhookJobParameters}; use http_body_util::BodyExt; // for `collect` use std::collections; @@ -122,7 +122,6 @@ mod tests { "test_index", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to construct pg_queue"); @@ -171,7 +170,6 @@ mod tests { "test_index", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to construct pg_queue"); @@ -215,7 +213,6 @@ mod tests { "test_index", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to construct pg_queue"); @@ -243,7 +240,6 @@ mod tests { "test_index", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to construct pg_queue"); @@ -271,7 +267,6 @@ mod tests { "test_index", "job_queue", "postgres://posthog:posthog@localhost:15432/test_database", - RetryPolicy::default(), ) .await .expect("failed to construct pg_queue"); diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index 7c2b73c0f12bb..39f45004271bc 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -4,7 +4,7 @@ use envconfig::Envconfig; use eyre::Result; use hook_common::metrics; -use hook_common::pgqueue::{PgQueue, RetryPolicy}; +use hook_common::pgqueue::PgQueue; mod config; mod handlers; @@ -29,8 +29,6 @@ async fn main() { &config.queue_name, &config.table_name, &config.database_url, - // TODO: It seems unnecessary that the producer side needs to know about the retry policy. - RetryPolicy::default(), ) .await .expect("failed to initialize queue"); From 9b7ab99d19327b274adf6c9b2bfb877c8eb0a59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 21 Dec 2023 15:15:59 +0100 Subject: [PATCH 149/249] feat: Provide a function to start serving metrics --- Cargo.lock | 1 + hook-common/Cargo.toml | 1 + hook-common/src/metrics.rs | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 130f765cff81d..ac2c8fc525e44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1035,6 +1035,7 @@ dependencies = [ "futures", "hook-common", "http 0.2.11", + "metrics", "reqwest", "serde", "serde_derive", diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 9b20396a388c9..00c7bd2296a49 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -18,6 +18,7 @@ serde = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } +tokio = { workspace = true } thiserror = { workspace = true } uuid = { workspace = true } diff --git a/hook-common/src/metrics.rs b/hook-common/src/metrics.rs index dbdc7b1fa1107..7d881ea06bc40 100644 --- a/hook-common/src/metrics.rs +++ b/hook-common/src/metrics.rs @@ -2,9 +2,25 @@ use std::time::Instant; use axum::{ body::Body, extract::MatchedPath, http::Request, middleware::Next, response::IntoResponse, + routing::get, Router, }; use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; +/// Bind a TcpListener on the provided bind address to serve metrics on it. +pub async fn serve_metrics(bind: &str) -> Result<(), std::io::Error> { + let recorder_handle = setup_metrics_recorder(); + + let router = Router::new() + .route("/metrics", get(recorder_handle.render())) + .layer(axum::middleware::from_fn(track_metrics)); + + let listener = tokio::net::TcpListener::bind(bind).await?; + + axum::serve(listener, router).await?; + + Ok(()) +} + pub fn setup_metrics_recorder() -> PrometheusHandle { const EXPONENTIAL_SECONDS: &[f64] = &[ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, From ec3728606fa18ff0c6b8db5d8be1660c740d2779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 21 Dec 2023 15:16:12 +0100 Subject: [PATCH 150/249] feat: Serve basic count metrics on consumer --- hook-consumer/Cargo.toml | 1 + hook-consumer/src/config.rs | 13 +++++++++ hook-consumer/src/consumer.rs | 54 ++++++++++++++++++++++++++++++++++- hook-consumer/src/main.rs | 9 +++++- 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/hook-consumer/Cargo.toml b/hook-consumer/Cargo.toml index 35c64b5e7c7a4..2733a59b91109 100644 --- a/hook-consumer/Cargo.toml +++ b/hook-consumer/Cargo.toml @@ -10,6 +10,7 @@ envconfig = { workspace = true } futures = "0.3" hook-common = { path = "../hook-common" } http = { version = "0.2" } +metrics = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } diff --git a/hook-consumer/src/config.rs b/hook-consumer/src/config.rs index 36c120ad421cc..91695128f2192 100644 --- a/hook-consumer/src/config.rs +++ b/hook-consumer/src/config.rs @@ -5,6 +5,12 @@ use envconfig::Envconfig; #[derive(Envconfig, Clone)] pub struct Config { + #[envconfig(from = "BIND_HOST", default = "0.0.0.0")] + pub host: String, + + #[envconfig(from = "BIND_PORT", default = "8001")] + pub port: u16, + #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] pub database_url: String, @@ -33,6 +39,13 @@ pub struct Config { pub table_name: String, } +impl Config { + /// Produce a host:port address for binding a TcpListener. + pub fn bind(&self) -> String { + format!("{}:{}", self.host, self.port) + } +} + #[derive(Debug, Clone, Copy)] pub struct EnvMsDuration(pub time::Duration); diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index b1e507177a924..065555e15f61e 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -19,6 +19,8 @@ trait WebhookJob: PgQueueJob + std::marker::Send { fn parameters(&self) -> &WebhookJobParameters; fn metadata(&self) -> &WebhookJobMetadata; fn attempt(&self) -> i32; + fn queue(&self) -> String; + fn target(&self) -> String; } impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata> { @@ -33,6 +35,14 @@ impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadat fn attempt(&self) -> i32 { self.job.attempt } + + fn queue(&self) -> String { + self.job.queue.to_owned() + } + + fn target(&self) -> String { + self.job.target.to_owned() + } } impl WebhookJob for PgJob { @@ -47,6 +57,14 @@ impl WebhookJob for PgJob { fn attempt(&self) -> i32 { self.job.attempt } + + fn queue(&self) -> String { + self.job.queue.to_owned() + } + + fn target(&self) -> String { + self.job.target.to_owned() + } } /// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. @@ -170,6 +188,13 @@ async fn spawn_webhook_job_processing_task( .await .expect("semaphore has been closed"); + let labels = [ + ("queue", webhook_job.queue()), + ("target", webhook_job.target()), + ]; + + metrics::increment_counter!("webhook_jobs_total", &labels); + tokio::spawn(async move { let result = process_webhook_job(client, webhook_job, &retry_policy).await; drop(permit); @@ -197,6 +222,11 @@ async fn process_webhook_job( ) -> Result<(), ConsumerError> { let parameters = webhook_job.parameters(); + let labels = [ + ("queue", webhook_job.queue()), + ("target", webhook_job.target()), + ]; + match send_webhook( client, ¶meters.method, @@ -211,6 +241,9 @@ async fn process_webhook_job( .complete() .await .map_err(|error| ConsumerError::PgJobError(error.to_string()))?; + + metrics::increment_counter!("webhook_jobs_completed", &labels); + Ok(()) } Err(WebhookError::ParseHeadersError(e)) => { @@ -218,6 +251,9 @@ async fn process_webhook_job( .fail(WebhookJobError::new_parse(&e.to_string())) .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + + metrics::increment_counter!("webhook_jobs_failed", &labels); + Ok(()) } Err(WebhookError::ParseHttpMethodError(e)) => { @@ -225,6 +261,9 @@ async fn process_webhook_job( .fail(WebhookJobError::new_parse(&e)) .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + + metrics::increment_counter!("webhook_jobs_failed", &labels); + Ok(()) } Err(WebhookError::ParseUrlError(e)) => { @@ -232,6 +271,9 @@ async fn process_webhook_job( .fail(WebhookJobError::new_parse(&e.to_string())) .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + + metrics::increment_counter!("webhook_jobs_failed", &labels); + Ok(()) } Err(WebhookError::RetryableRequestError { error, retry_after }) => { @@ -242,7 +284,11 @@ async fn process_webhook_job( .retry(WebhookJobError::from(&error), retry_interval) .await { - Ok(_) => Ok(()), + Ok(_) => { + metrics::increment_counter!("webhook_jobs_retried", &labels); + + Ok(()) + } Err(PgJobError::RetryInvalidError { job: webhook_job, .. }) => { @@ -250,6 +296,9 @@ async fn process_webhook_job( .fail(WebhookJobError::from(&error)) .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + + metrics::increment_counter!("webhook_jobs_failed", &labels); + Ok(()) } Err(job_error) => Err(ConsumerError::PgJobError(job_error.to_string())), @@ -260,6 +309,9 @@ async fn process_webhook_job( .fail(WebhookJobError::from(&error)) .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + + metrics::increment_counter!("webhook_jobs_failed", &labels); + Ok(()) } } diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index 3cefc1d04f42d..b3ed22ce36ff3 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -1,6 +1,6 @@ use envconfig::Envconfig; -use hook_common::{pgqueue::PgQueue, retry::RetryPolicy}; +use hook_common::{metrics::serve_metrics, pgqueue::PgQueue, retry::RetryPolicy}; use hook_consumer::config::Config; use hook_consumer::consumer::WebhookConsumer; use hook_consumer::error::ConsumerError; @@ -27,6 +27,13 @@ async fn main() -> Result<(), ConsumerError> { retry_policy, ); + let bind = config.bind(); + tokio::task::spawn(async move { + serve_metrics(&bind) + .await + .expect("failed to start serving metrics"); + }); + consumer.run(config.transactional).await?; Ok(()) From 0a014cb2da24f98b649c11d053bd79b840070791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 21 Dec 2023 15:20:30 +0100 Subject: [PATCH 151/249] refactor: Use tokio::time::interval instead of async_std::task::sleep --- Cargo.lock | 381 +--------------------------------- hook-consumer/Cargo.toml | 1 - hook-consumer/src/consumer.rs | 13 +- 3 files changed, 15 insertions(+), 380 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac2c8fc525e44..7cc66455c70db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,150 +60,6 @@ dependencies = [ "libc", ] -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c" -dependencies = [ - "concurrent-queue", - "event-listener 4.0.0", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" -dependencies = [ - "async-lock 3.2.0", - "async-task", - "concurrent-queue", - "fastrand 2.0.1", - "futures-lite 2.1.0", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.1.1", - "async-executor", - "async-io 2.2.2", - "async-lock 3.2.0", - "blocking", - "futures-lite 2.1.0", - "once_cell", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", -] - -[[package]] -name = "async-io" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afaa937395a620e33dc6a742c593c01aced20aa376ffb0f628121198578ccc7" -dependencies = [ - "async-lock 3.2.0", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite 2.1.0", - "parking", - "polling 3.3.1", - "rustix 0.38.28", - "slab", - "tracing", - "windows-sys 0.52.0", -] - -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - -[[package]] -name = "async-lock" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7125e42787d53db9dd54261812ef17e937c95a51e4d291373b670342fa44310c" -dependencies = [ - "event-listener 4.0.0", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-std" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io 1.13.0", - "async-lock 2.8.0", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite 1.13.0", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d90cd0b264dfdd8eb5bad0a2c217c1f88fa96a8573f40e7b12de23fb468f46" - [[package]] name = "async-trait" version = "0.1.74" @@ -230,12 +86,6 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "atomic-write-file" version = "0.1.2" @@ -356,22 +206,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" -dependencies = [ - "async-channel 2.1.1", - "async-lock 3.2.0", - "async-task", - "fastrand 2.0.1", - "futures-io", - "futures-lite 2.1.0", - "piper", - "tracing", -] - [[package]] name = "bumpalo" version = "3.14.0" @@ -428,15 +262,6 @@ dependencies = [ "cc", ] -[[package]] -name = "concurrent-queue" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "const-oid" version = "0.9.6" @@ -624,27 +449,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "event-listener" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.0", - "pin-project-lite", -] - [[package]] name = "eyre" version = "0.6.11" @@ -655,15 +459,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.0.1" @@ -776,34 +571,6 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-lite" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeee267a1883f7ebef3700f262d2d54de95dfaf38189015a74fdc4e0c7ad8143" -dependencies = [ - "fastrand 2.0.1", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.29" @@ -872,18 +639,6 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "gloo-timers" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "h2" version = "0.3.22" @@ -1029,7 +784,6 @@ dependencies = [ name = "hook-consumer" version = "0.1.0" dependencies = [ - "async-std", "chrono", "envconfig", "futures", @@ -1178,7 +932,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio", "tower-service", "tracing", @@ -1230,7 +984,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.1.0", "pin-project-lite", - "socket2 0.5.5", + "socket2", "tokio", "tower", "tower-service", @@ -1296,26 +1050,6 @@ dependencies = [ "hashbrown 0.14.3", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -1346,15 +1080,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -1399,12 +1124,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.4.12" @@ -1426,9 +1145,6 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -dependencies = [ - "value-bag", -] [[package]] name = "mach2" @@ -1750,12 +1466,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - [[package]] name = "parking_lot" version = "0.12.1" @@ -1832,17 +1542,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" -dependencies = [ - "atomic-waker", - "fastrand 2.0.1", - "futures-io", -] - [[package]] name = "pkcs1" version = "0.7.5" @@ -1870,36 +1569,6 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - -[[package]] -name = "polling" -version = "3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf63fa624ab313c11656b4cda960bfc46c410187ad493c41f6ba2d8c1e991c9e" -dependencies = [ - "cfg-if", - "concurrent-queue", - "pin-project-lite", - "rustix 0.38.28", - "tracing", - "windows-sys 0.52.0", -] - [[package]] name = "portable-atomic" version = "1.6.0" @@ -2129,20 +1798,6 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" -[[package]] -name = "rustix" -version = "0.37.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "0.38.28" @@ -2152,7 +1807,7 @@ dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys 0.4.12", + "linux-raw-sys", "windows-sys 0.52.0", ] @@ -2330,16 +1985,6 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.5" @@ -2414,7 +2059,7 @@ dependencies = [ "crossbeam-queue", "dotenvy", "either", - "event-listener 2.5.3", + "event-listener", "futures-channel", "futures-core", "futures-intrusive", @@ -2665,9 +2310,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", - "fastrand 2.0.1", + "fastrand", "redox_syscall", - "rustix 0.38.28", + "rustix", "windows-sys 0.48.0", ] @@ -2730,7 +2375,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -2962,12 +2607,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "value-bag" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72e1902dde2bd6441347de2b70b7f5d59bf157c6c62f0c44572607a1d55bbe" - [[package]] name = "vcpkg" version = "0.2.15" @@ -2980,12 +2619,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "waker-fn" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" - [[package]] name = "want" version = "0.3.1" diff --git a/hook-consumer/Cargo.toml b/hook-consumer/Cargo.toml index 2733a59b91109..fc8ee4a797fac 100644 --- a/hook-consumer/Cargo.toml +++ b/hook-consumer/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -async-std = { version = "1.12" } chrono = { workspace = true } envconfig = { workspace = true } futures = "0.3" diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 065555e15f61e..de857c55f777f 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -2,7 +2,6 @@ use std::collections; use std::sync::Arc; use std::time; -use async_std::task; use hook_common::{ pgqueue::{PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, retry::RetryPolicy, @@ -118,11 +117,13 @@ impl<'p> WebhookConsumer<'p> { async fn wait_for_job<'a>( &self, ) -> Result, ConsumerError> { + let mut interval = tokio::time::interval(self.poll_interval); + loop { + interval.tick().await; + if let Some(job) = self.queue.dequeue(&self.name).await? { return Ok(job); - } else { - task::sleep(self.poll_interval).await; } } } @@ -131,11 +132,13 @@ impl<'p> WebhookConsumer<'p> { async fn wait_for_job_tx<'a>( &self, ) -> Result, ConsumerError> { + let mut interval = tokio::time::interval(self.poll_interval); + loop { + interval.tick().await; + if let Some(job) = self.queue.dequeue_tx(&self.name).await? { return Ok(job); - } else { - task::sleep(self.poll_interval).await; } } } From f3e1252aed2a91f23a642bf38e466f4f3e123ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 21 Dec 2023 15:43:42 +0100 Subject: [PATCH 152/249] feat: Track webhook job processing --- hook-common/src/metrics.rs | 5 +--- hook-consumer/src/consumer.rs | 49 +++++++++++++++++------------------ 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/hook-common/src/metrics.rs b/hook-common/src/metrics.rs index 7d881ea06bc40..f83edb5b062c9 100644 --- a/hook-common/src/metrics.rs +++ b/hook-common/src/metrics.rs @@ -27,10 +27,7 @@ pub fn setup_metrics_recorder() -> PrometheusHandle { ]; PrometheusBuilder::new() - .set_buckets_for_metric( - Matcher::Full("http_requests_duration_seconds".to_string()), - EXPONENTIAL_SECONDS, - ) + .set_buckets(EXPONENTIAL_SECONDS) .unwrap() .install_recorder() .unwrap() diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index de857c55f777f..8e67949466c9f 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time; use hook_common::{ - pgqueue::{PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, + pgqueue::{Job, PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, retry::RetryPolicy, webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}, }; @@ -13,38 +13,26 @@ use tokio::sync; use crate::error::{ConsumerError, WebhookError}; -/// A WebhookJob is any PgQueueJob that returns a reference to webhook parameters and metadata. +/// A WebhookJob is any PgQueueJob with WebhookJobParameters and WebhookJobMetadata. trait WebhookJob: PgQueueJob + std::marker::Send { fn parameters(&self) -> &WebhookJobParameters; fn metadata(&self) -> &WebhookJobMetadata; - fn attempt(&self) -> i32; - fn queue(&self) -> String; - fn target(&self) -> String; -} - -impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata> { - fn parameters(&self) -> &WebhookJobParameters { - &self.job.parameters - } - - fn metadata(&self) -> &WebhookJobMetadata { - &self.job.metadata - } + fn job(&self) -> &Job; fn attempt(&self) -> i32 { - self.job.attempt + self.job().attempt } fn queue(&self) -> String { - self.job.queue.to_owned() + self.job().queue.to_owned() } fn target(&self) -> String { - self.job.target.to_owned() + self.job().target.to_owned() } } -impl WebhookJob for PgJob { +impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadata> { fn parameters(&self) -> &WebhookJobParameters { &self.job.parameters } @@ -53,16 +41,22 @@ impl WebhookJob for PgJob { &self.job.metadata } - fn attempt(&self) -> i32 { - self.job.attempt + fn job(&self) -> &Job { + &self.job } +} - fn queue(&self) -> String { - self.job.queue.to_owned() +impl WebhookJob for PgJob { + fn parameters(&self) -> &WebhookJobParameters { + &self.job.parameters } - fn target(&self) -> String { - self.job.target.to_owned() + fn metadata(&self) -> &WebhookJobMetadata { + &self.job.metadata + } + + fn job(&self) -> &Job { + &self.job } } @@ -199,8 +193,13 @@ async fn spawn_webhook_job_processing_task( metrics::increment_counter!("webhook_jobs_total", &labels); tokio::spawn(async move { + let now = tokio::time::Instant::now(); let result = process_webhook_job(client, webhook_job, &retry_policy).await; drop(permit); + + let elapsed = now.elapsed().as_secs_f64(); + metrics::histogram!("webhook_jobs_processing_duration_seconds", elapsed, &labels); + result }) } From 0e54ceaa639c6ae5103a818d89caa953e9b15003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 21 Dec 2023 15:53:58 +0100 Subject: [PATCH 153/249] fix: Remove unused imports --- hook-common/src/metrics.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-common/src/metrics.rs b/hook-common/src/metrics.rs index f83edb5b062c9..4411fead9e5a7 100644 --- a/hook-common/src/metrics.rs +++ b/hook-common/src/metrics.rs @@ -4,7 +4,7 @@ use axum::{ body::Body, extract::MatchedPath, http::Request, middleware::Next, response::IntoResponse, routing::get, Router, }; -use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; +use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle}; /// Bind a TcpListener on the provided bind address to serve metrics on it. pub async fn serve_metrics(bind: &str) -> Result<(), std::io::Error> { From 1f8088bd7c7d101f3e99775f0351bf9baf866dfb Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 21 Dec 2023 09:58:27 -0700 Subject: [PATCH 154/249] Update hook-common/src/kafka_messages/app_metrics.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomás Farías Santana --- hook-common/src/kafka_messages/app_metrics.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 5aff62cd9fedd..145d4afee020a 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -128,8 +128,8 @@ where let error_type = match error_type { ErrorType::ConnectionError => "Connection Error".to_owned(), - ErrorType::TimeoutError => "Timeout".to_owned(), - ErrorType::BadHttpStatus(s) => format!("HTTP Status: {}", s), + ErrorType::TimeoutError => "Timeout Error".to_owned(), + ErrorType::BadHttpStatus(s) => format!("Bad HTTP Status: {}", s), ErrorType::ParseError => "Parse Error".to_owned(), }; serializer.serialize_str(&error_type) From 2220b6bf2a331765b852f7ae4ff78d031a7f33bb Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 21 Dec 2023 09:59:21 -0700 Subject: [PATCH 155/249] Update hook-common/src/kafka_messages/app_metrics.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomás Farías Santana --- hook-common/src/kafka_messages/app_metrics.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 145d4afee020a..94ec24ee5bc43 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -144,9 +144,9 @@ where Some(s) => { let error_type = match &s[..] { "Connection Error" => ErrorType::ConnectionError, - "Timeout" => ErrorType::TimeoutError, - _ if s.starts_with("HTTP Status:") => { - let status = &s["HTTP Status:".len()..]; + "Timeout Error" => ErrorType::TimeoutError, + _ if s.starts_with("Bad HTTP Status:") => { + let status = &s["Bad HTTP Status:".len()..]; ErrorType::BadHttpStatus(status.parse().map_err(serde::de::Error::custom)?) } "Parse Error" => ErrorType::ParseError, From 50884295fa660f78510c42493ce88bd61b669ba4 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 21 Dec 2023 09:59:30 -0700 Subject: [PATCH 156/249] Update hook-common/src/kafka_messages/app_metrics.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomás Farías Santana --- hook-common/src/kafka_messages/app_metrics.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 94ec24ee5bc43..9acc4112e1113 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -155,8 +155,8 @@ where &s, &[ "Connection Error", - "Timeout", - "HTTP Status: ", + "Timeout Error", + "Bad HTTP Status: ", "Parse Error", ], )) From db076bf7a61fa0f0d5548ea1d37581fbaa072e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 22 Dec 2023 11:43:21 +0100 Subject: [PATCH 157/249] refactor: Split-up router creation from metrics serving to allow callers to add more routes --- hook-common/src/metrics.rs | 20 ++++++++++++-------- hook-consumer/src/main.rs | 7 +++++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/hook-common/src/metrics.rs b/hook-common/src/metrics.rs index 4411fead9e5a7..3d9e4c02655a5 100644 --- a/hook-common/src/metrics.rs +++ b/hook-common/src/metrics.rs @@ -6,14 +6,9 @@ use axum::{ }; use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle}; -/// Bind a TcpListener on the provided bind address to serve metrics on it. -pub async fn serve_metrics(bind: &str) -> Result<(), std::io::Error> { - let recorder_handle = setup_metrics_recorder(); - - let router = Router::new() - .route("/metrics", get(recorder_handle.render())) - .layer(axum::middleware::from_fn(track_metrics)); - +/// Bind a `TcpListener` on the provided bind address to serve a `Router` on it. +/// This function is intended to take a Router as returned by `setup_metrics_router`, potentially with more routes added by the caller. +pub async fn serve(router: Router, bind: &str) -> Result<(), std::io::Error> { let listener = tokio::net::TcpListener::bind(bind).await?; axum::serve(listener, router).await?; @@ -21,6 +16,15 @@ pub async fn serve_metrics(bind: &str) -> Result<(), std::io::Error> { Ok(()) } +/// Build a Router for a metrics endpoint. +pub fn setup_metrics_router() -> Router { + let recorder_handle = setup_metrics_recorder(); + + Router::new() + .route("/metrics", get(recorder_handle.render())) + .layer(axum::middleware::from_fn(track_metrics)) +} + pub fn setup_metrics_recorder() -> PrometheusHandle { const EXPONENTIAL_SECONDS: &[f64] = &[ 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index b3ed22ce36ff3..38c2ee23042de 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -1,6 +1,8 @@ use envconfig::Envconfig; -use hook_common::{metrics::serve_metrics, pgqueue::PgQueue, retry::RetryPolicy}; +use hook_common::{ + metrics::serve, metrics::setup_metrics_router, pgqueue::PgQueue, retry::RetryPolicy, +}; use hook_consumer::config::Config; use hook_consumer::consumer::WebhookConsumer; use hook_consumer::error::ConsumerError; @@ -29,7 +31,8 @@ async fn main() -> Result<(), ConsumerError> { let bind = config.bind(); tokio::task::spawn(async move { - serve_metrics(&bind) + let router = setup_metrics_router(); + serve(router, &bind) .await .expect("failed to start serving metrics"); }); From 232c54084a7db5f6baea351045b04f72e848edf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 22 Dec 2023 14:24:23 +0100 Subject: [PATCH 158/249] refactor: Only track duration seconds on success --- hook-consumer/src/consumer.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 8e67949466c9f..ecfa4b0ba42bd 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -193,13 +193,8 @@ async fn spawn_webhook_job_processing_task( metrics::increment_counter!("webhook_jobs_total", &labels); tokio::spawn(async move { - let now = tokio::time::Instant::now(); let result = process_webhook_job(client, webhook_job, &retry_policy).await; drop(permit); - - let elapsed = now.elapsed().as_secs_f64(); - metrics::histogram!("webhook_jobs_processing_duration_seconds", elapsed, &labels); - result }) } @@ -229,15 +224,20 @@ async fn process_webhook_job( ("target", webhook_job.target()), ]; - match send_webhook( + let now = tokio::time::Instant::now(); + + let send_result = send_webhook( client, ¶meters.method, ¶meters.url, ¶meters.headers, parameters.body.clone(), ) - .await - { + .await; + + let elapsed = now.elapsed().as_secs_f64(); + + match send_result { Ok(_) => { webhook_job .complete() @@ -245,6 +245,7 @@ async fn process_webhook_job( .map_err(|error| ConsumerError::PgJobError(error.to_string()))?; metrics::increment_counter!("webhook_jobs_completed", &labels); + metrics::histogram!("webhook_jobs_processing_duration_seconds", elapsed, &labels); Ok(()) } From 83426b8613c82ef26098339f04135f35b13c1bf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 3 Jan 2024 11:06:30 +0100 Subject: [PATCH 159/249] feat: Implement support for retrying jobs to different queue (#19) Co-authored-by: Brett Hoerner --- .dockerignore | 5 + .github/workflows/docker-hook-consumer.yml | 63 +++ .github/workflows/docker-hook-janitor.yml | 63 +++ .github/workflows/docker-hook-producer.yml | 63 +++ Dockerfile | 31 ++ hook-common/src/pgqueue.rs | 522 +++++++++++++-------- hook-common/src/retry.rs | 208 +++++++- hook-consumer/src/config.rs | 3 + hook-consumer/src/consumer.rs | 15 +- hook-consumer/src/main.rs | 9 +- 10 files changed, 747 insertions(+), 235 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-hook-consumer.yml create mode 100644 .github/workflows/docker-hook-janitor.yml create mode 100644 .github/workflows/docker-hook-producer.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000..9879089557b41 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +target +docker +.env +.git +.github diff --git a/.github/workflows/docker-hook-consumer.yml b/.github/workflows/docker-hook-consumer.yml new file mode 100644 index 0000000000000..5975120e3ff1a --- /dev/null +++ b/.github/workflows/docker-hook-consumer.yml @@ -0,0 +1,63 @@ +name: Build hook-consumer docker image + +on: + workflow_dispatch: + push: + branches: + - 'main' + +permissions: + packages: write + +jobs: + build: + name: build and publish hook-consumer image + runs-on: buildjet-4vcpu-ubuntu-2204-arm + steps: + + - name: Check Out Repo + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/hook-consumer + tags: | + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push consumer + id: docker_build_hook_consumer + uses: docker/build-push-action@v4 + with: + context: ./ + file: ./Dockerfile + builder: ${{ steps.buildx.outputs.name }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: BIN=hook-consumer + + - name: Hook-consumer image digest + run: echo ${{ steps.docker_build_hook_consumer.outputs.digest }} diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml new file mode 100644 index 0000000000000..2649821f697b3 --- /dev/null +++ b/.github/workflows/docker-hook-janitor.yml @@ -0,0 +1,63 @@ +name: Build hook-janitor docker image + +on: + workflow_dispatch: + push: + branches: + - 'main' + +permissions: + packages: write + +jobs: + build: + name: build and publish hook-janitor image + runs-on: buildjet-4vcpu-ubuntu-2204-arm + steps: + + - name: Check Out Repo + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/hook-janitor + tags: | + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push janitor + id: docker_build_hook_janitor + uses: docker/build-push-action@v4 + with: + context: ./ + file: ./Dockerfile + builder: ${{ steps.buildx.outputs.name }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: BIN=hook-janitor + + - name: Hook-janitor image digest + run: echo ${{ steps.docker_build_hook_janitor.outputs.digest }} diff --git a/.github/workflows/docker-hook-producer.yml b/.github/workflows/docker-hook-producer.yml new file mode 100644 index 0000000000000..d5e131feb014b --- /dev/null +++ b/.github/workflows/docker-hook-producer.yml @@ -0,0 +1,63 @@ +name: Build hook-producer docker image + +on: + workflow_dispatch: + push: + branches: + - 'main' + +permissions: + packages: write + +jobs: + build: + name: build and publish hook-producer image + runs-on: buildjet-4vcpu-ubuntu-2204-arm + steps: + + - name: Check Out Repo + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/hook-producer + tags: | + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push producer + id: docker_build_hook_producer + uses: docker/build-push-action@v4 + with: + context: ./ + file: ./Dockerfile + builder: ${{ steps.buildx.outputs.name }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: BIN=hook-producer + + - name: Hook-producer image digest + run: echo ${{ steps.docker_build_hook_producer.outputs.digest }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000..959fd17180fb5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.74.0-buster AS chef +ARG BIN +WORKDIR app + +FROM chef AS planner +ARG BIN + +COPY . . +RUN cargo chef prepare --recipe-path recipe.json --bin $BIN + +FROM chef AS builder +ARG BIN + +# Ensure working C compile setup (not installed by default in arm64 images) +RUN apt update && apt install build-essential cmake -y + +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json + +COPY . . +RUN cargo build --release --bin $BIN + +FROM debian:bullseye-20230320-slim AS runtime +ARG BIN +ENV ENTRYPOINT=/usr/local/bin/$BIN +WORKDIR app + +USER nobody + +COPY --from=builder /app/target/release/$BIN /usr/local/bin +ENTRYPOINT [ $ENTRYPOINT ] diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index a47864d9e104d..39a09ce7fda27 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -109,42 +109,103 @@ impl Job { self.attempt >= self.max_attempts } - /// Consume Job to retry it. - /// This returns a RetryableJob that can be enqueued by PgQueue. - /// - /// # Arguments - /// - /// * `error`: Any JSON-serializable value to be stored as an error. - fn retry(self, error: E) -> RetryableJob { + /// Consume `Job` to transition it to a `RetryableJob`, i.e. a `Job` that may be retried. + fn retryable(self) -> RetryableJob { RetryableJob { id: self.id, attempt: self.attempt, - error: sqlx::types::Json(error), queue: self.queue, + retry_queue: None, } } - /// Consume Job to complete it. - /// This returns a CompletedJob that can be marked as completed by PgQueue. - fn complete(self) -> CompletedJob { - CompletedJob { + /// Consume `Job` to complete it. + /// A `CompletedJob` is finalized and cannot be used further; it is returned for reporting or inspection. + /// + /// # Arguments + /// + /// * `table`: The table where this job will be marked as completed. + /// * `executor`: Any sqlx::Executor that can execute the UPDATE query required to mark this `Job` as completed. + async fn complete<'c, E>(self, table: &str, executor: E) -> Result + where + E: sqlx::Executor<'c, Database = sqlx::Postgres>, + { + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'completed'::job_status +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + "#, + table + ); + + sqlx::query(&base_query) + .bind(&self.queue) + .bind(self.id) + .execute(executor) + .await?; + + Ok(CompletedJob { id: self.id, queue: self.queue, - } + }) } - /// Consume Job to fail it. - /// This returns a FailedJob that can be marked as failed by PgQueue. + /// Consume `Job` to fail it. + /// A `FailedJob` is finalized and cannot be used further; it is returned for reporting or inspection. /// /// # Arguments /// /// * `error`: Any JSON-serializable value to be stored as an error. - fn fail(self, error: E) -> FailedJob { - FailedJob { + /// * `table`: The table where this job will be marked as failed. + /// * `executor`: Any sqlx::Executor that can execute the UPDATE query required to mark this `Job` as failed. + async fn fail<'c, E, S>( + self, + error: S, + table: &str, + executor: E, + ) -> Result, sqlx::Error> + where + S: serde::Serialize + std::marker::Sync + std::marker::Send, + E: sqlx::Executor<'c, Database = sqlx::Postgres>, + { + let json_error = sqlx::types::Json(error); + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + status = 'failed'::job_status + errors = array_append("{0}".errors, $3) +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + "#, + &table + ); + + sqlx::query(&base_query) + .bind(&self.queue) + .bind(self.id) + .bind(&json_error) + .execute(executor) + .await?; + + Ok(FailedJob { id: self.id, - error: sqlx::types::Json(error), + error: json_error, queue: self.queue, - } + }) } } @@ -161,7 +222,8 @@ pub trait PgQueueJob { mut self, error: E, retry_interval: time::Duration, - ) -> Result, PgJobError>>; + queue: &str, + ) -> Result>>; } /// A Job that can be updated in PostgreSQL. @@ -175,28 +237,9 @@ pub struct PgJob { #[async_trait] impl PgQueueJob for PgJob { async fn complete(mut self) -> Result>>> { - let completed_job = self.job.complete(); - - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - status = 'completed'::job_status -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&completed_job.queue) - .bind(completed_job.id) - .execute(&mut *self.connection) + let completed_job = self + .job + .complete(&self.table, &mut *self.connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -210,31 +253,9 @@ RETURNING mut self, error: E, ) -> Result, PgJobError>>> { - let failed_job = self.job.fail(error); - - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - status = 'failed'::job_status - errors = array_append("{0}".errors, $3) -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&failed_job.queue) - .bind(failed_job.id) - .bind(&failed_job.error) - .execute(&mut *self.connection) + let failed_job = self + .job + .fail(error, &self.table, &mut *self.connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -248,46 +269,27 @@ RETURNING mut self, error: E, retry_interval: time::Duration, - ) -> Result, PgJobError>>> { + queue: &str, + ) -> Result>>> { if self.job.is_gte_max_attempts() { return Err(PgJobError::RetryInvalidError { job: Box::new(self), error: "Maximum attempts reached".to_owned(), }); } - let retryable_job = self.job.retry(error); - - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - status = 'available'::job_status, - scheduled_at = NOW() + $3, - errors = array_append("{0}".errors, $4) -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - sqlx::query(&base_query) - .bind(&retryable_job.queue) - .bind(retryable_job.id) - .bind(retry_interval) - .bind(&retryable_job.error) - .execute(&mut *self.connection) + let retried_job = self + .job + .retryable() + .queue(queue) + .retry(error, retry_interval, &self.table, &mut *self.connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; - Ok(retryable_job) + Ok(retried_job) } } @@ -305,28 +307,9 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio async fn complete( mut self, ) -> Result>>> { - let completed_job = self.job.complete(); - - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - status = 'completed'::job_status -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&completed_job.queue) - .bind(completed_job.id) - .execute(&mut *self.transaction) + let completed_job = self + .job + .complete(&self.table, &mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -344,34 +327,13 @@ RETURNING Ok(completed_job) } - async fn fail( + async fn fail( mut self, - error: E, - ) -> Result, PgJobError>>> { - let failed_job = self.job.fail(error); - - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - status = 'failed'::job_status - errors = array_append("{0}".errors, $3) -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&failed_job.queue) - .bind(failed_job.id) - .bind(&failed_job.error) - .execute(&mut *self.transaction) + error: S, + ) -> Result, PgJobError>>> { + let failed_job = self + .job + .fail(error, &self.table, &mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -393,40 +355,22 @@ RETURNING mut self, error: E, retry_interval: time::Duration, - ) -> Result, PgJobError>>> { + queue: &str, + ) -> Result>>> { + // Ideally, the transition to RetryableJob should be fallible. + // But taking ownership of self when we return this error makes things difficult. if self.job.is_gte_max_attempts() { return Err(PgJobError::RetryInvalidError { job: Box::new(self), error: "Maximum attempts reached".to_owned(), }); } - let retryable_job = self.job.retry(error); - - let base_query = format!( - r#" -UPDATE - "{0}" -SET - finished_at = NOW(), - status = 'available'::job_status, - scheduled_at = NOW() + $3, - errors = array_append("{0}".errors, $4) -WHERE - "{0}".id = $2 - AND queue = $1 -RETURNING - "{0}".* - "#, - &self.table - ); - - sqlx::query(&base_query) - .bind(&retryable_job.queue) - .bind(retryable_job.id) - .bind(retry_interval) - .bind(&retryable_job.error) - .execute(&mut *self.transaction) + let retried_job = self + .job + .retryable() + .queue(queue) + .retry(error, retry_interval, &self.table, &mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -441,42 +385,127 @@ RETURNING error, })?; - Ok(retryable_job) + Ok(retried_job) } } /// A Job that has failed but can still be enqueued into a PgQueue to be retried at a later point. /// The time until retry will depend on the PgQueue's RetryPolicy. -pub struct RetryableJob { +pub struct RetryableJob { /// A unique id identifying a job. - id: i64, + pub id: i64, /// A number corresponding to the current job attempt. pub attempt: i32, - /// Any JSON-serializable value to be stored as an error. - pub error: sqlx::types::Json, /// A unique id identifying a job queue. - pub queue: String, + queue: String, + /// An optional separate queue where to enqueue this job when retrying. + retry_queue: Option, +} + +impl RetryableJob { + /// Set the queue for a `RetryableJob`. + /// If not set, `Job` will be retried to its original queue on calling `retry`. + fn queue(mut self, queue: &str) -> Self { + self.retry_queue = Some(queue.to_owned()); + self + } + + /// Return the queue that a `Job` is to be retried into. + fn retry_queue(&self) -> &str { + self.retry_queue.as_ref().unwrap_or(&self.queue) + } + + /// Consume `Job` to retry it. + /// A `RetriedJob` cannot be used further; it is returned for reporting or inspection. + /// + /// # Arguments + /// + /// * `error`: Any JSON-serializable value to be stored as an error. + /// * `retry_interval`: The duration until the `Job` is to be retried again. Used to set `scheduled_at`. + /// * `table`: The table where this job will be marked as completed. + /// * `executor`: Any sqlx::Executor that can execute the UPDATE query required to mark this `Job` as completed. + async fn retry<'c, S, E>( + self, + error: S, + retry_interval: time::Duration, + table: &str, + executor: E, + ) -> Result + where + S: serde::Serialize + std::marker::Sync + std::marker::Send, + E: sqlx::Executor<'c, Database = sqlx::Postgres>, + { + let json_error = sqlx::types::Json(error); + let base_query = format!( + r#" +UPDATE + "{0}" +SET + finished_at = NOW(), + errors = array_append("{0}".errors, $4), + queue = $5, + status = 'available'::job_status, + scheduled_at = NOW() + $3 +WHERE + "{0}".id = $2 + AND queue = $1 +RETURNING + "{0}".* + "#, + &table + ); + + sqlx::query(&base_query) + .bind(&self.queue) + .bind(self.id) + .bind(retry_interval) + .bind(&json_error) + .bind(self.retry_queue()) + .execute(executor) + .await?; + + Ok(RetriedJob { + id: self.id, + table: table.to_owned(), + queue: self.queue, + retry_queue: self.retry_queue.to_owned(), + }) + } } -/// A Job that has completed to be enqueued into a PgQueue and marked as completed. +/// State a `Job` is transitioned to after successfully completing. +#[derive(Debug)] pub struct CompletedJob { /// A unique id identifying a job. - id: i64, + pub id: i64, /// A unique id identifying a job queue. pub queue: String, } -/// A Job that has failed to be enqueued into a PgQueue and marked as failed. +/// State a `Job` is transitioned to after it has been enqueued for retrying. +#[derive(Debug)] +pub struct RetriedJob { + /// A unique id identifying a job. + pub id: i64, + /// A unique id identifying a job queue. + pub queue: String, + pub retry_queue: Option, + pub table: String, +} + +/// State a `Job` is transitioned to after exhausting all of their attempts. +#[derive(Debug)] pub struct FailedJob { /// A unique id identifying a job. - id: i64, + pub id: i64, /// Any JSON-serializable value to be stored as an error. pub error: sqlx::types::Json, /// A unique id identifying a job queue. pub queue: String, } -/// A NewJob to be enqueued into a PgQueue. +/// This struct represents a new job being created to be enqueued into a `PgQueue`. +#[derive(Debug)] pub struct NewJob { /// The maximum amount of attempts this NewJob has to complete. pub max_attempts: i32, @@ -513,14 +542,13 @@ pub struct PgQueue { pub type PgQueueResult = std::result::Result; impl PgQueue { - /// Initialize a new PgQueue backed by table in PostgreSQL. + /// Initialize a new PgQueue backed by table in PostgreSQL by intializing a connection pool to the database in `url`. /// /// # Arguments /// /// * `queue_name`: A name for the queue we are going to initialize. /// * `table_name`: The name for the table the queue will use in PostgreSQL. /// * `url`: A URL pointing to where the PostgreSQL database is hosted. - /// * `worker_name`: The name of the worker that is operating with this queue. pub async fn new(queue_name: &str, table_name: &str, url: &str) -> PgQueueResult { let name = queue_name.to_owned(); let table = table_name.to_owned(); @@ -531,6 +559,13 @@ impl PgQueue { Ok(Self { name, pool, table }) } + /// Initialize a new PgQueue backed by table in PostgreSQL from a provided connection pool. + /// + /// # Arguments + /// + /// * `queue_name`: A name for the queue we are going to initialize. + /// * `table_name`: The name for the table the queue will use in PostgreSQL. + /// * `pool`: A database connection pool to be used by this queue. pub async fn new_from_pool( queue_name: &str, table_name: &str, @@ -542,7 +577,8 @@ impl PgQueue { Ok(Self { name, pool, table }) } - /// Dequeue a Job from this PgQueue to work on it. + /// Dequeue a `Job` from this `PgQueue`. + /// The `Job` will be updated to `'running'` status, so any other `dequeue` calls will skip it. pub async fn dequeue< J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, M: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, @@ -620,7 +656,9 @@ RETURNING } } - /// Dequeue a Job from this PgQueue to work on it. + /// Dequeue a `Job` from this `PgQueue` and hold the transaction. + /// Any other `dequeue_tx` calls will skip rows locked, so by holding a transaction we ensure only one worker can dequeue a job. + /// Holding a transaction open can have performance implications, but it means no `'running'` state is required. pub async fn dequeue_tx< 'a, J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, @@ -692,8 +730,8 @@ RETURNING } } - /// Enqueue a Job into this PgQueue. - /// We take ownership of NewJob to enforce a specific NewJob is only enqueued once. + /// Enqueue a `NewJob` into this PgQueue. + /// We take ownership of `NewJob` to enforce a specific `NewJob` is only enqueued once. pub async fn enqueue< J: serde::Serialize + std::marker::Sync, M: serde::Serialize + std::marker::Sync, @@ -875,19 +913,16 @@ mod tests { let job_metadata = JobMetadata::default(); let worker_id = worker_id(); let new_job = NewJob::new(2, job_metadata, job_parameters, &job_target); - let retry_policy = RetryPolicy { - backoff_coefficient: 0, - initial_interval: time::Duration::from_secs(0), - maximum_interval: None, - }; + let table_name = "job_queue".to_owned(); + let queue_name = "test_can_retry_job_with_remaining_attempts".to_owned(); - let queue = PgQueue::new_from_pool( - "test_can_retry_job_with_remaining_attempts", - "job_queue", - db, - ) - .await - .expect("failed to connect to local test postgresql database"); + let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)) + .queue(&queue_name) + .provide(); + + let queue = PgQueue::new_from_pool(&queue_name, &table_name, db) + .await + .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); let job: PgJob = queue @@ -895,11 +930,18 @@ mod tests { .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); - let retry_interval = retry_policy.time_until_next_retry(job.job.attempt as u32, None); + + let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); + let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); let _ = job - .retry("a very reasonable failure reason", retry_interval) + .retry( + "a very reasonable failure reason", + retry_interval, + &retry_queue, + ) .await .expect("failed to retry job"); + let retried_job: PgJob = queue .dequeue(&worker_id) .await @@ -918,6 +960,72 @@ mod tests { assert_eq!(retried_job.job.target, job_target); } + #[sqlx::test(migrations = "../migrations")] + async fn test_can_retry_job_to_different_queue(db: PgPool) { + let job_target = job_target(); + let job_parameters = JobParameters::default(); + let job_metadata = JobMetadata::default(); + let worker_id = worker_id(); + let new_job = NewJob::new(2, job_metadata, job_parameters, &job_target); + let table_name = "job_queue".to_owned(); + let queue_name = "test_can_retry_job_to_different_queue".to_owned(); + let retry_queue_name = "test_can_retry_job_to_different_queue_retry".to_owned(); + + let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)) + .queue(&retry_queue_name) + .provide(); + + let queue = PgQueue::new_from_pool(&queue_name, &table_name, db.clone()) + .await + .expect("failed to connect to queue in local test postgresql database"); + + queue.enqueue(new_job).await.expect("failed to enqueue job"); + let job: PgJob = queue + .dequeue(&worker_id) + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + + let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); + let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); + let _ = job + .retry( + "a very reasonable failure reason", + retry_interval, + &retry_queue, + ) + .await + .expect("failed to retry job"); + + let retried_job_not_found: Option> = queue + .dequeue(&worker_id) + .await + .expect("failed to dequeue job"); + + assert!(retried_job_not_found.is_none()); + + let queue = PgQueue::new_from_pool(&retry_queue_name, &table_name, db) + .await + .expect("failed to connect to retry queue in local test postgresql database"); + + let retried_job: PgJob = queue + .dequeue(&worker_id) + .await + .expect("failed to dequeue job") + .expect("job not found in retry queue"); + + assert_eq!(retried_job.job.attempt, 2); + assert!(retried_job.job.attempted_by.contains(&worker_id)); + assert_eq!(retried_job.job.attempted_by.len(), 2); + assert_eq!(retried_job.job.max_attempts, 2); + assert_eq!( + *retried_job.job.parameters.as_ref(), + JobParameters::default() + ); + assert_eq!(retried_job.job.status, JobStatus::Running); + assert_eq!(retried_job.job.target, job_target); + } + #[sqlx::test(migrations = "../migrations")] #[should_panic(expected = "failed to retry job")] async fn test_cannot_retry_job_without_remaining_attempts(db: PgPool) { @@ -926,11 +1034,7 @@ mod tests { let job_metadata = JobMetadata::default(); let worker_id = worker_id(); let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); - let retry_policy = RetryPolicy { - backoff_coefficient: 0, - initial_interval: time::Duration::from_secs(0), - maximum_interval: None, - }; + let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)).provide(); let queue = PgQueue::new_from_pool( "test_cannot_retry_job_without_remaining_attempts", @@ -947,8 +1051,10 @@ mod tests { .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue"); - let retry_interval = retry_policy.time_until_next_retry(job.job.attempt as u32, None); - job.retry("a very reasonable failure reason", retry_interval) + + let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); + + job.retry("a very reasonable failure reason", retry_interval, "any") .await .expect("failed to retry job"); } diff --git a/hook-common/src/retry.rs b/hook-common/src/retry.rs index f72b0d166fdc8..b00f967a7b6e5 100644 --- a/hook-common/src/retry.rs +++ b/hook-common/src/retry.rs @@ -1,7 +1,10 @@ +//! # Retry +//! +//! Module providing a `RetryPolicy` struct to configure job retrying. use std::time; -#[derive(Copy, Clone, Debug)] -/// The retry policy that PgQueue will use to determine how to set scheduled_at when enqueuing a retry. +#[derive(Clone, Debug)] +/// A retry policy to determine retry parameters for a job. pub struct RetryPolicy { /// Coefficient to multiply initial_interval with for every past attempt. pub backoff_coefficient: u32, @@ -9,47 +12,214 @@ pub struct RetryPolicy { pub initial_interval: time::Duration, /// The maximum possible backoff between retries. pub maximum_interval: Option, + /// An optional queue to send WebhookJob retries to. + pub queue: Option, } impl RetryPolicy { - pub fn new( - backoff_coefficient: u32, - initial_interval: time::Duration, - maximum_interval: Option, - ) -> Self { - Self { - backoff_coefficient, - initial_interval, - maximum_interval, - } + /// Initialize a `RetryPolicyBuilder`. + pub fn build(backoff_coefficient: u32, initial_interval: time::Duration) -> RetryPolicyBuilder { + RetryPolicyBuilder::new(backoff_coefficient, initial_interval) } - /// Calculate the time until the next retry for a given RetryableJob. - pub fn time_until_next_retry( + /// Determine interval for retrying at a given attempt number. + /// If not `None`, this method will respect `preferred_retry_interval` as long as it falls within `candidate_interval <= preferred_retry_interval <= maximum_interval`. + pub fn retry_interval( &self, attempt: u32, preferred_retry_interval: Option, ) -> time::Duration { - let candidate_interval = self.initial_interval * self.backoff_coefficient.pow(attempt); + let candidate_interval = + self.initial_interval * self.backoff_coefficient.pow(attempt.saturating_sub(1)); match (preferred_retry_interval, self.maximum_interval) { - (Some(duration), Some(max_interval)) => std::cmp::min( - std::cmp::max(std::cmp::min(candidate_interval, max_interval), duration), - max_interval, - ), + (Some(duration), Some(max_interval)) => { + let min_interval_allowed = std::cmp::min(candidate_interval, max_interval); + + if min_interval_allowed <= duration && duration <= max_interval { + duration + } else { + min_interval_allowed + } + } (Some(duration), None) => std::cmp::max(candidate_interval, duration), (None, Some(max_interval)) => std::cmp::min(candidate_interval, max_interval), (None, None) => candidate_interval, } } + + /// Determine the queue to be used for retrying. + /// Only whether a queue is configured in this RetryPolicy is used to determine which queue to use for retrying. + /// This may be extended in the future to support more decision parameters. + pub fn retry_queue<'s>(&'s self, current_queue: &'s str) -> &'s str { + if let Some(new_queue) = &self.queue { + new_queue + } else { + current_queue + } + } } impl Default for RetryPolicy { + fn default() -> Self { + RetryPolicyBuilder::default().provide() + } +} + +/// Builder pattern struct to provide a `RetryPolicy`. +pub struct RetryPolicyBuilder { + /// Coefficient to multiply initial_interval with for every past attempt. + pub backoff_coefficient: u32, + /// The backoff interval for the first retry. + pub initial_interval: time::Duration, + /// The maximum possible backoff between retries. + pub maximum_interval: Option, + /// An optional queue to send WebhookJob retries to. + pub queue: Option, +} + +impl Default for RetryPolicyBuilder { fn default() -> Self { Self { backoff_coefficient: 2, initial_interval: time::Duration::from_secs(1), maximum_interval: None, + queue: None, + } + } +} + +impl RetryPolicyBuilder { + pub fn new(backoff_coefficient: u32, initial_interval: time::Duration) -> Self { + Self { + backoff_coefficient, + initial_interval, + ..RetryPolicyBuilder::default() + } + } + + pub fn maximum_interval(mut self, interval: time::Duration) -> RetryPolicyBuilder { + self.maximum_interval = Some(interval); + self + } + + pub fn queue(mut self, queue: &str) -> RetryPolicyBuilder { + self.queue = Some(queue.to_owned()); + self + } + + /// Provide a `RetryPolicy` according to build parameters provided thus far. + pub fn provide(&self) -> RetryPolicy { + RetryPolicy { + backoff_coefficient: self.backoff_coefficient, + initial_interval: self.initial_interval, + maximum_interval: self.maximum_interval, + queue: self.queue.clone(), } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_constant_retry_interval() { + let retry_policy = RetryPolicy::build(1, time::Duration::from_secs(2)).provide(); + let first_interval = retry_policy.retry_interval(1, None); + let second_interval = retry_policy.retry_interval(2, None); + let third_interval = retry_policy.retry_interval(3, None); + + assert_eq!(first_interval, time::Duration::from_secs(2)); + assert_eq!(second_interval, time::Duration::from_secs(2)); + assert_eq!(third_interval, time::Duration::from_secs(2)); + } + + #[test] + fn test_retry_interval_never_exceeds_maximum() { + let retry_policy = RetryPolicy::build(2, time::Duration::from_secs(2)) + .maximum_interval(time::Duration::from_secs(4)) + .provide(); + let first_interval = retry_policy.retry_interval(1, None); + let second_interval = retry_policy.retry_interval(2, None); + let third_interval = retry_policy.retry_interval(3, None); + let fourth_interval = retry_policy.retry_interval(4, None); + + assert_eq!(first_interval, time::Duration::from_secs(2)); + assert_eq!(second_interval, time::Duration::from_secs(4)); + assert_eq!(third_interval, time::Duration::from_secs(4)); + assert_eq!(fourth_interval, time::Duration::from_secs(4)); + } + + #[test] + fn test_retry_interval_increases_with_coefficient() { + let retry_policy = RetryPolicy::build(2, time::Duration::from_secs(2)).provide(); + let first_interval = retry_policy.retry_interval(1, None); + let second_interval = retry_policy.retry_interval(2, None); + let third_interval = retry_policy.retry_interval(3, None); + + assert_eq!(first_interval, time::Duration::from_secs(2)); + assert_eq!(second_interval, time::Duration::from_secs(4)); + assert_eq!(third_interval, time::Duration::from_secs(8)); + } + + #[test] + fn test_retry_interval_respects_preferred() { + let retry_policy = RetryPolicy::build(1, time::Duration::from_secs(2)).provide(); + let preferred = time::Duration::from_secs(999); + let first_interval = retry_policy.retry_interval(1, Some(preferred)); + let second_interval = retry_policy.retry_interval(2, Some(preferred)); + let third_interval = retry_policy.retry_interval(3, Some(preferred)); + + assert_eq!(first_interval, preferred); + assert_eq!(second_interval, preferred); + assert_eq!(third_interval, preferred); + } + + #[test] + fn test_retry_interval_ignores_small_preferred() { + let retry_policy = RetryPolicy::build(1, time::Duration::from_secs(5)).provide(); + let preferred = time::Duration::from_secs(2); + let first_interval = retry_policy.retry_interval(1, Some(preferred)); + let second_interval = retry_policy.retry_interval(2, Some(preferred)); + let third_interval = retry_policy.retry_interval(3, Some(preferred)); + + assert_eq!(first_interval, time::Duration::from_secs(5)); + assert_eq!(second_interval, time::Duration::from_secs(5)); + assert_eq!(third_interval, time::Duration::from_secs(5)); + } + + #[test] + fn test_retry_interval_ignores_large_preferred() { + let retry_policy = RetryPolicy::build(2, time::Duration::from_secs(2)) + .maximum_interval(time::Duration::from_secs(4)) + .provide(); + let preferred = time::Duration::from_secs(10); + let first_interval = retry_policy.retry_interval(1, Some(preferred)); + let second_interval = retry_policy.retry_interval(2, Some(preferred)); + let third_interval = retry_policy.retry_interval(3, Some(preferred)); + + assert_eq!(first_interval, time::Duration::from_secs(2)); + assert_eq!(second_interval, time::Duration::from_secs(4)); + assert_eq!(third_interval, time::Duration::from_secs(4)); + } + + #[test] + fn test_returns_retry_queue_if_set() { + let retry_queue_name = "retry_queue".to_owned(); + let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)) + .queue(&retry_queue_name) + .provide(); + let current_queue = "queue".to_owned(); + + assert_eq!(retry_policy.retry_queue(¤t_queue), retry_queue_name); + } + + #[test] + fn test_returns_queue_if_retry_queue_not_set() { + let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)).provide(); + let current_queue = "queue".to_owned(); + + assert_eq!(retry_policy.retry_queue(¤t_queue), current_queue); + } +} diff --git a/hook-consumer/src/config.rs b/hook-consumer/src/config.rs index 91695128f2192..6525b25253943 100644 --- a/hook-consumer/src/config.rs +++ b/hook-consumer/src/config.rs @@ -72,4 +72,7 @@ pub struct RetryPolicyConfig { #[envconfig(default = "100000")] pub maximum_interval: EnvMsDuration, + + #[envconfig(default = "default")] + pub retry_queue_name: String, } diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 99e97f08d9724..42cb1ff77537e 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -13,7 +13,7 @@ use tokio::sync; use crate::error::{ConsumerError, WebhookError}; -/// A WebhookJob is any PgQueueJob with WebhookJobParameters and WebhookJobMetadata. +/// A WebhookJob is any `PgQueueJob` with `WebhookJobParameters` and `WebhookJobMetadata`. trait WebhookJob: PgQueueJob + std::marker::Send { fn parameters(&self) -> &WebhookJobParameters; fn metadata(&self) -> &WebhookJobMetadata; @@ -147,7 +147,7 @@ impl<'p> WebhookConsumer<'p> { spawn_webhook_job_processing_task( self.client.clone(), semaphore.clone(), - self.retry_policy, + self.retry_policy.clone(), webhook_job, ) .await; @@ -155,10 +155,11 @@ impl<'p> WebhookConsumer<'p> { } else { loop { let webhook_job = self.wait_for_job().await?; + spawn_webhook_job_processing_task( self.client.clone(), semaphore.clone(), - self.retry_policy, + self.retry_policy.clone(), webhook_job, ) .await; @@ -173,6 +174,7 @@ impl<'p> WebhookConsumer<'p> { /// /// * `client`: An HTTP client to execute the webhook job request. /// * `semaphore`: A semaphore used for rate limiting purposes. This function will panic if this semaphore is closed. +/// * `retry_policy`: The retry policy used to set retry parameters if a job fails and has remaining attempts. /// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. async fn spawn_webhook_job_processing_task( client: reqwest::Client, @@ -212,6 +214,7 @@ async fn spawn_webhook_job_processing_task( /// /// * `client`: An HTTP client to execute the webhook job request. /// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. +/// * `retry_policy`: The retry policy used to set retry parameters if a job fails and has remaining attempts. async fn process_webhook_job( client: reqwest::Client, webhook_job: W, @@ -281,10 +284,12 @@ async fn process_webhook_job( } Err(WebhookError::RetryableRequestError { error, retry_after }) => { let retry_interval = - retry_policy.time_until_next_retry(webhook_job.attempt() as u32, retry_after); + retry_policy.retry_interval(webhook_job.attempt() as u32, retry_after); + let current_queue = webhook_job.queue(); + let retry_queue = retry_policy.retry_queue(¤t_queue); match webhook_job - .retry(WebhookJobError::from(&error), retry_interval) + .retry(WebhookJobError::from(&error), retry_interval, retry_queue) .await { Ok(_) => { diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index 38c2ee23042de..c71b8eb91a7d0 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -1,3 +1,4 @@ +//! Consume `PgQueue` jobs to run webhook calls. use envconfig::Envconfig; use hook_common::{ @@ -11,11 +12,13 @@ use hook_consumer::error::ConsumerError; async fn main() -> Result<(), ConsumerError> { let config = Config::init_from_env().expect("Invalid configuration:"); - let retry_policy = RetryPolicy::new( + let retry_policy = RetryPolicy::build( config.retry_policy.backoff_coefficient, config.retry_policy.initial_interval.0, - Some(config.retry_policy.maximum_interval.0), - ); + ) + .maximum_interval(config.retry_policy.maximum_interval.0) + .queue(&config.retry_policy.retry_queue_name) + .provide(); let queue = PgQueue::new(&config.queue_name, &config.table_name, &config.database_url) .await .expect("failed to initialize queue"); From 76476efd75bcd8db3decae7454821ec3fdf17eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 3 Jan 2024 12:11:03 +0100 Subject: [PATCH 160/249] fix: Add org name to image path (#22) --- .github/workflows/docker-hook-consumer.yml | 2 +- .github/workflows/docker-hook-janitor.yml | 2 +- .github/workflows/docker-hook-producer.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-hook-consumer.yml b/.github/workflows/docker-hook-consumer.yml index 5975120e3ff1a..db920744f145c 100644 --- a/.github/workflows/docker-hook-consumer.yml +++ b/.github/workflows/docker-hook-consumer.yml @@ -25,7 +25,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/hook-consumer + images: ghcr.io/posthog/hook-consumer tags: | type=ref,event=pr type=ref,event=branch diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 2649821f697b3..b426d51a5f526 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -25,7 +25,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/hook-janitor + images: ghcr.io/posthog/hook-janitor tags: | type=ref,event=pr type=ref,event=branch diff --git a/.github/workflows/docker-hook-producer.yml b/.github/workflows/docker-hook-producer.yml index d5e131feb014b..ec2594f1f3d46 100644 --- a/.github/workflows/docker-hook-producer.yml +++ b/.github/workflows/docker-hook-producer.yml @@ -25,7 +25,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/hook-producer + images: ghcr.io/posthog/hook-producer tags: | type=ref,event=pr type=ref,event=branch From a277c452190ab7397e70508b3441e1c94364f66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 5 Jan 2024 11:28:19 +0100 Subject: [PATCH 161/249] refactor: Deploy the migration container too (#24) --- .github/workflows/docker-migrator.yml | 62 +++++++++++++++++++++++++++ Dockerfile.migrate | 16 +++++++ Dockerfile.sqlx | 5 --- bin/migrate | 4 ++ docker-compose.yml | 6 +-- 5 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/docker-migrator.yml create mode 100644 Dockerfile.migrate delete mode 100644 Dockerfile.sqlx create mode 100755 bin/migrate diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml new file mode 100644 index 0000000000000..73d7afa20cb62 --- /dev/null +++ b/.github/workflows/docker-migrator.yml @@ -0,0 +1,62 @@ +name: Build hook-migrator docker image + +on: + workflow_dispatch: + push: + branches: + - 'main' + +permissions: + packages: write + +jobs: + build: + name: build and publish hook-migrator image + runs-on: buildjet-4vcpu-ubuntu-2204-arm + steps: + + - name: Check Out Repo + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/posthog/hook-migrator + tags: | + type=ref,event=pr + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push migrator + id: docker_build_hook_migrator + uses: docker/build-push-action@v4 + with: + context: ./ + file: ./Dockerfile.migrate + builder: ${{ steps.buildx.outputs.name }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + platforms: linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Hook-migrator image digest + run: echo ${{ steps.docker_build_hook_migrator.outputs.digest }} diff --git a/Dockerfile.migrate b/Dockerfile.migrate new file mode 100644 index 0000000000000..47790778f4396 --- /dev/null +++ b/Dockerfile.migrate @@ -0,0 +1,16 @@ +FROM docker.io/library/rust:1.74.0-buster as builder + +RUN apt update && apt install build-essential cmake -y +RUN cargo install sqlx-cli@0.7.3 --no-default-features --features native-tls,postgres --root /app/target/release/ + +FROM debian:bullseye-20230320-slim AS runtime +WORKDIR /sqlx + +ADD bin /sqlx/bin/ +ADD migrations /sqlx/migrations/ + +COPY --from=builder /app/target/release/bin/sqlx /usr/local/bin + +RUN chmod +x ./bin/migrate + +CMD ["./bin/migrate"] diff --git a/Dockerfile.sqlx b/Dockerfile.sqlx deleted file mode 100644 index c55dfaa8a960a..0000000000000 --- a/Dockerfile.sqlx +++ /dev/null @@ -1,5 +0,0 @@ -FROM docker.io/library/rust:1.74.0 - -RUN cargo install sqlx-cli --no-default-features --features native-tls,postgres - -WORKDIR /sqlx diff --git a/bin/migrate b/bin/migrate new file mode 100755 index 0000000000000..6e36fc40f8f9a --- /dev/null +++ b/bin/migrate @@ -0,0 +1,4 @@ +#!/bin/sh + +sqlx database create +sqlx migrate run diff --git a/docker-compose.yml b/docker-compose.yml index afaf48ef86d06..d95186492d63e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,18 +19,14 @@ services: container_name: setup-test-db build: context: . - dockerfile: Dockerfile.sqlx + dockerfile: Dockerfile.migrate restart: on-failure - command: > - sh -c "sqlx database create && sqlx migrate run" depends_on: db: condition: service_healthy restart: true environment: DATABASE_URL: postgres://posthog:posthog@db:5432/test_database - volumes: - - ./migrations:/sqlx/migrations/ echo_server: image: docker.io/library/caddy:2 From 3b5229dca92740e0fe0a1af833144c433f109278 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 5 Jan 2024 08:29:24 -0700 Subject: [PATCH 162/249] Change dequeue ORDER BY and rename finished_at (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomás Farías Santana --- hook-common/src/pgqueue.rs | 12 +++++++----- hook-janitor/src/fixtures/webhook_cleanup.sql | 2 +- hook-janitor/src/webhooks.rs | 6 +++--- migrations/20231129172339_job_queue_table.sql | 12 ++++++------ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 39a09ce7fda27..ec5b6843331dd 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -135,7 +135,7 @@ impl Job { UPDATE "{0}" SET - finished_at = NOW(), + last_attempt_finished_at = NOW(), status = 'completed'::job_status WHERE "{0}".id = $2 @@ -182,7 +182,7 @@ RETURNING UPDATE "{0}" SET - finished_at = NOW(), + last_attempt_finished_at = NOW(), status = 'failed'::job_status errors = array_append("{0}".errors, $3) WHERE @@ -441,7 +441,7 @@ impl RetryableJob { UPDATE "{0}" SET - finished_at = NOW(), + last_attempt_finished_at = NOW(), errors = array_append("{0}".errors, $4), queue = $5, status = 'available'::job_status, @@ -606,7 +606,8 @@ WITH available_in_queue AS ( AND scheduled_at <= NOW() AND queue = $1 ORDER BY - id + attempt, + scheduled_at LIMIT 1 FOR UPDATE SKIP LOCKED ) @@ -687,7 +688,8 @@ WITH available_in_queue AS ( AND scheduled_at <= NOW() AND queue = $1 ORDER BY - id + attempt, + scheduled_at LIMIT 1 FOR UPDATE SKIP LOCKED ) diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql index 4aeb231febbd3..bddaf269763d5 100644 --- a/hook-janitor/src/fixtures/webhook_cleanup.sql +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -2,7 +2,7 @@ INSERT INTO job_queue ( errors, metadata, - finished_at, + last_attempt_finished_at, parameters, queue, status, diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 57b984d7bae2f..b14dba1c34b34 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -198,13 +198,13 @@ impl WebhookCleaner { async fn get_completed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { let base_query = format!( r#" - SELECT DATE_TRUNC('hour', finished_at) AS hour, + SELECT DATE_TRUNC('hour', last_attempt_finished_at) AS hour, (metadata->>'team_id')::bigint AS team_id, (metadata->>'plugin_config_id')::bigint AS plugin_config_id, count(*) as successes FROM {0} WHERE status = 'completed' - AND queue = $1 + AND queue = $1 GROUP BY hour, team_id, plugin_config_id ORDER BY hour, team_id, plugin_config_id; "#, @@ -223,7 +223,7 @@ impl WebhookCleaner { async fn get_failed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { let base_query = format!( r#" - SELECT DATE_TRUNC('hour', finished_at) AS hour, + SELECT DATE_TRUNC('hour', last_attempt_finished_at) AS hour, (metadata->>'team_id')::bigint AS team_id, (metadata->>'plugin_config_id')::bigint AS plugin_config_id, errors[array_upper(errors, 1)] AS last_error, diff --git a/migrations/20231129172339_job_queue_table.sql b/migrations/20231129172339_job_queue_table.sql index 7efa154c85816..bf8c3df0a3706 100644 --- a/migrations/20231129172339_job_queue_table.sql +++ b/migrations/20231129172339_job_queue_table.sql @@ -9,21 +9,21 @@ CREATE TABLE job_queue( id BIGSERIAL PRIMARY KEY, attempt INT NOT NULL DEFAULT 0, attempted_at TIMESTAMPTZ DEFAULT NULL, - attempted_by TEXT[] DEFAULT ARRAY[]::TEXT[], + attempted_by TEXT [] DEFAULT ARRAY [] :: TEXT [], created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - errors JSONB[], + errors JSONB [], max_attempts INT NOT NULL DEFAULT 1, metadata JSONB, - finished_at TIMESTAMPTZ DEFAULT NULL, + last_attempt_finished_at TIMESTAMPTZ DEFAULT NULL, parameters JSONB, - queue TEXT NOT NULL DEFAULT 'default'::text, + queue TEXT NOT NULL DEFAULT 'default' :: text, scheduled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - status job_status NOT NULL DEFAULT 'available'::job_status, + status job_status NOT NULL DEFAULT 'available' :: job_status, target TEXT NOT NULL ); -- Needed for `dequeue` queries -CREATE INDEX idx_queue_scheduled_at ON job_queue(queue, status, scheduled_at); +CREATE INDEX idx_queue_scheduled_at ON job_queue(queue, status, scheduled_at, attempt); -- Needed for UPDATE-ing incomplete jobs with a specific target (i.e. slow destinations) CREATE INDEX idx_queue_target ON job_queue(queue, status, target); From 7d519ab19981759aacc0a01ae63859996e0e131e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Mon, 8 Jan 2024 19:22:59 +0100 Subject: [PATCH 163/249] refactor: Remove table name config and hardcode table name (#25) --- hook-common/src/pgqueue.rs | 187 +++++++++----------------- hook-consumer/src/config.rs | 3 - hook-consumer/src/consumer.rs | 3 +- hook-consumer/src/main.rs | 2 +- hook-janitor/src/config.rs | 3 - hook-janitor/src/main.rs | 1 - hook-janitor/src/webhooks.rs | 44 ++---- hook-producer/src/config.rs | 3 - hook-producer/src/handlers/app.rs | 2 +- hook-producer/src/handlers/webhook.rs | 10 +- hook-producer/src/main.rs | 1 - 11 files changed, 88 insertions(+), 171 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index ec5b6843331dd..04573d53f9a41 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -124,29 +124,25 @@ impl Job { /// /// # Arguments /// - /// * `table`: The table where this job will be marked as completed. /// * `executor`: Any sqlx::Executor that can execute the UPDATE query required to mark this `Job` as completed. - async fn complete<'c, E>(self, table: &str, executor: E) -> Result + async fn complete<'c, E>(self, executor: E) -> Result where E: sqlx::Executor<'c, Database = sqlx::Postgres>, { - let base_query = format!( - r#" + let base_query = r#" UPDATE - "{0}" + "job_queue" SET last_attempt_finished_at = NOW(), status = 'completed'::job_status WHERE - "{0}".id = $2 + "job_queue".id = $2 AND queue = $1 RETURNING - "{0}".* - "#, - table - ); + "job_queue".* + "#; - sqlx::query(&base_query) + sqlx::query(base_query) .bind(&self.queue) .bind(self.id) .execute(executor) @@ -164,37 +160,28 @@ RETURNING /// # Arguments /// /// * `error`: Any JSON-serializable value to be stored as an error. - /// * `table`: The table where this job will be marked as failed. /// * `executor`: Any sqlx::Executor that can execute the UPDATE query required to mark this `Job` as failed. - async fn fail<'c, E, S>( - self, - error: S, - table: &str, - executor: E, - ) -> Result, sqlx::Error> + async fn fail<'c, E, S>(self, error: S, executor: E) -> Result, sqlx::Error> where S: serde::Serialize + std::marker::Sync + std::marker::Send, E: sqlx::Executor<'c, Database = sqlx::Postgres>, { let json_error = sqlx::types::Json(error); - let base_query = format!( - r#" + let base_query = r#" UPDATE - "{0}" + "job_queue" SET last_attempt_finished_at = NOW(), status = 'failed'::job_status - errors = array_append("{0}".errors, $3) + errors = array_append("job_queue".errors, $3) WHERE - "{0}".id = $2 + "job_queue".id = $2 AND queue = $1 RETURNING - "{0}".* - "#, - &table - ); + "job_queue".* + "#; - sqlx::query(&base_query) + sqlx::query(base_query) .bind(&self.queue) .bind(self.id) .bind(&json_error) @@ -230,7 +217,6 @@ pub trait PgQueueJob { #[derive(Debug)] pub struct PgJob { pub job: Job, - pub table: String, pub connection: sqlx::pool::PoolConnection, } @@ -239,7 +225,7 @@ impl PgQueueJob for PgJob { async fn complete(mut self) -> Result>>> { let completed_job = self .job - .complete(&self.table, &mut *self.connection) + .complete(&mut *self.connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -255,7 +241,7 @@ impl PgQueueJob for PgJob { ) -> Result, PgJobError>>> { let failed_job = self .job - .fail(error, &self.table, &mut *self.connection) + .fail(error, &mut *self.connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -282,7 +268,7 @@ impl PgQueueJob for PgJob { .job .retryable() .queue(queue) - .retry(error, retry_interval, &self.table, &mut *self.connection) + .retry(error, retry_interval, &mut *self.connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -298,7 +284,6 @@ impl PgQueueJob for PgJob { #[derive(Debug)] pub struct PgTransactionJob<'c, J, M> { pub job: Job, - pub table: String, pub transaction: sqlx::Transaction<'c, sqlx::postgres::Postgres>, } @@ -309,7 +294,7 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio ) -> Result>>> { let completed_job = self .job - .complete(&self.table, &mut *self.transaction) + .complete(&mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -333,7 +318,7 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio ) -> Result, PgJobError>>> { let failed_job = self .job - .fail(error, &self.table, &mut *self.transaction) + .fail(error, &mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -370,7 +355,7 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio .job .retryable() .queue(queue) - .retry(error, retry_interval, &self.table, &mut *self.transaction) + .retry(error, retry_interval, &mut *self.transaction) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -422,13 +407,11 @@ impl RetryableJob { /// /// * `error`: Any JSON-serializable value to be stored as an error. /// * `retry_interval`: The duration until the `Job` is to be retried again. Used to set `scheduled_at`. - /// * `table`: The table where this job will be marked as completed. /// * `executor`: Any sqlx::Executor that can execute the UPDATE query required to mark this `Job` as completed. async fn retry<'c, S, E>( self, error: S, retry_interval: time::Duration, - table: &str, executor: E, ) -> Result where @@ -436,26 +419,23 @@ impl RetryableJob { E: sqlx::Executor<'c, Database = sqlx::Postgres>, { let json_error = sqlx::types::Json(error); - let base_query = format!( - r#" + let base_query = r#" UPDATE - "{0}" + "job_queue" SET last_attempt_finished_at = NOW(), - errors = array_append("{0}".errors, $4), + errors = array_append("job_queue".errors, $4), queue = $5, status = 'available'::job_status, scheduled_at = NOW() + $3 WHERE - "{0}".id = $2 + "job_queue".id = $2 AND queue = $1 RETURNING - "{0}".* - "#, - &table - ); + "job_queue".* + "#; - sqlx::query(&base_query) + sqlx::query(base_query) .bind(&self.queue) .bind(self.id) .bind(retry_interval) @@ -466,7 +446,6 @@ RETURNING Ok(RetriedJob { id: self.id, - table: table.to_owned(), queue: self.queue, retry_queue: self.retry_queue.to_owned(), }) @@ -490,7 +469,6 @@ pub struct RetriedJob { /// A unique id identifying a job queue. pub queue: String, pub retry_queue: Option, - pub table: String, } /// State a `Job` is transitioned to after exhausting all of their attempts. @@ -535,8 +513,6 @@ pub struct PgQueue { name: String, /// A connection pool used to connect to the PostgreSQL database. pool: PgPool, - /// The identifier of the PostgreSQL table this queue runs on. - table: String, } pub type PgQueueResult = std::result::Result; @@ -547,16 +523,14 @@ impl PgQueue { /// # Arguments /// /// * `queue_name`: A name for the queue we are going to initialize. - /// * `table_name`: The name for the table the queue will use in PostgreSQL. /// * `url`: A URL pointing to where the PostgreSQL database is hosted. - pub async fn new(queue_name: &str, table_name: &str, url: &str) -> PgQueueResult { + pub async fn new(queue_name: &str, url: &str) -> PgQueueResult { let name = queue_name.to_owned(); - let table = table_name.to_owned(); let pool = PgPoolOptions::new() .connect_lazy(url) .map_err(|error| PgQueueError::PoolCreationError { error })?; - Ok(Self { name, pool, table }) + Ok(Self { name, pool }) } /// Initialize a new PgQueue backed by table in PostgreSQL from a provided connection pool. @@ -564,17 +538,11 @@ impl PgQueue { /// # Arguments /// /// * `queue_name`: A name for the queue we are going to initialize. - /// * `table_name`: The name for the table the queue will use in PostgreSQL. /// * `pool`: A database connection pool to be used by this queue. - pub async fn new_from_pool( - queue_name: &str, - table_name: &str, - pool: PgPool, - ) -> PgQueueResult { + pub async fn new_from_pool(queue_name: &str, pool: PgPool) -> PgQueueResult { let name = queue_name.to_owned(); - let table = table_name.to_owned(); - Ok(Self { name, pool, table }) + Ok(Self { name, pool }) } /// Dequeue a `Job` from this `PgQueue`. @@ -594,13 +562,12 @@ impl PgQueue { // The query that follows uses a FOR UPDATE SKIP LOCKED clause. // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. - let base_query = format!( - r#" + let base_query = r#" WITH available_in_queue AS ( SELECT id FROM - "{0}" + "job_queue" WHERE status = 'available' AND scheduled_at <= NOW() @@ -612,34 +579,28 @@ WITH available_in_queue AS ( FOR UPDATE SKIP LOCKED ) UPDATE - "{0}" + "job_queue" SET attempted_at = NOW(), status = 'running'::job_status, - attempt = "{0}".attempt + 1, - attempted_by = array_append("{0}".attempted_by, $2::text) + attempt = "job_queue".attempt + 1, + attempted_by = array_append("job_queue".attempted_by, $2::text) FROM available_in_queue WHERE - "{0}".id = available_in_queue.id + "job_queue".id = available_in_queue.id RETURNING - "{0}".* - "#, - &self.table - ); + "job_queue".* + "#; - let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) + let query_result: Result, sqlx::Error> = sqlx::query_as(base_query) .bind(&self.name) .bind(attempted_by) .fetch_one(&mut *connection) .await; match query_result { - Ok(job) => Ok(Some(PgJob { - job, - table: self.table.to_owned(), - connection, - })), + Ok(job) => Ok(Some(PgJob { job, connection })), // Although connection would be closed once it goes out of scope, sqlx recommends explicitly calling close(). // See: https://docs.rs/sqlx/latest/sqlx/postgres/any/trait.AnyConnectionBackend.html#tymethod.close. @@ -676,13 +637,12 @@ RETURNING // The query that follows uses a FOR UPDATE SKIP LOCKED clause. // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. - let base_query = format!( - r#" + let base_query = r#" WITH available_in_queue AS ( SELECT id FROM - "{0}" + "job_queue" WHERE status = 'available' AND scheduled_at <= NOW() @@ -694,23 +654,21 @@ WITH available_in_queue AS ( FOR UPDATE SKIP LOCKED ) UPDATE - "{0}" + "job_queue" SET attempted_at = NOW(), status = 'running'::job_status, - attempt = "{0}".attempt + 1, - attempted_by = array_append("{0}".attempted_by, $2::text) + attempt = "job_queue".attempt + 1, + attempted_by = array_append("job_queue".attempted_by, $2::text) FROM available_in_queue WHERE - "{0}".id = available_in_queue.id + "job_queue".id = available_in_queue.id RETURNING - "{0}".* - "#, - &self.table - ); + "job_queue".* + "#; - let query_result: Result, sqlx::Error> = sqlx::query_as(&base_query) + let query_result: Result, sqlx::Error> = sqlx::query_as(base_query) .bind(&self.name) .bind(attempted_by) .fetch_one(&mut *tx) @@ -719,7 +677,6 @@ RETURNING match query_result { Ok(job) => Ok(Some(PgTransactionJob { job, - table: self.table.to_owned(), transaction: tx, })), @@ -742,17 +699,14 @@ RETURNING job: NewJob, ) -> PgQueueResult<()> { // TODO: Escaping. I think sqlx doesn't support identifiers. - let base_query = format!( - r#" -INSERT INTO {} + let base_query = r#" +INSERT INTO job_queue (attempt, created_at, scheduled_at, max_attempts, metadata, parameters, queue, status, target) VALUES (0, NOW(), NOW(), $1, $2, $3, $4, 'available'::job_status, $5) - "#, - &self.table - ); + "#; - sqlx::query(&base_query) + sqlx::query(base_query) .bind(job.max_attempts) .bind(&job.metadata) .bind(&job.parameters) @@ -826,7 +780,7 @@ mod tests { let worker_id = worker_id(); let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); - let queue = PgQueue::new_from_pool("test_can_dequeue_job", "job_queue", db) + let queue = PgQueue::new_from_pool("test_can_dequeue_job", db) .await .expect("failed to connect to local test postgresql database"); @@ -850,7 +804,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn test_dequeue_returns_none_on_no_jobs(db: PgPool) { let worker_id = worker_id(); - let queue = PgQueue::new_from_pool("test_dequeue_returns_none_on_no_jobs", "job_queue", db) + let queue = PgQueue::new_from_pool("test_dequeue_returns_none_on_no_jobs", db) .await .expect("failed to connect to local test postgresql database"); @@ -870,7 +824,7 @@ mod tests { let worker_id = worker_id(); let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); - let queue = PgQueue::new_from_pool("test_can_dequeue_tx_job", "job_queue", db) + let queue = PgQueue::new_from_pool("test_can_dequeue_tx_job", db) .await .expect("failed to connect to local test postgresql database"); @@ -895,10 +849,9 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn test_dequeue_tx_returns_none_on_no_jobs(db: PgPool) { let worker_id = worker_id(); - let queue = - PgQueue::new_from_pool("test_dequeue_tx_returns_none_on_no_jobs", "job_queue", db) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new_from_pool("test_dequeue_tx_returns_none_on_no_jobs", db) + .await + .expect("failed to connect to local test postgresql database"); let tx_job: Option> = queue .dequeue_tx(&worker_id) @@ -915,14 +868,13 @@ mod tests { let job_metadata = JobMetadata::default(); let worker_id = worker_id(); let new_job = NewJob::new(2, job_metadata, job_parameters, &job_target); - let table_name = "job_queue".to_owned(); let queue_name = "test_can_retry_job_with_remaining_attempts".to_owned(); let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)) .queue(&queue_name) .provide(); - let queue = PgQueue::new_from_pool(&queue_name, &table_name, db) + let queue = PgQueue::new_from_pool(&queue_name, db) .await .expect("failed to connect to local test postgresql database"); @@ -969,7 +921,6 @@ mod tests { let job_metadata = JobMetadata::default(); let worker_id = worker_id(); let new_job = NewJob::new(2, job_metadata, job_parameters, &job_target); - let table_name = "job_queue".to_owned(); let queue_name = "test_can_retry_job_to_different_queue".to_owned(); let retry_queue_name = "test_can_retry_job_to_different_queue_retry".to_owned(); @@ -977,7 +928,7 @@ mod tests { .queue(&retry_queue_name) .provide(); - let queue = PgQueue::new_from_pool(&queue_name, &table_name, db.clone()) + let queue = PgQueue::new_from_pool(&queue_name, db.clone()) .await .expect("failed to connect to queue in local test postgresql database"); @@ -1006,7 +957,7 @@ mod tests { assert!(retried_job_not_found.is_none()); - let queue = PgQueue::new_from_pool(&retry_queue_name, &table_name, db) + let queue = PgQueue::new_from_pool(&retry_queue_name, db) .await .expect("failed to connect to retry queue in local test postgresql database"); @@ -1038,13 +989,9 @@ mod tests { let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)).provide(); - let queue = PgQueue::new_from_pool( - "test_cannot_retry_job_without_remaining_attempts", - "job_queue", - db, - ) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new_from_pool("test_cannot_retry_job_without_remaining_attempts", db) + .await + .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); diff --git a/hook-consumer/src/config.rs b/hook-consumer/src/config.rs index 6525b25253943..01f94e76bd428 100644 --- a/hook-consumer/src/config.rs +++ b/hook-consumer/src/config.rs @@ -34,9 +34,6 @@ pub struct Config { #[envconfig(default = "true")] pub transactional: bool, - - #[envconfig(default = "job_queue")] - pub table_name: String, } impl Config { diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 42cb1ff77537e..671c7b94325d3 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -484,8 +484,7 @@ mod tests { async fn test_wait_for_job(db: PgPool) { let worker_id = worker_id(); let queue_name = "test_wait_for_job".to_string(); - let table_name = "job_queue".to_string(); - let queue = PgQueue::new_from_pool(&queue_name, &table_name, db) + let queue = PgQueue::new_from_pool(&queue_name, db) .await .expect("failed to connect to PG"); diff --git a/hook-consumer/src/main.rs b/hook-consumer/src/main.rs index c71b8eb91a7d0..4182348121bd6 100644 --- a/hook-consumer/src/main.rs +++ b/hook-consumer/src/main.rs @@ -19,7 +19,7 @@ async fn main() -> Result<(), ConsumerError> { .maximum_interval(config.retry_policy.maximum_interval.0) .queue(&config.retry_policy.retry_queue_name) .provide(); - let queue = PgQueue::new(&config.queue_name, &config.table_name, &config.database_url) + let queue = PgQueue::new(&config.queue_name, &config.database_url) .await .expect("failed to initialize queue"); diff --git a/hook-janitor/src/config.rs b/hook-janitor/src/config.rs index c1efb85d38ee5..64db0e613f355 100644 --- a/hook-janitor/src/config.rs +++ b/hook-janitor/src/config.rs @@ -11,9 +11,6 @@ pub struct Config { #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] pub database_url: String, - #[envconfig(default = "job_queue")] - pub table_name: String, - #[envconfig(default = "default")] pub queue_name: String, diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs index 5de3ec4d93978..7d7e2230e548e 100644 --- a/hook-janitor/src/main.rs +++ b/hook-janitor/src/main.rs @@ -55,7 +55,6 @@ async fn main() { Box::new( WebhookCleaner::new( &config.queue_name, - &config.table_name, &config.database_url, kafka_producer, config.kafka.app_metrics_topic.to_owned(), diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index b14dba1c34b34..18a21c9e5d403 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -44,7 +44,6 @@ type Result = std::result::Result; pub struct WebhookCleaner { queue_name: String, - table_name: String, pg_pool: PgPool, kafka_producer: FutureProducer, app_metrics_topic: String, @@ -133,13 +132,11 @@ struct CleanupStats { impl WebhookCleaner { pub fn new( queue_name: &str, - table_name: &str, database_url: &str, kafka_producer: FutureProducer, app_metrics_topic: String, ) -> Result { let queue_name = queue_name.to_owned(); - let table_name = table_name.to_owned(); let pg_pool = PgPoolOptions::new() .acquire_timeout(Duration::from_secs(10)) .connect_lazy(database_url) @@ -147,7 +144,6 @@ impl WebhookCleaner { Ok(Self { queue_name, - table_name, pg_pool, kafka_producer, app_metrics_topic, @@ -157,17 +153,14 @@ impl WebhookCleaner { #[allow(dead_code)] // This is used in tests. pub fn new_from_pool( queue_name: &str, - table_name: &str, pg_pool: PgPool, kafka_producer: FutureProducer, app_metrics_topic: String, ) -> Result { let queue_name = queue_name.to_owned(); - let table_name = table_name.to_owned(); Ok(Self { queue_name, - table_name, pg_pool, kafka_producer, app_metrics_topic, @@ -196,22 +189,19 @@ impl WebhookCleaner { } async fn get_completed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { - let base_query = format!( - r#" + let base_query = r#" SELECT DATE_TRUNC('hour', last_attempt_finished_at) AS hour, (metadata->>'team_id')::bigint AS team_id, (metadata->>'plugin_config_id')::bigint AS plugin_config_id, count(*) as successes - FROM {0} + FROM job_queue WHERE status = 'completed' AND queue = $1 GROUP BY hour, team_id, plugin_config_id ORDER BY hour, team_id, plugin_config_id; - "#, - self.table_name - ); + "#; - let rows = sqlx::query_as::<_, CompletedRow>(&base_query) + let rows = sqlx::query_as::<_, CompletedRow>(base_query) .bind(&self.queue_name) .fetch_all(&mut *tx.0) .await @@ -221,23 +211,20 @@ impl WebhookCleaner { } async fn get_failed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { - let base_query = format!( - r#" + let base_query = r#" SELECT DATE_TRUNC('hour', last_attempt_finished_at) AS hour, (metadata->>'team_id')::bigint AS team_id, (metadata->>'plugin_config_id')::bigint AS plugin_config_id, errors[array_upper(errors, 1)] AS last_error, count(*) as failures - FROM {0} + FROM job_queue WHERE status = 'failed' AND queue = $1 GROUP BY hour, team_id, plugin_config_id, last_error ORDER BY hour, team_id, plugin_config_id, last_error; - "#, - self.table_name - ); + "#; - let rows = sqlx::query_as::<_, FailedRow>(&base_query) + let rows = sqlx::query_as::<_, FailedRow>(base_query) .bind(&self.queue_name) .fetch_all(&mut *tx.0) .await @@ -292,16 +279,13 @@ impl WebhookCleaner { async fn delete_observed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result { // This DELETE is only safe because we are in serializable isolation mode, see the note // in `start_serializable_txn`. - let base_query = format!( - r#" - DELETE FROM {0} + let base_query = r#" + DELETE FROM job_queue WHERE status IN ('failed', 'completed') AND queue = $1; - "#, - self.table_name - ); + "#; - let result = sqlx::query(&base_query) + let result = sqlx::query(base_query) .bind(&self.queue_name) .execute(&mut *tx.0) .await @@ -460,7 +444,6 @@ mod tests { let webhook_cleaner = WebhookCleaner::new_from_pool( &"webhooks", - &"job_queue", db, mock_producer, APP_METRICS_TOPIC.to_owned(), @@ -642,14 +625,13 @@ mod tests { let (_, mock_producer) = create_mock_kafka().await; let webhook_cleaner = WebhookCleaner::new_from_pool( &"webhooks", - &"job_queue", db.clone(), mock_producer, APP_METRICS_TOPIC.to_owned(), ) .expect("unable to create webhook cleaner"); - let queue = PgQueue::new_from_pool("webhooks", "job_queue", db.clone()) + let queue = PgQueue::new_from_pool("webhooks", db.clone()) .await .expect("failed to connect to local test postgresql database"); diff --git a/hook-producer/src/config.rs b/hook-producer/src/config.rs index 87fad5d07dbb1..8daf04e4ca8b8 100644 --- a/hook-producer/src/config.rs +++ b/hook-producer/src/config.rs @@ -11,9 +11,6 @@ pub struct Config { #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] pub database_url: String, - #[envconfig(default = "job_queue")] - pub table_name: String, - #[envconfig(default = "default")] pub queue_name: String, } diff --git a/hook-producer/src/handlers/app.rs b/hook-producer/src/handlers/app.rs index e588dbdb06007..b4d099c96f241 100644 --- a/hook-producer/src/handlers/app.rs +++ b/hook-producer/src/handlers/app.rs @@ -38,7 +38,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn index(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", "job_queue", db) + let pg_queue = PgQueue::new_from_pool("test_index", db) .await .expect("failed to construct pg_queue"); diff --git a/hook-producer/src/handlers/webhook.rs b/hook-producer/src/handlers/webhook.rs index 72923b2441063..ab1552cf71033 100644 --- a/hook-producer/src/handlers/webhook.rs +++ b/hook-producer/src/handlers/webhook.rs @@ -119,7 +119,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_success(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", "job_queue", db) + let pg_queue = PgQueue::new_from_pool("test_index", db) .await .expect("failed to construct pg_queue"); @@ -163,7 +163,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_bad_url(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", "job_queue", db) + let pg_queue = PgQueue::new_from_pool("test_index", db) .await .expect("failed to construct pg_queue"); @@ -202,7 +202,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_payload_missing_fields(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", "job_queue", db) + let pg_queue = PgQueue::new_from_pool("test_index", db) .await .expect("failed to construct pg_queue"); @@ -225,7 +225,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_payload_not_json(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", "job_queue", db) + let pg_queue = PgQueue::new_from_pool("test_index", db) .await .expect("failed to construct pg_queue"); @@ -248,7 +248,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_payload_body_too_large(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", "job_queue", db) + let pg_queue = PgQueue::new_from_pool("test_index", db) .await .expect("failed to construct pg_queue"); diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index 39f45004271bc..d0190c2a37a54 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -27,7 +27,6 @@ async fn main() { // TODO: Coupling the queue name to the PgQueue object doesn't seem ideal from the producer // side, but we don't need more than one queue for now. &config.queue_name, - &config.table_name, &config.database_url, ) .await From ead1bf28ef8d092848cceedf8e2ae8867b02276d Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 9 Jan 2024 02:13:30 -0700 Subject: [PATCH 164/249] Minor query syntax/ordering cleanup (#26) --- hook-common/src/pgqueue.rs | 56 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 04573d53f9a41..35f5b4a9e47fb 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -131,15 +131,15 @@ impl Job { { let base_query = r#" UPDATE - "job_queue" + job_queue SET last_attempt_finished_at = NOW(), status = 'completed'::job_status WHERE - "job_queue".id = $2 - AND queue = $1 + queue = $1 + AND id = $2 RETURNING - "job_queue".* + job_queue.* "#; sqlx::query(base_query) @@ -169,16 +169,16 @@ RETURNING let json_error = sqlx::types::Json(error); let base_query = r#" UPDATE - "job_queue" + job_queue SET last_attempt_finished_at = NOW(), status = 'failed'::job_status - errors = array_append("job_queue".errors, $3) + errors = array_append(errors, $3) WHERE - "job_queue".id = $2 - AND queue = $1 + queue = $1 + AND id = $2 RETURNING - "job_queue".* + job_queue.* "#; sqlx::query(base_query) @@ -421,18 +421,18 @@ impl RetryableJob { let json_error = sqlx::types::Json(error); let base_query = r#" UPDATE - "job_queue" + job_queue SET last_attempt_finished_at = NOW(), - errors = array_append("job_queue".errors, $4), - queue = $5, status = 'available'::job_status, - scheduled_at = NOW() + $3 + scheduled_at = NOW() + $3, + errors = array_append(errors, $4), + queue = $5 WHERE - "job_queue".id = $2 - AND queue = $1 + queue = $1 + AND id = $2 RETURNING - "job_queue".* + job_queue.* "#; sqlx::query(base_query) @@ -567,7 +567,7 @@ WITH available_in_queue AS ( SELECT id FROM - "job_queue" + job_queue WHERE status = 'available' AND scheduled_at <= NOW() @@ -579,18 +579,18 @@ WITH available_in_queue AS ( FOR UPDATE SKIP LOCKED ) UPDATE - "job_queue" + job_queue SET attempted_at = NOW(), status = 'running'::job_status, - attempt = "job_queue".attempt + 1, - attempted_by = array_append("job_queue".attempted_by, $2::text) + attempt = attempt + 1, + attempted_by = array_append(attempted_by, $2::text) FROM available_in_queue WHERE - "job_queue".id = available_in_queue.id + job_queue.id = available_in_queue.id RETURNING - "job_queue".* + job_queue.* "#; let query_result: Result, sqlx::Error> = sqlx::query_as(base_query) @@ -642,7 +642,7 @@ WITH available_in_queue AS ( SELECT id FROM - "job_queue" + job_queue WHERE status = 'available' AND scheduled_at <= NOW() @@ -654,18 +654,18 @@ WITH available_in_queue AS ( FOR UPDATE SKIP LOCKED ) UPDATE - "job_queue" + job_queue SET attempted_at = NOW(), status = 'running'::job_status, - attempt = "job_queue".attempt + 1, - attempted_by = array_append("job_queue".attempted_by, $2::text) + attempt = attempt + 1, + attempted_by = array_append(attempted_by, $2::text) FROM available_in_queue WHERE - "job_queue".id = available_in_queue.id + job_queue.id = available_in_queue.id RETURNING - "job_queue".* + job_queue.* "#; let query_result: Result, sqlx::Error> = sqlx::query_as(base_query) From 892d30b5c53fc4f4a724997f1fc370da93c88db7 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 9 Jan 2024 09:23:09 -0700 Subject: [PATCH 165/249] Add metrics, fix metrics endpoint, bump deps (#27) --- Cargo.lock | 271 +++++++++++--------------- Cargo.toml | 4 +- hook-common/src/metrics.rs | 9 +- hook-consumer/src/consumer.rs | 19 +- hook-janitor/src/webhooks.rs | 17 +- hook-producer/src/handlers/app.rs | 16 +- hook-producer/src/handlers/mod.rs | 2 +- hook-producer/src/handlers/webhook.rs | 20 +- hook-producer/src/main.rs | 7 +- 9 files changed, 172 insertions(+), 193 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdef24de2af58..e4da8161adcc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "getrandom", @@ -62,13 +62,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -104,9 +104,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +checksum = "d09dbe0e490df5da9d69b36dca48a76635288a82f92eca90024883a56202026d" dependencies = [ "async-trait", "axum-core", @@ -133,13 +133,14 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" +checksum = "e87c8503f93e6d144ee5690907ba22db7ba79ab001a932ab99034f0fe836b3df" dependencies = [ "async-trait", "bytes", @@ -153,6 +154,7 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -172,9 +174,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" [[package]] name = "base64ct" @@ -286,9 +288,9 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -310,34 +312,27 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crossbeam-epoch" -version = "0.9.16" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2fe95351b870527a5d09bf563ed3c97c0cffb87cf1c78a591bf48bb218d9aa" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", ] [[package]] name = "crossbeam-queue" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9bcf5bdbfdd6030fb4a1c497b5d5fc5921aa2f60d359a17e249c0e6df3de153" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.17" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d96137f14f244c37f989d9fff8f95e6c18b918e71f36638f8c49112e4c78f" -dependencies = [ - "cfg-if", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -514,9 +509,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -529,9 +524,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -539,15 +534,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -567,38 +562,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -660,9 +655,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +checksum = "991910e35c615d8cab86b5ab04be67e6ad24d2bf5f4f11fdbbed26da999bbeab" dependencies = [ "bytes", "fnv", @@ -948,7 +943,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.0", + "h2 0.4.1", "http 1.0.0", "http-body 1.0.0", "httparse", @@ -973,9 +968,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" dependencies = [ "bytes", "futures-channel", @@ -986,16 +981,14 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", - "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1091,9 +1084,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libm" @@ -1114,9 +1107,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +checksum = "5f526fdd09d99e19742883e43de41e1aa9e36db0c7ab7f935165d611c5cccc66" dependencies = [ "cc", "libc", @@ -1146,15 +1139,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "mach2" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" -dependencies = [ - "libc", -] - [[package]] name = "matchit" version = "0.7.3" @@ -1173,38 +1157,29 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "metrics" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5" +checksum = "77b9e10a211c839210fd7f99954bda26e5f8e26ec686ad68da6a32df7c80e782" dependencies = [ "ahash", - "metrics-macros", "portable-atomic", ] [[package]] name = "metrics-exporter-prometheus" -version = "0.12.2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d4fa7ce7c4862db464a37b0b31d89bca874562f034bd7993895572783d02950" +checksum = "83a4c4718a371ddfb7806378f23617876eea8b82e5ff1324516bcd283249d9ea" dependencies = [ "base64", "hyper 0.14.28", + "hyper-tls", "indexmap 1.9.3", "ipnet", "metrics", @@ -1215,22 +1190,11 @@ dependencies = [ "tracing", ] -[[package]] -name = "metrics-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddece26afd34c31585c74a4db0630c376df271c285d682d1e55012197830b6df" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.41", -] - [[package]] name = "metrics-util" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de2ed6e491ed114b40b732e4d1659a9d53992ebd87490c44a6ffe23739d973e" +checksum = "2670b8badcc285d486261e2e9f1615b506baff91427b61bd336a472b65bbf5ed" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1403,9 +1367,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1418,9 +1382,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.61" +version = "0.10.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -1439,7 +1403,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1450,9 +1414,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.97" +version = "0.9.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", @@ -1527,7 +1491,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -1565,9 +1529,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "portable-atomic" @@ -1593,22 +1557,21 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quanta" -version = "0.11.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +checksum = "9ca0b7bac0b97248c40bb77288fc52029cf1459c0461ea1b05ee32ccf011de2c" dependencies = [ "crossbeam-utils", "libc", - "mach2", "once_cell", "raw-cpuid", "wasi", @@ -1618,9 +1581,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1657,11 +1620,11 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "10.7.0" +version = "11.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +checksum = "9d86a7c4638d42c44551f4791a20e687dbb4c3de1f33c43dd71e355cd429def1" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.1", ] [[package]] @@ -1826,11 +1789,11 @@ checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1864,29 +1827,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1895,9 +1858,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" dependencies = [ "itoa", "serde", @@ -2268,9 +2231,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2306,35 +2269,35 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2364,9 +2327,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d45b238a16291a4e1584e61820b8ae57d696cc5015c459c229ccc6990cc1c" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -2389,7 +2352,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2492,7 +2455,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] @@ -2657,7 +2620,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -2691,7 +2654,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2742,11 +2705,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.0", ] [[package]] @@ -2883,9 +2846,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.30" +version = "0.5.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" +checksum = "b7520bbdec7211caa7c4e682eb1fbe07abe20cee6756b6e00f537c82c11816aa" dependencies = [ "memchr", ] @@ -2902,22 +2865,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.41", + "syn 2.0.48", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1f6a38b6e3ed1..b7cb8fac622c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,8 @@ eyre = "0.6.9" futures = { version = "0.3.29" } http = { version = "0.2" } http-body-util = "0.1.0" -metrics = "0.21.1" -metrics-exporter-prometheus = "0.12.1" +metrics = "0.22.0" +metrics-exporter-prometheus = "0.13.0" rdkafka = { version = "0.35.0", features = ["cmake-build", "ssl", "tracing"] } reqwest = { version = "0.11" } regex = "1.10.2" diff --git a/hook-common/src/metrics.rs b/hook-common/src/metrics.rs index 3d9e4c02655a5..1f57c5eec51f9 100644 --- a/hook-common/src/metrics.rs +++ b/hook-common/src/metrics.rs @@ -21,7 +21,10 @@ pub fn setup_metrics_router() -> Router { let recorder_handle = setup_metrics_recorder(); Router::new() - .route("/metrics", get(recorder_handle.render())) + .route( + "/metrics", + get(move || std::future::ready(recorder_handle.render())), + ) .layer(axum::middleware::from_fn(track_metrics)) } @@ -63,8 +66,8 @@ pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse ("status", status), ]; - metrics::increment_counter!("http_requests_total", &labels); - metrics::histogram!("http_requests_duration_seconds", latency, &labels); + metrics::counter!("http_requests_total", &labels).increment(1); + metrics::histogram!("http_requests_duration_seconds", &labels).record(latency); response } diff --git a/hook-consumer/src/consumer.rs b/hook-consumer/src/consumer.rs index 671c7b94325d3..2114ef80c8bc8 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-consumer/src/consumer.rs @@ -192,7 +192,7 @@ async fn spawn_webhook_job_processing_task( ("target", webhook_job.target()), ]; - metrics::increment_counter!("webhook_jobs_total", &labels); + metrics::counter!("webhook_jobs_total", &labels).increment(1); tokio::spawn(async move { let result = process_webhook_job(client, webhook_job, &retry_policy).await; @@ -247,8 +247,9 @@ async fn process_webhook_job( .await .map_err(|error| ConsumerError::PgJobError(error.to_string()))?; - metrics::increment_counter!("webhook_jobs_completed", &labels); - metrics::histogram!("webhook_jobs_processing_duration_seconds", elapsed, &labels); + metrics::counter!("webhook_jobs_completed", &labels).increment(1); + metrics::histogram!("webhook_jobs_processing_duration_seconds", &labels) + .record(elapsed); Ok(()) } @@ -258,7 +259,7 @@ async fn process_webhook_job( .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; - metrics::increment_counter!("webhook_jobs_failed", &labels); + metrics::counter!("webhook_jobs_failed", &labels).increment(1); Ok(()) } @@ -268,7 +269,7 @@ async fn process_webhook_job( .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; - metrics::increment_counter!("webhook_jobs_failed", &labels); + metrics::counter!("webhook_jobs_failed", &labels).increment(1); Ok(()) } @@ -278,7 +279,7 @@ async fn process_webhook_job( .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; - metrics::increment_counter!("webhook_jobs_failed", &labels); + metrics::counter!("webhook_jobs_failed", &labels).increment(1); Ok(()) } @@ -293,7 +294,7 @@ async fn process_webhook_job( .await { Ok(_) => { - metrics::increment_counter!("webhook_jobs_retried", &labels); + metrics::counter!("webhook_jobs_retried", &labels).increment(1); Ok(()) } @@ -305,7 +306,7 @@ async fn process_webhook_job( .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; - metrics::increment_counter!("webhook_jobs_failed", &labels); + metrics::counter!("webhook_jobs_failed", &labels).increment(1); Ok(()) } @@ -318,7 +319,7 @@ async fn process_webhook_job( .await .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; - metrics::increment_counter!("webhook_jobs_failed", &labels); + metrics::counter!("webhook_jobs_failed", &labels).increment(1); Ok(()) } diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 18a21c9e5d403..228187a735433 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::time::{Duration, Instant}; use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -350,9 +350,23 @@ impl WebhookCleaner { #[async_trait] impl Cleaner for WebhookCleaner { async fn cleanup(&self) { + let start_time = Instant::now(); + match self.cleanup_impl().await { Ok(stats) => { + metrics::counter!("webhook_cleanup_runs",).increment(1); + if stats.rows_processed > 0 { + let elapsed_time = start_time.elapsed().as_secs_f64(); + metrics::histogram!("webhook_cleanup_duration").record(elapsed_time); + + metrics::counter!("webhook_cleanup_rows_processed",) + .increment(stats.rows_processed); + metrics::counter!("webhook_cleanup_completed_agg_row_count",) + .increment(stats.completed_agg_row_count as u64); + metrics::counter!("webhook_cleanup_failed_agg_row_count",) + .increment(stats.failed_agg_row_count as u64); + debug!( rows_processed = stats.rows_processed, completed_agg_row_count = stats.completed_agg_row_count, @@ -364,6 +378,7 @@ impl Cleaner for WebhookCleaner { } } Err(error) => { + metrics::counter!("webhook_cleanup_failures",).increment(1); error!(error = ?error, "WebhookCleaner::cleanup failed"); } } diff --git a/hook-producer/src/handlers/app.rs b/hook-producer/src/handlers/app.rs index b4d099c96f241..2cafc4a497ae2 100644 --- a/hook-producer/src/handlers/app.rs +++ b/hook-producer/src/handlers/app.rs @@ -1,23 +1,13 @@ use axum::{routing, Router}; -use metrics_exporter_prometheus::PrometheusHandle; -use hook_common::metrics; use hook_common::pgqueue::PgQueue; use super::webhook; -pub fn app(pg_pool: PgQueue, metrics: Option) -> Router { - Router::new() +pub fn add_routes(router: Router, pg_pool: PgQueue) -> Router { + router .route("/", routing::get(index)) - .route( - "/metrics", - routing::get(move || match metrics { - Some(ref recorder_handle) => std::future::ready(recorder_handle.render()), - None => std::future::ready("no metrics recorder installed".to_owned()), - }), - ) .route("/webhook", routing::post(webhook::post).with_state(pg_pool)) - .layer(axum::middleware::from_fn(metrics::track_metrics)) } pub async fn index() -> &'static str { @@ -42,7 +32,7 @@ mod tests { .await .expect("failed to construct pg_queue"); - let app = app(pg_queue, None); + let app = add_routes(Router::new(), pg_queue); let response = app .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) diff --git a/hook-producer/src/handlers/mod.rs b/hook-producer/src/handlers/mod.rs index 88f96717c130a..e392f8a9b67f7 100644 --- a/hook-producer/src/handlers/mod.rs +++ b/hook-producer/src/handlers/mod.rs @@ -1,4 +1,4 @@ mod app; mod webhook; -pub use app::app; +pub use app::add_routes; diff --git a/hook-producer/src/handlers/webhook.rs b/hook-producer/src/handlers/webhook.rs index ab1552cf71033..62a4aaa8833c8 100644 --- a/hook-producer/src/handlers/webhook.rs +++ b/hook-producer/src/handlers/webhook.rs @@ -1,3 +1,5 @@ +use std::time::Instant; + use axum::{extract::State, http::StatusCode, Json}; use hook_common::webhook::{WebhookJobMetadata, WebhookJobParameters}; use serde_derive::Deserialize; @@ -61,8 +63,13 @@ pub async fn post( url_hostname.as_str(), ); + let start_time = Instant::now(); + pg_queue.enqueue(job).await.map_err(internal_error)?; + let elapsed_time = start_time.elapsed().as_secs_f64(); + metrics::histogram!("webhook_producer_enqueue").record(elapsed_time); + Ok(Json(WebhookPostResponse { error: None })) } @@ -107,6 +114,7 @@ mod tests { use axum::{ body::Body, http::{self, Request, StatusCode}, + Router, }; use hook_common::pgqueue::PgQueue; use hook_common::webhook::{HttpMethod, WebhookJobParameters}; @@ -115,7 +123,7 @@ mod tests { use std::collections; use tower::ServiceExt; // for `call`, `oneshot`, and `ready` - use crate::handlers::app; + use crate::handlers::app::add_routes; #[sqlx::test(migrations = "../migrations")] async fn webhook_success(db: PgPool) { @@ -123,7 +131,7 @@ mod tests { .await .expect("failed to construct pg_queue"); - let app = app(pg_queue, None); + let app = add_routes(Router::new(), pg_queue); let mut headers = collections::HashMap::new(); headers.insert("Content-Type".to_owned(), "application/json".to_owned()); @@ -167,7 +175,7 @@ mod tests { .await .expect("failed to construct pg_queue"); - let app = app(pg_queue, None); + let app = add_routes(Router::new(), pg_queue); let response = app .oneshot( @@ -206,7 +214,7 @@ mod tests { .await .expect("failed to construct pg_queue"); - let app = app(pg_queue, None); + let app = add_routes(Router::new(), pg_queue); let response = app .oneshot( @@ -229,7 +237,7 @@ mod tests { .await .expect("failed to construct pg_queue"); - let app = app(pg_queue, None); + let app = add_routes(Router::new(), pg_queue); let response = app .oneshot( @@ -252,7 +260,7 @@ mod tests { .await .expect("failed to construct pg_queue"); - let app = app(pg_queue, None); + let app = add_routes(Router::new(), pg_queue); let bytes: Vec = vec![b'a'; 1_000_000 * 2]; let long_string = String::from_utf8_lossy(&bytes); diff --git a/hook-producer/src/main.rs b/hook-producer/src/main.rs index d0190c2a37a54..f078f2fae7781 100644 --- a/hook-producer/src/main.rs +++ b/hook-producer/src/main.rs @@ -3,7 +3,7 @@ use config::Config; use envconfig::Envconfig; use eyre::Result; -use hook_common::metrics; +use hook_common::metrics::setup_metrics_router; use hook_common::pgqueue::PgQueue; mod config; @@ -32,9 +32,8 @@ async fn main() { .await .expect("failed to initialize queue"); - let recorder_handle = metrics::setup_metrics_recorder(); - - let app = handlers::app(pg_queue, Some(recorder_handle)); + let router = setup_metrics_router(); + let app = handlers::add_routes(router, pg_queue); match listen(app, config.bind()).await { Ok(_) => {} From 35d7e331aee28fa5cf2f76196d5664b6d1620492 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 9 Jan 2024 09:33:54 -0700 Subject: [PATCH 166/249] Get total row counts in janitor for metrics and consistency checking (#28) --- hook-janitor/src/webhooks.rs | 114 ++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 22 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 228187a735433..de02d07a2520e 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -9,7 +9,7 @@ use rdkafka::producer::{FutureProducer, FutureRecord}; use serde_json::error::Error as SerdeError; use sqlx::postgres::{PgPool, PgPoolOptions, Postgres}; use sqlx::types::{chrono, Uuid}; -use sqlx::Transaction; +use sqlx::{Row, Transaction}; use thiserror::Error; use tracing::{debug, error}; @@ -24,6 +24,8 @@ pub enum WebhookCleanerError { PoolCreationError { error: sqlx::Error }, #[error("failed to acquire conn and start txn: {error}")] StartTxnError { error: sqlx::Error }, + #[error("failed to get row count: {error}")] + GetRowCountError { error: sqlx::Error }, #[error("failed to get completed rows: {error}")] GetCompletedRowsError { error: sqlx::Error }, #[error("failed to get failed rows: {error}")] @@ -36,6 +38,10 @@ pub enum WebhookCleanerError { KafkaProduceCanceled, #[error("failed to delete rows: {error}")] DeleteRowsError { error: sqlx::Error }, + #[error("attempted to delete a different number of rows than expected")] + DeleteConsistencyError, + #[error("failed to rollback txn: {error}")] + RollbackTxnError { error: sqlx::Error }, #[error("failed to commit txn: {error}")] CommitTxnError { error: sqlx::Error }, } @@ -125,8 +131,10 @@ struct SerializableTxn<'a>(Transaction<'a, Postgres>); struct CleanupStats { rows_processed: u64, - completed_agg_row_count: usize, - failed_agg_row_count: usize, + completed_row_count: u64, + completed_agg_row_count: u64, + failed_row_count: u64, + failed_agg_row_count: u64, } impl WebhookCleaner { @@ -188,7 +196,32 @@ impl WebhookCleaner { Ok(SerializableTxn(tx)) } - async fn get_completed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { + async fn get_row_count_for_status( + &self, + tx: &mut SerializableTxn<'_>, + status: &str, + ) -> Result { + let base_query = r#" + SELECT count(*) FROM job_queue + WHERE queue = $1 + AND status = $2::job_status; + "#; + + let count: i64 = sqlx::query(base_query) + .bind(&self.queue_name) + .bind(status) + .fetch_one(&mut *tx.0) + .await + .map_err(|e| WebhookCleanerError::GetRowCountError { error: e })? + .get(0); + + Ok(count as u64) + } + + async fn get_completed_agg_rows( + &self, + tx: &mut SerializableTxn<'_>, + ) -> Result> { let base_query = r#" SELECT DATE_TRUNC('hour', last_attempt_finished_at) AS hour, (metadata->>'team_id')::bigint AS team_id, @@ -210,7 +243,7 @@ impl WebhookCleaner { Ok(rows) } - async fn get_failed_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { + async fn get_failed_agg_rows(&self, tx: &mut SerializableTxn<'_>) -> Result> { let base_query = r#" SELECT DATE_TRUNC('hour', last_attempt_finished_at) AS hour, (metadata->>'team_id')::bigint AS team_id, @@ -294,6 +327,14 @@ impl WebhookCleaner { Ok(result.rows_affected()) } + async fn rollback_txn(&self, tx: SerializableTxn<'_>) -> Result<()> { + tx.0.rollback() + .await + .map_err(|e| WebhookCleanerError::RollbackTxnError { error: e })?; + + Ok(()) + } + async fn commit_txn(&self, tx: SerializableTxn<'_>) -> Result<()> { tx.0.commit() .await @@ -315,33 +356,53 @@ impl WebhookCleaner { let mut tx = self.start_serializable_txn().await?; - let completed_agg_row_count = { - let completed_rows = self.get_completed_rows(&mut tx).await?; - let row_count = completed_rows.len(); + let (completed_row_count, completed_agg_row_count) = { + let completed_row_count = self.get_row_count_for_status(&mut tx, "completed").await?; + let completed_agg_rows = self.get_completed_agg_rows(&mut tx).await?; + let agg_row_count = completed_agg_rows.len() as u64; let completed_app_metrics: Vec = - completed_rows.into_iter().map(Into::into).collect(); + completed_agg_rows.into_iter().map(Into::into).collect(); self.send_metrics_to_kafka(completed_app_metrics).await?; - row_count + (completed_row_count, agg_row_count) }; - let failed_agg_row_count = { - let failed_rows = self.get_failed_rows(&mut tx).await?; - let row_count = failed_rows.len(); + let (failed_row_count, failed_agg_row_count) = { + let failed_row_count = self.get_row_count_for_status(&mut tx, "failed").await?; + let failed_agg_rows = self.get_failed_agg_rows(&mut tx).await?; + let agg_row_count = failed_agg_rows.len() as u64; let failed_app_metrics: Vec = - failed_rows.into_iter().map(Into::into).collect(); + failed_agg_rows.into_iter().map(Into::into).collect(); self.send_metrics_to_kafka(failed_app_metrics).await?; - row_count + (failed_row_count, agg_row_count) }; - let mut rows_processed = 0; + let mut rows_deleted = 0; if completed_agg_row_count + failed_agg_row_count != 0 { - rows_processed = self.delete_observed_rows(&mut tx).await?; + rows_deleted = self.delete_observed_rows(&mut tx).await?; + + if rows_deleted != completed_row_count + failed_row_count { + // This should never happen, but if it does, we want to know about it (and abort the + // txn). + error!( + attempted_rows_deleted = rows_deleted, + completed_row_count = completed_row_count, + failed_row_count = failed_row_count, + "WebhookCleaner::cleanup attempted to delete a different number of rows than expected" + ); + + self.rollback_txn(tx).await?; + + return Err(WebhookCleanerError::DeleteConsistencyError); + } + self.commit_txn(tx).await?; } Ok(CleanupStats { - rows_processed, + rows_processed: rows_deleted, + completed_row_count, completed_agg_row_count, + failed_row_count, failed_agg_row_count, }) } @@ -362,14 +423,20 @@ impl Cleaner for WebhookCleaner { metrics::counter!("webhook_cleanup_rows_processed",) .increment(stats.rows_processed); + metrics::counter!("webhook_cleanup_completed_row_count",) + .increment(stats.completed_row_count); metrics::counter!("webhook_cleanup_completed_agg_row_count",) - .increment(stats.completed_agg_row_count as u64); + .increment(stats.completed_agg_row_count); + metrics::counter!("webhook_cleanup_failed_row_count",) + .increment(stats.failed_row_count); metrics::counter!("webhook_cleanup_failed_agg_row_count",) - .increment(stats.failed_agg_row_count as u64); + .increment(stats.failed_agg_row_count); debug!( rows_processed = stats.rows_processed, + completed_row_count = stats.completed_row_count, completed_agg_row_count = stats.completed_agg_row_count, + failed_row_count = stats.failed_row_count, failed_agg_row_count = stats.failed_agg_row_count, "WebhookCleaner::cleanup finished" ); @@ -665,8 +732,11 @@ mod tests { // Important! Serializable txn is started here. let mut tx = webhook_cleaner.start_serializable_txn().await.unwrap(); - webhook_cleaner.get_completed_rows(&mut tx).await.unwrap(); - webhook_cleaner.get_failed_rows(&mut tx).await.unwrap(); + webhook_cleaner + .get_completed_agg_rows(&mut tx) + .await + .unwrap(); + webhook_cleaner.get_failed_agg_rows(&mut tx).await.unwrap(); // All 13 rows in the queue are visible from outside the txn. // The 11 the cleaner will process, plus 1 available and 1 running. From b00ef383563ffe22ec40e5607e57863c65b1560b Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 9 Jan 2024 10:23:40 -0700 Subject: [PATCH 167/249] Login to DockerHub in CI (#74) * Login to DockerHub * Add to rust.yml --- .github/workflows/docker.yml | 8 +++++++- .github/workflows/rust.yml | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 77e731ecebcf2..1fdc6520bd004 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -37,7 +37,13 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io uses: docker/login-action@v2 with: registry: ghcr.io diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 91cf2018ee7e4..de8ec08916e85 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -40,6 +40,12 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Setup end2end dependencies run: | docker compose up -d --wait From 86e6a26ef049f522af5cd0774a92328b8c7161ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Tue, 9 Jan 2024 18:24:11 +0100 Subject: [PATCH 168/249] refactor: Rename producer and consumer images (#29) --- ...ocker-hook-producer.yml => docker-hook-api.yml} | 14 +++++++------- ...er-hook-consumer.yml => docker-hook-worker.yml} | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) rename .github/workflows/{docker-hook-producer.yml => docker-hook-api.yml} (80%) rename .github/workflows/{docker-hook-consumer.yml => docker-hook-worker.yml} (80%) diff --git a/.github/workflows/docker-hook-producer.yml b/.github/workflows/docker-hook-api.yml similarity index 80% rename from .github/workflows/docker-hook-producer.yml rename to .github/workflows/docker-hook-api.yml index ec2594f1f3d46..8c645bc4696fd 100644 --- a/.github/workflows/docker-hook-producer.yml +++ b/.github/workflows/docker-hook-api.yml @@ -1,4 +1,4 @@ -name: Build hook-producer docker image +name: Build hook-api docker image on: workflow_dispatch: @@ -11,7 +11,7 @@ permissions: jobs: build: - name: build and publish hook-producer image + name: build and publish hook-api image runs-on: buildjet-4vcpu-ubuntu-2204-arm steps: @@ -25,7 +25,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hook-producer + images: ghcr.io/posthog/hook-api tags: | type=ref,event=pr type=ref,event=branch @@ -44,8 +44,8 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push producer - id: docker_build_hook_producer + - name: Build and push api + id: docker_build_hook_api uses: docker/build-push-action@v4 with: context: ./ @@ -59,5 +59,5 @@ jobs: cache-to: type=gha,mode=max build-args: BIN=hook-producer - - name: Hook-producer image digest - run: echo ${{ steps.docker_build_hook_producer.outputs.digest }} + - name: Hook-api image digest + run: echo ${{ steps.docker_build_hook_api.outputs.digest }} diff --git a/.github/workflows/docker-hook-consumer.yml b/.github/workflows/docker-hook-worker.yml similarity index 80% rename from .github/workflows/docker-hook-consumer.yml rename to .github/workflows/docker-hook-worker.yml index db920744f145c..a5d6c923b57d1 100644 --- a/.github/workflows/docker-hook-consumer.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -1,4 +1,4 @@ -name: Build hook-consumer docker image +name: Build hook-worker docker image on: workflow_dispatch: @@ -11,7 +11,7 @@ permissions: jobs: build: - name: build and publish hook-consumer image + name: build and publish hook-worker image runs-on: buildjet-4vcpu-ubuntu-2204-arm steps: @@ -25,7 +25,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hook-consumer + images: ghcr.io/posthog/hook-worker tags: | type=ref,event=pr type=ref,event=branch @@ -44,8 +44,8 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push consumer - id: docker_build_hook_consumer + - name: Build and push worker + id: docker_build_hook_worker uses: docker/build-push-action@v4 with: context: ./ @@ -59,5 +59,5 @@ jobs: cache-to: type=gha,mode=max build-args: BIN=hook-consumer - - name: Hook-consumer image digest - run: echo ${{ steps.docker_build_hook_consumer.outputs.digest }} + - name: Hook-worker image digest + run: echo ${{ steps.docker_build_hook_worker.outputs.digest }} From 29c2415466665b48c12b0dfa09b601b2006ef51f Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 9 Jan 2024 10:35:24 -0700 Subject: [PATCH 169/249] Login to DockerHub in CI (#30) --- .github/workflows/docker-hook-api.yml | 8 +++++++- .github/workflows/docker-hook-janitor.yml | 8 +++++++- .github/workflows/docker-hook-worker.yml | 8 +++++++- .github/workflows/docker-migrator.yml | 8 +++++++- .github/workflows/rust.yml | 6 ++++++ 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 8c645bc4696fd..abcd9f654a3a5 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -37,7 +37,13 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io uses: docker/login-action@v2 with: registry: ghcr.io diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index b426d51a5f526..1f4cae924ff72 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -37,7 +37,13 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io uses: docker/login-action@v2 with: registry: ghcr.io diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index a5d6c923b57d1..25e4b3a92db8f 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -37,7 +37,13 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io uses: docker/login-action@v2 with: registry: ghcr.io diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index 73d7afa20cb62..1ad3893c8386a 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -37,7 +37,13 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io uses: docker/login-action@v2 with: registry: ghcr.io diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b811c1a52e668..f3aafb00260b4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -43,6 +43,12 @@ jobs: with: toolchain: stable + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Stop/Start stack with Docker Compose shell: bash run: | From 2499ea6a1f63f0584403462869b181364d0a9db6 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 9 Jan 2024 12:26:46 -0700 Subject: [PATCH 170/249] Login to Docker before QEMU (#32) --- .github/workflows/docker-hook-api.yml | 26 +++++++++++------------ .github/workflows/docker-hook-janitor.yml | 26 +++++++++++------------ .github/workflows/docker-hook-worker.yml | 26 +++++++++++------------ .github/workflows/docker-migrator.yml | 26 +++++++++++------------ 4 files changed, 52 insertions(+), 52 deletions(-) diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index abcd9f654a3a5..5ae94f5531036 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -17,6 +17,19 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -37,19 +50,6 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push api id: docker_build_hook_api uses: docker/build-push-action@v4 diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 1f4cae924ff72..12b9ddeab423a 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -18,6 +18,19 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -37,19 +50,6 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push janitor id: docker_build_hook_janitor uses: docker/build-push-action@v4 diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 25e4b3a92db8f..052629100ce5f 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -17,6 +17,19 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -37,19 +50,6 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push worker id: docker_build_hook_worker uses: docker/build-push-action@v4 diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index 1ad3893c8386a..d239da49afb2d 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -18,6 +18,19 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -37,19 +50,6 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push migrator id: docker_build_hook_migrator uses: docker/build-push-action@v4 From 9cd11327f899b8c12b6e67a4529110d9600dce22 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 10 Jan 2024 02:53:42 -0700 Subject: [PATCH 171/249] Login to DockerHub (#75) --- .github/workflows/docker.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1fdc6520bd004..a2180db21e4c0 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,6 +18,19 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -37,19 +50,6 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push id: docker_build uses: docker/build-push-action@v4 From 46f9f93bb78450a055504e1562934481595f1d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 10 Jan 2024 16:01:29 +0100 Subject: [PATCH 172/249] refactor: Rename producer for api and consumer for worker (#31) Co-authored-by: Brett Hoerner --- .github/workflows/docker-hook-api.yml | 2 +- .github/workflows/docker-hook-worker.yml | 2 +- Cargo.lock | 44 +++++++++---------- Cargo.toml | 2 +- {hook-producer => hook-api}/Cargo.toml | 2 +- {hook-producer => hook-api}/src/config.rs | 0 .../src/handlers/app.rs | 4 +- .../src/handlers/mod.rs | 0 .../src/handlers/webhook.rs | 2 +- {hook-producer => hook-api}/src/main.rs | 4 +- hook-common/src/webhook.rs | 4 +- hook-janitor/src/kafka_producer.rs | 5 +-- {hook-consumer => hook-worker}/Cargo.toml | 2 +- {hook-consumer => hook-worker}/README.md | 2 +- {hook-consumer => hook-worker}/src/config.rs | 4 +- {hook-consumer => hook-worker}/src/error.rs | 4 +- {hook-consumer => hook-worker}/src/lib.rs | 2 +- {hook-consumer => hook-worker}/src/main.rs | 14 +++--- .../consumer.rs => hook-worker/src/worker.rs | 42 +++++++++--------- 19 files changed, 70 insertions(+), 71 deletions(-) rename {hook-producer => hook-api}/Cargo.toml (96%) rename {hook-producer => hook-api}/src/config.rs (100%) rename {hook-producer => hook-api}/src/handlers/app.rs (93%) rename {hook-producer => hook-api}/src/handlers/mod.rs (100%) rename {hook-producer => hook-api}/src/handlers/webhook.rs (99%) rename {hook-producer => hook-api}/src/main.rs (88%) rename {hook-consumer => hook-worker}/Cargo.toml (95%) rename {hook-consumer => hook-worker}/README.md (67%) rename {hook-consumer => hook-worker}/src/config.rs (96%) rename {hook-consumer => hook-worker}/src/error.rs (96%) rename {hook-consumer => hook-worker}/src/lib.rs (63%) rename {hook-consumer => hook-worker}/src/main.rs (78%) rename hook-consumer/src/consumer.rs => hook-worker/src/worker.rs (93%) diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 5ae94f5531036..6331413e44839 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -63,7 +63,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: BIN=hook-producer + build-args: BIN=hook-api - name: Hook-api image digest run: echo ${{ steps.docker_build_hook_api.outputs.digest }} diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 052629100ce5f..1f06542c0a0d3 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -63,7 +63,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: BIN=hook-consumer + build-args: BIN=hook-worker - name: Hook-worker image digest run: echo ${{ steps.docker_build_hook_worker.outputs.digest }} diff --git a/Cargo.lock b/Cargo.lock index e4da8161adcc0..2bf16f1654ede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -755,43 +755,46 @@ dependencies = [ ] [[package]] -name = "hook-common" +name = "hook-api" version = "0.1.0" dependencies = [ - "async-trait", "axum", - "chrono", - "http 0.2.11", + "envconfig", + "eyre", + "hook-common", + "http-body-util", "metrics", "metrics-exporter-prometheus", - "regex", - "reqwest", "serde", "serde_derive", "serde_json", "sqlx", - "thiserror", "tokio", - "uuid", + "tower", + "tracing", + "tracing-subscriber", + "url", ] [[package]] -name = "hook-consumer" +name = "hook-common" version = "0.1.0" dependencies = [ + "async-trait", + "axum", "chrono", - "envconfig", - "futures", - "hook-common", "http 0.2.11", "metrics", + "metrics-exporter-prometheus", + "regex", "reqwest", "serde", "serde_derive", + "serde_json", "sqlx", "thiserror", "tokio", - "url", + "uuid", ] [[package]] @@ -821,24 +824,21 @@ dependencies = [ ] [[package]] -name = "hook-producer" +name = "hook-worker" version = "0.1.0" dependencies = [ - "axum", + "chrono", "envconfig", - "eyre", + "futures", "hook-common", - "http-body-util", + "http 0.2.11", "metrics", - "metrics-exporter-prometheus", + "reqwest", "serde", "serde_derive", - "serde_json", "sqlx", + "thiserror", "tokio", - "tower", - "tracing", - "tracing-subscriber", "url", ] diff --git a/Cargo.toml b/Cargo.toml index b7cb8fac622c1..0b48fb23b015b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" -members = ["hook-common", "hook-producer", "hook-consumer", "hook-janitor"] +members = ["hook-common", "hook-api", "hook-worker", "hook-janitor"] [workspace.dependencies] async-trait = "0.1.74" diff --git a/hook-producer/Cargo.toml b/hook-api/Cargo.toml similarity index 96% rename from hook-producer/Cargo.toml rename to hook-api/Cargo.toml index f4b116563fd12..96c897cd3ab1d 100644 --- a/hook-producer/Cargo.toml +++ b/hook-api/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hook-producer" +name = "hook-api" version = "0.1.0" edition = "2021" diff --git a/hook-producer/src/config.rs b/hook-api/src/config.rs similarity index 100% rename from hook-producer/src/config.rs rename to hook-api/src/config.rs diff --git a/hook-producer/src/handlers/app.rs b/hook-api/src/handlers/app.rs similarity index 93% rename from hook-producer/src/handlers/app.rs rename to hook-api/src/handlers/app.rs index 2cafc4a497ae2..73c29e1225842 100644 --- a/hook-producer/src/handlers/app.rs +++ b/hook-api/src/handlers/app.rs @@ -11,7 +11,7 @@ pub fn add_routes(router: Router, pg_pool: PgQueue) -> Router { } pub async fn index() -> &'static str { - "rusty-hook producer" + "rusty-hook api" } #[cfg(test)] @@ -42,6 +42,6 @@ mod tests { assert_eq!(response.status(), StatusCode::OK); let body = response.into_body().collect().await.unwrap().to_bytes(); - assert_eq!(&body[..], b"rusty-hook producer"); + assert_eq!(&body[..], b"rusty-hook api"); } } diff --git a/hook-producer/src/handlers/mod.rs b/hook-api/src/handlers/mod.rs similarity index 100% rename from hook-producer/src/handlers/mod.rs rename to hook-api/src/handlers/mod.rs diff --git a/hook-producer/src/handlers/webhook.rs b/hook-api/src/handlers/webhook.rs similarity index 99% rename from hook-producer/src/handlers/webhook.rs rename to hook-api/src/handlers/webhook.rs index 62a4aaa8833c8..16ebc6dc57179 100644 --- a/hook-producer/src/handlers/webhook.rs +++ b/hook-api/src/handlers/webhook.rs @@ -68,7 +68,7 @@ pub async fn post( pg_queue.enqueue(job).await.map_err(internal_error)?; let elapsed_time = start_time.elapsed().as_secs_f64(); - metrics::histogram!("webhook_producer_enqueue").record(elapsed_time); + metrics::histogram!("webhook_api_enqueue").record(elapsed_time); Ok(Json(WebhookPostResponse { error: None })) } diff --git a/hook-producer/src/main.rs b/hook-api/src/main.rs similarity index 88% rename from hook-producer/src/main.rs rename to hook-api/src/main.rs index f078f2fae7781..dc2739a859131 100644 --- a/hook-producer/src/main.rs +++ b/hook-api/src/main.rs @@ -24,7 +24,7 @@ async fn main() { let config = Config::init_from_env().expect("failed to load configuration from env"); let pg_queue = PgQueue::new( - // TODO: Coupling the queue name to the PgQueue object doesn't seem ideal from the producer + // TODO: Coupling the queue name to the PgQueue object doesn't seem ideal from the api // side, but we don't need more than one queue for now. &config.queue_name, &config.database_url, @@ -37,6 +37,6 @@ async fn main() { match listen(app, config.bind()).await { Ok(_) => {} - Err(e) => tracing::error!("failed to start hook-producer http server, {}", e), + Err(e) => tracing::error!("failed to start hook-api http server, {}", e), } } diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index bb1b5be04390a..475f3decb4b11 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -117,7 +117,7 @@ impl From<&HttpMethod> for http::Method { } } -/// `JobParameters` required for the `WebhookConsumer` to execute a webhook. +/// `JobParameters` required for the `WebhookWorker` to execute a webhook. /// These parameters should match the exported Webhook interface that PostHog plugins. /// implement. See: https://github.com/PostHog/plugin-scaffold/blob/main/src/types.ts#L15. #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] @@ -128,7 +128,7 @@ pub struct WebhookJobParameters { pub url: String, } -/// `JobMetadata` required for the `WebhookConsumer` to execute a webhook. +/// `JobMetadata` required for the `WebhookWorker` to execute a webhook. /// These should be set if the Webhook is associated with a plugin `composeWebhook` invocation. #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct WebhookJobMetadata { diff --git a/hook-janitor/src/kafka_producer.rs b/hook-janitor/src/kafka_producer.rs index 4845e9410df56..1d0144cb75436 100644 --- a/hook-janitor/src/kafka_producer.rs +++ b/hook-janitor/src/kafka_producer.rs @@ -38,8 +38,7 @@ pub async fn create_kafka_producer( }; debug!("rdkafka configuration: {:?}", client_config); - let producer: FutureProducer = - client_config.create_with_context(KafkaContext {})?; + let api: FutureProducer = client_config.create_with_context(KafkaContext {})?; - Ok(producer) + Ok(api) } diff --git a/hook-consumer/Cargo.toml b/hook-worker/Cargo.toml similarity index 95% rename from hook-consumer/Cargo.toml rename to hook-worker/Cargo.toml index fc8ee4a797fac..c84d348f9990b 100644 --- a/hook-consumer/Cargo.toml +++ b/hook-worker/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hook-consumer" +name = "hook-worker" version = "0.1.0" edition = "2021" diff --git a/hook-consumer/README.md b/hook-worker/README.md similarity index 67% rename from hook-consumer/README.md rename to hook-worker/README.md index 1adab6ea571f4..9b1884aab16b6 100644 --- a/hook-consumer/README.md +++ b/hook-worker/README.md @@ -1,2 +1,2 @@ -# hook-consumer +# hook-worker Consume and process webhook jobs diff --git a/hook-consumer/src/config.rs b/hook-worker/src/config.rs similarity index 96% rename from hook-consumer/src/config.rs rename to hook-worker/src/config.rs index 01f94e76bd428..6f16c893c5282 100644 --- a/hook-consumer/src/config.rs +++ b/hook-worker/src/config.rs @@ -14,8 +14,8 @@ pub struct Config { #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] pub database_url: String, - #[envconfig(default = "consumer")] - pub consumer_name: String, + #[envconfig(default = "worker")] + pub worker_name: String, #[envconfig(default = "default")] pub queue_name: String, diff --git a/hook-consumer/src/error.rs b/hook-worker/src/error.rs similarity index 96% rename from hook-consumer/src/error.rs rename to hook-worker/src/error.rs index b05d476e22849..d0259985c6f6a 100644 --- a/hook-consumer/src/error.rs +++ b/hook-worker/src/error.rs @@ -3,7 +3,7 @@ use std::time; use hook_common::pgqueue; use thiserror::Error; -/// Enumeration of errors related to webhook job processing in the WebhookConsumer. +/// Enumeration of errors related to webhook job processing in the WebhookWorker. #[derive(Error, Debug)] pub enum WebhookError { #[error("{0} is not a valid HttpMethod")] @@ -23,7 +23,7 @@ pub enum WebhookError { /// Enumeration of errors related to initialization and consumption of webhook jobs. #[derive(Error, Debug)] -pub enum ConsumerError { +pub enum WorkerError { #[error("timed out while waiting for jobs to be available")] TimeoutError, #[error("an error occurred in the underlying queue")] diff --git a/hook-consumer/src/lib.rs b/hook-worker/src/lib.rs similarity index 63% rename from hook-consumer/src/lib.rs rename to hook-worker/src/lib.rs index b99481bbc3b58..22823c9a7e5cd 100644 --- a/hook-consumer/src/lib.rs +++ b/hook-worker/src/lib.rs @@ -1,3 +1,3 @@ pub mod config; -pub mod consumer; pub mod error; +pub mod worker; diff --git a/hook-consumer/src/main.rs b/hook-worker/src/main.rs similarity index 78% rename from hook-consumer/src/main.rs rename to hook-worker/src/main.rs index 4182348121bd6..10b34d7b8eb5f 100644 --- a/hook-consumer/src/main.rs +++ b/hook-worker/src/main.rs @@ -4,12 +4,12 @@ use envconfig::Envconfig; use hook_common::{ metrics::serve, metrics::setup_metrics_router, pgqueue::PgQueue, retry::RetryPolicy, }; -use hook_consumer::config::Config; -use hook_consumer::consumer::WebhookConsumer; -use hook_consumer::error::ConsumerError; +use hook_worker::config::Config; +use hook_worker::error::WorkerError; +use hook_worker::worker::WebhookWorker; #[tokio::main] -async fn main() -> Result<(), ConsumerError> { +async fn main() -> Result<(), WorkerError> { let config = Config::init_from_env().expect("Invalid configuration:"); let retry_policy = RetryPolicy::build( @@ -23,8 +23,8 @@ async fn main() -> Result<(), ConsumerError> { .await .expect("failed to initialize queue"); - let consumer = WebhookConsumer::new( - &config.consumer_name, + let worker = WebhookWorker::new( + &config.worker_name, &queue, config.poll_interval.0, config.request_timeout.0, @@ -40,7 +40,7 @@ async fn main() -> Result<(), ConsumerError> { .expect("failed to start serving metrics"); }); - consumer.run(config.transactional).await?; + worker.run(config.transactional).await?; Ok(()) } diff --git a/hook-consumer/src/consumer.rs b/hook-worker/src/worker.rs similarity index 93% rename from hook-consumer/src/consumer.rs rename to hook-worker/src/worker.rs index 2114ef80c8bc8..02ab7e962b71e 100644 --- a/hook-consumer/src/consumer.rs +++ b/hook-worker/src/worker.rs @@ -11,7 +11,7 @@ use http::StatusCode; use reqwest::header; use tokio::sync; -use crate::error::{ConsumerError, WebhookError}; +use crate::error::{WebhookError, WorkerError}; /// A WebhookJob is any `PgQueueJob` with `WebhookJobParameters` and `WebhookJobMetadata`. trait WebhookJob: PgQueueJob + std::marker::Send { @@ -60,9 +60,9 @@ impl WebhookJob for PgJob { } } -/// A consumer to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. -pub struct WebhookConsumer<'p> { - /// An identifier for this consumer. Used to mark jobs we have consumed. +/// A worker to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. +pub struct WebhookWorker<'p> { + /// An identifier for this worker. Used to mark jobs we have consumed. name: String, /// The queue we will be dequeuing jobs from. queue: &'p PgQueue, @@ -76,7 +76,7 @@ pub struct WebhookConsumer<'p> { retry_policy: RetryPolicy, } -impl<'p> WebhookConsumer<'p> { +impl<'p> WebhookWorker<'p> { pub fn new( name: &str, queue: &'p PgQueue, @@ -95,7 +95,7 @@ impl<'p> WebhookConsumer<'p> { .default_headers(headers) .timeout(request_timeout) .build() - .expect("failed to construct reqwest client for webhook consumer"); + .expect("failed to construct reqwest client for webhook worker"); Self { name: name.to_owned(), @@ -110,7 +110,7 @@ impl<'p> WebhookConsumer<'p> { /// Wait until a job becomes available in our queue. async fn wait_for_job<'a>( &self, - ) -> Result, ConsumerError> { + ) -> Result, WorkerError> { let mut interval = tokio::time::interval(self.poll_interval); loop { @@ -125,7 +125,7 @@ impl<'p> WebhookConsumer<'p> { /// Wait until a job becomes available in our queue in transactional mode. async fn wait_for_job_tx<'a>( &self, - ) -> Result, ConsumerError> { + ) -> Result, WorkerError> { let mut interval = tokio::time::interval(self.poll_interval); loop { @@ -137,8 +137,8 @@ impl<'p> WebhookConsumer<'p> { } } - /// Run this consumer to continuously process any jobs that become available. - pub async fn run(&self, transactional: bool) -> Result<(), ConsumerError> { + /// Run this worker to continuously process any jobs that become available. + pub async fn run(&self, transactional: bool) -> Result<(), WorkerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); if transactional { @@ -181,7 +181,7 @@ async fn spawn_webhook_job_processing_task( semaphore: Arc, retry_policy: RetryPolicy, webhook_job: W, -) -> tokio::task::JoinHandle> { +) -> tokio::task::JoinHandle> { let permit = semaphore .acquire_owned() .await @@ -219,7 +219,7 @@ async fn process_webhook_job( client: reqwest::Client, webhook_job: W, retry_policy: &RetryPolicy, -) -> Result<(), ConsumerError> { +) -> Result<(), WorkerError> { let parameters = webhook_job.parameters(); let labels = [ @@ -245,7 +245,7 @@ async fn process_webhook_job( webhook_job .complete() .await - .map_err(|error| ConsumerError::PgJobError(error.to_string()))?; + .map_err(|error| WorkerError::PgJobError(error.to_string()))?; metrics::counter!("webhook_jobs_completed", &labels).increment(1); metrics::histogram!("webhook_jobs_processing_duration_seconds", &labels) @@ -257,7 +257,7 @@ async fn process_webhook_job( webhook_job .fail(WebhookJobError::new_parse(&e.to_string())) .await - .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -267,7 +267,7 @@ async fn process_webhook_job( webhook_job .fail(WebhookJobError::new_parse(&e)) .await - .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -277,7 +277,7 @@ async fn process_webhook_job( webhook_job .fail(WebhookJobError::new_parse(&e.to_string())) .await - .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -304,20 +304,20 @@ async fn process_webhook_job( webhook_job .fail(WebhookJobError::from(&error)) .await - .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); Ok(()) } - Err(job_error) => Err(ConsumerError::PgJobError(job_error.to_string())), + Err(job_error) => Err(WorkerError::PgJobError(job_error.to_string())), } } Err(WebhookError::NonRetryableRetryableRequestError(error)) => { webhook_job .fail(WebhookJobError::from(&error)) .await - .map_err(|job_error| ConsumerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -512,7 +512,7 @@ mod tests { ) .await .expect("failed to enqueue job"); - let consumer = WebhookConsumer::new( + let worker = WebhookWorker::new( &worker_id, &queue, time::Duration::from_millis(100), @@ -521,7 +521,7 @@ mod tests { RetryPolicy::default(), ); - let consumed_job = consumer + let consumed_job = worker .wait_for_job() .await .expect("failed to wait and read job"); From d6cc49f4d290a8125a3dc390c3664511c0d68694 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 10 Jan 2024 08:53:09 -0700 Subject: [PATCH 173/249] Sync default ports with charts (#33) --- hook-api/src/config.rs | 2 +- hook-janitor/src/config.rs | 2 +- hook-worker/src/config.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hook-api/src/config.rs b/hook-api/src/config.rs index 8daf04e4ca8b8..3fe88b3e436c0 100644 --- a/hook-api/src/config.rs +++ b/hook-api/src/config.rs @@ -5,7 +5,7 @@ pub struct Config { #[envconfig(from = "BIND_HOST", default = "0.0.0.0")] pub host: String, - #[envconfig(from = "BIND_PORT", default = "8000")] + #[envconfig(from = "BIND_PORT", default = "3300")] pub port: u16, #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] diff --git a/hook-janitor/src/config.rs b/hook-janitor/src/config.rs index 64db0e613f355..852b7cfed14c1 100644 --- a/hook-janitor/src/config.rs +++ b/hook-janitor/src/config.rs @@ -5,7 +5,7 @@ pub struct Config { #[envconfig(from = "BIND_HOST", default = "0.0.0.0")] pub host: String, - #[envconfig(from = "BIND_PORT", default = "8000")] + #[envconfig(from = "BIND_PORT", default = "3302")] pub port: u16, #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] diff --git a/hook-worker/src/config.rs b/hook-worker/src/config.rs index 6f16c893c5282..8b2b4ba49f205 100644 --- a/hook-worker/src/config.rs +++ b/hook-worker/src/config.rs @@ -8,7 +8,7 @@ pub struct Config { #[envconfig(from = "BIND_HOST", default = "0.0.0.0")] pub host: String, - #[envconfig(from = "BIND_PORT", default = "8001")] + #[envconfig(from = "BIND_PORT", default = "3301")] pub port: u16, #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] From 878f11201df2e5777967229f176e971c39d00a9e Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 10 Jan 2024 09:39:01 -0700 Subject: [PATCH 174/249] Log errors in worker (#34) --- Cargo.lock | 2 ++ hook-worker/Cargo.toml | 2 ++ hook-worker/src/main.rs | 2 ++ hook-worker/src/worker.rs | 9 ++++++++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2bf16f1654ede..d242eca5b3925 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -839,6 +839,8 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "tracing", + "tracing-subscriber", "url", ] diff --git a/hook-worker/Cargo.toml b/hook-worker/Cargo.toml index c84d348f9990b..f69489877b4ec 100644 --- a/hook-worker/Cargo.toml +++ b/hook-worker/Cargo.toml @@ -15,5 +15,7 @@ serde = { workspace = true } serde_derive = { workspace = true } sqlx = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } tokio = { workspace = true } url = { version = "2.2" } diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index 10b34d7b8eb5f..fbd4746fd000d 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -10,6 +10,8 @@ use hook_worker::worker::WebhookWorker; #[tokio::main] async fn main() -> Result<(), WorkerError> { + tracing_subscriber::fmt::init(); + let config = Config::init_from_env().expect("Invalid configuration:"); let retry_policy = RetryPolicy::build( diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 02ab7e962b71e..cc5082e35b5ea 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -10,6 +10,7 @@ use hook_common::{ use http::StatusCode; use reqwest::header; use tokio::sync; +use tracing::error; use crate::error::{WebhookError, WorkerError}; @@ -197,7 +198,13 @@ async fn spawn_webhook_job_processing_task( tokio::spawn(async move { let result = process_webhook_job(client, webhook_job, &retry_policy).await; drop(permit); - result + match result { + Ok(_) => Ok(()), + Err(error) => { + error!("failed to process webhook job: {}", error); + Err(error) + } + } }) } From b0966788b0b959f6d286bc9b91d48d23159e925f Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 10 Jan 2024 09:58:01 -0700 Subject: [PATCH 175/249] Log PgJobError String (#35) --- hook-worker/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-worker/src/error.rs b/hook-worker/src/error.rs index d0259985c6f6a..614fe721957e4 100644 --- a/hook-worker/src/error.rs +++ b/hook-worker/src/error.rs @@ -28,6 +28,6 @@ pub enum WorkerError { TimeoutError, #[error("an error occurred in the underlying queue")] QueueError(#[from] pgqueue::PgQueueError), - #[error("an error occurred in the underlying job")] + #[error("an error occurred in the underlying job: {0}")] PgJobError(String), } From 35877d3d9d94738197312525ffc4290d2bb56542 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 10 Jan 2024 10:27:52 -0700 Subject: [PATCH 176/249] Install ca-certificates (#37) --- Dockerfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Dockerfile b/Dockerfile index 959fd17180fb5..74682f6855330 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,13 @@ COPY . . RUN cargo build --release --bin $BIN FROM debian:bullseye-20230320-slim AS runtime + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + "ca-certificates" \ + && \ + rm -rf /var/lib/apt/lists/* + ARG BIN ENV ENTRYPOINT=/usr/local/bin/$BIN WORKDIR app From 5011f18a44e7ea69f8293152367c0d82e8f92523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 10 Jan 2024 18:34:05 +0100 Subject: [PATCH 177/249] fix: Syntax error in fail method (#38) --- hook-common/src/pgqueue.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 35f5b4a9e47fb..d3a409bdbd889 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -172,7 +172,7 @@ UPDATE job_queue SET last_attempt_finished_at = NOW(), - status = 'failed'::job_status + status = 'failed'::job_status, errors = array_append(errors, $3) WHERE queue = $1 From 9b7d313433876ec685b0d6dc1136252ff2af7c18 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 11 Jan 2024 09:42:10 -0700 Subject: [PATCH 178/249] Log productive janitor runs at info level (#40) --- hook-janitor/src/webhooks.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index de02d07a2520e..6f28a153f70a6 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -11,7 +11,7 @@ use sqlx::postgres::{PgPool, PgPoolOptions, Postgres}; use sqlx::types::{chrono, Uuid}; use sqlx::{Row, Transaction}; use thiserror::Error; -use tracing::{debug, error}; +use tracing::{debug, error, info}; use crate::cleanup::Cleaner; use crate::kafka_producer::KafkaContext; @@ -432,7 +432,7 @@ impl Cleaner for WebhookCleaner { metrics::counter!("webhook_cleanup_failed_agg_row_count",) .increment(stats.failed_agg_row_count); - debug!( + info!( rows_processed = stats.rows_processed, completed_row_count = stats.completed_row_count, completed_agg_row_count = stats.completed_agg_row_count, From 677a094f64908ddced814f1a569d2ba1bdaa70e3 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 11 Jan 2024 09:45:47 -0700 Subject: [PATCH 179/249] Drop target from Prom labels (#41) --- hook-worker/src/worker.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index cc5082e35b5ea..8d8c824d7f82b 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -188,10 +188,7 @@ async fn spawn_webhook_job_processing_task( .await .expect("semaphore has been closed"); - let labels = [ - ("queue", webhook_job.queue()), - ("target", webhook_job.target()), - ]; + let labels = [("queue", webhook_job.queue())]; metrics::counter!("webhook_jobs_total", &labels).increment(1); @@ -229,10 +226,7 @@ async fn process_webhook_job( ) -> Result<(), WorkerError> { let parameters = webhook_job.parameters(); - let labels = [ - ("queue", webhook_job.queue()), - ("target", webhook_job.target()), - ]; + let labels = [("queue", webhook_job.queue())]; let now = tokio::time::Instant::now(); From 2437bd55567a43adbd05397919368334f322cf58 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 11 Jan 2024 09:55:10 -0700 Subject: [PATCH 180/249] Add user-agent (#42) --- hook-worker/src/worker.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 8d8c824d7f82b..77c6adb4b13f8 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -94,6 +94,7 @@ impl<'p> WebhookWorker<'p> { let client = reqwest::Client::builder() .default_headers(headers) + .user_agent("PostHog Webhook Worker") .timeout(request_timeout) .build() .expect("failed to construct reqwest client for webhook worker"); From d5dd35d9017bf0f01f65ed0ea38dd54a6faca47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Thu, 11 Jan 2024 17:55:28 +0100 Subject: [PATCH 181/249] feat: Set idle tx timeout in migration (#39) --- migrations/20240110180056_set_idle_in_transaction_timeout.sql | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 migrations/20240110180056_set_idle_in_transaction_timeout.sql diff --git a/migrations/20240110180056_set_idle_in_transaction_timeout.sql b/migrations/20240110180056_set_idle_in_transaction_timeout.sql new file mode 100644 index 0000000000000..e17113125e41b --- /dev/null +++ b/migrations/20240110180056_set_idle_in_transaction_timeout.sql @@ -0,0 +1,2 @@ +-- If running worker in transactional mode, this ensures we clean up any open transactions. +ALTER USER current_user SET idle_in_transaction_session_timeout = '2min'; From 1982b0d8d9b0a45ddc3438c17fab369ad1acae37 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 11 Jan 2024 10:15:58 -0700 Subject: [PATCH 182/249] Fix app_metrics topic, don't encode null error_uuid (#43) --- hook-common/src/kafka_messages/app_metrics.rs | 1 + hook-janitor/src/config.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 9acc4112e1113..13f4f2e5000d4 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -62,6 +62,7 @@ pub struct AppMetric { pub successes: u32, pub successes_on_retry: u32, pub failures: u32, + #[serde(skip_serializing_if = "Option::is_none")] pub error_uuid: Option, #[serde( serialize_with = "serialize_error_type", diff --git a/hook-janitor/src/config.rs b/hook-janitor/src/config.rs index 852b7cfed14c1..252a67011460c 100644 --- a/hook-janitor/src/config.rs +++ b/hook-janitor/src/config.rs @@ -44,7 +44,7 @@ pub struct KafkaConfig { #[envconfig(default = "false")] pub kafka_tls: bool, - #[envconfig(default = "app_metrics")] + #[envconfig(default = "clickhouse_app_metrics")] pub app_metrics_topic: String, #[envconfig(default = "plugin_log_entries")] From 51a5c5f0211291075489b36143c3184a1e16d6a1 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 12 Jan 2024 04:03:30 -0700 Subject: [PATCH 183/249] Remove idle_in_transaction_session_timeout migration (#44) --- migrations/20240110180056_set_idle_in_transaction_timeout.sql | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 migrations/20240110180056_set_idle_in_transaction_timeout.sql diff --git a/migrations/20240110180056_set_idle_in_transaction_timeout.sql b/migrations/20240110180056_set_idle_in_transaction_timeout.sql deleted file mode 100644 index e17113125e41b..0000000000000 --- a/migrations/20240110180056_set_idle_in_transaction_timeout.sql +++ /dev/null @@ -1,2 +0,0 @@ --- If running worker in transactional mode, this ensures we clean up any open transactions. -ALTER USER current_user SET idle_in_transaction_session_timeout = '2min'; From 8900f50655a998d859d6d4560d87a00b67e482dd Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 12 Jan 2024 09:14:47 -0700 Subject: [PATCH 184/249] Use signed integers for plugin_id/plugin_config_id (#45) --- hook-common/src/kafka_messages/app_metrics.rs | 2 +- hook-common/src/kafka_messages/plugin_logs.rs | 4 ++-- hook-common/src/pgqueue.rs | 4 ++-- hook-common/src/webhook.rs | 4 ++-- hook-janitor/src/webhooks.rs | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/hook-common/src/kafka_messages/app_metrics.rs b/hook-common/src/kafka_messages/app_metrics.rs index 13f4f2e5000d4..f941f58138b76 100644 --- a/hook-common/src/kafka_messages/app_metrics.rs +++ b/hook-common/src/kafka_messages/app_metrics.rs @@ -51,7 +51,7 @@ pub struct AppMetric { )] pub timestamp: DateTime, pub team_id: u32, - pub plugin_config_id: u32, + pub plugin_config_id: i32, #[serde(skip_serializing_if = "Option::is_none")] pub job_id: Option, #[serde( diff --git a/hook-common/src/kafka_messages/plugin_logs.rs b/hook-common/src/kafka_messages/plugin_logs.rs index e761fa40799ea..fb835804c687c 100644 --- a/hook-common/src/kafka_messages/plugin_logs.rs +++ b/hook-common/src/kafka_messages/plugin_logs.rs @@ -30,8 +30,8 @@ pub struct PluginLogEntry { pub type_: PluginLogEntryType, pub id: Uuid, pub team_id: u32, - pub plugin_id: u32, - pub plugin_config_id: u32, + pub plugin_id: i32, + pub plugin_config_id: i32, #[serde(serialize_with = "serialize_datetime")] pub timestamp: DateTime, #[serde(serialize_with = "serialize_message")] diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index d3a409bdbd889..fa2b5eb0c38be 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -731,8 +731,8 @@ mod tests { #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] struct JobMetadata { team_id: u32, - plugin_config_id: u32, - plugin_id: u32, + plugin_config_id: i32, + plugin_id: i32, } impl Default for JobMetadata { diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 475f3decb4b11..11e02856703eb 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -133,8 +133,8 @@ pub struct WebhookJobParameters { #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub struct WebhookJobMetadata { pub team_id: u32, - pub plugin_id: u32, - pub plugin_config_id: u32, + pub plugin_id: i32, + pub plugin_config_id: i32, } /// An error originating during a Webhook Job invocation. diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 6f28a153f70a6..0c2941f99650f 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -66,7 +66,7 @@ struct CompletedRow { #[sqlx(try_from = "i64")] team_id: u32, #[sqlx(try_from = "i64")] - plugin_config_id: u32, + plugin_config_id: i32, #[sqlx(try_from = "i64")] successes: u32, } @@ -100,7 +100,7 @@ struct FailedRow { #[sqlx(try_from = "i64")] team_id: u32, #[sqlx(try_from = "i64")] - plugin_config_id: u32, + plugin_config_id: i32, #[sqlx(json)] last_error: WebhookJobError, #[sqlx(try_from = "i64")] From 58d573a2b40db67344b0952aa4811a4d000aba33 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 15 Jan 2024 17:35:59 +0100 Subject: [PATCH 185/249] feat: make sure metrics cover all axum endpoints (#46) --- Cargo.lock | 1 + hook-api/src/main.rs | 6 +++--- hook-common/src/metrics.rs | 6 +++--- hook-janitor/src/handlers/app.rs | 16 ++-------------- hook-janitor/src/main.rs | 5 ++--- hook-worker/Cargo.toml | 1 + hook-worker/src/main.rs | 5 +++-- 7 files changed, 15 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d242eca5b3925..5157ea14869fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,6 +827,7 @@ dependencies = [ name = "hook-worker" version = "0.1.0" dependencies = [ + "axum", "chrono", "envconfig", "futures", diff --git a/hook-api/src/main.rs b/hook-api/src/main.rs index dc2739a859131..4fbbdfbb7d4ed 100644 --- a/hook-api/src/main.rs +++ b/hook-api/src/main.rs @@ -3,7 +3,7 @@ use config::Config; use envconfig::Envconfig; use eyre::Result; -use hook_common::metrics::setup_metrics_router; +use hook_common::metrics::setup_metrics_routes; use hook_common::pgqueue::PgQueue; mod config; @@ -32,8 +32,8 @@ async fn main() { .await .expect("failed to initialize queue"); - let router = setup_metrics_router(); - let app = handlers::add_routes(router, pg_queue); + let app = handlers::add_routes(Router::new(), pg_queue); + let app = setup_metrics_routes(app); match listen(app, config.bind()).await { Ok(_) => {} diff --git a/hook-common/src/metrics.rs b/hook-common/src/metrics.rs index 1f57c5eec51f9..0e1ef2db72a33 100644 --- a/hook-common/src/metrics.rs +++ b/hook-common/src/metrics.rs @@ -16,11 +16,11 @@ pub async fn serve(router: Router, bind: &str) -> Result<(), std::io::Error> { Ok(()) } -/// Build a Router for a metrics endpoint. -pub fn setup_metrics_router() -> Router { +/// Add the prometheus endpoint and middleware to a router, should be called last. +pub fn setup_metrics_routes(router: Router) -> Router { let recorder_handle = setup_metrics_recorder(); - Router::new() + router .route( "/metrics", get(move || std::future::ready(recorder_handle.render())), diff --git a/hook-janitor/src/handlers/app.rs b/hook-janitor/src/handlers/app.rs index 279fa0e35923e..cab0e0d27991a 100644 --- a/hook-janitor/src/handlers/app.rs +++ b/hook-janitor/src/handlers/app.rs @@ -1,19 +1,7 @@ use axum::{routing, Router}; -use metrics_exporter_prometheus::PrometheusHandle; -use hook_common::metrics; - -pub fn app(metrics: Option) -> Router { - Router::new() - .route("/", routing::get(index)) - .route( - "/metrics", - routing::get(move || match metrics { - Some(ref recorder_handle) => std::future::ready(recorder_handle.render()), - None => std::future::ready("no metrics recorder installed".to_owned()), - }), - ) - .layer(axum::middleware::from_fn(metrics::track_metrics)) +pub fn app() -> Router { + Router::new().route("/", routing::get(index)) } pub async fn index() -> &'static str { diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs index 7d7e2230e548e..63068c35262bf 100644 --- a/hook-janitor/src/main.rs +++ b/hook-janitor/src/main.rs @@ -9,7 +9,7 @@ use std::{str::FromStr, time::Duration}; use tokio::sync::Semaphore; use webhooks::WebhookCleaner; -use hook_common::metrics; +use hook_common::metrics::setup_metrics_routes; mod cleanup; mod config; @@ -66,8 +66,7 @@ async fn main() { let cleanup_loop = Box::pin(cleanup_loop(cleaner, config.cleanup_interval_secs)); - let recorder_handle = metrics::setup_metrics_recorder(); - let app = handlers::app(Some(recorder_handle)); + let app = setup_metrics_routes(handlers::app()); let http_server = Box::pin(listen(app, config.bind())); match select(http_server, cleanup_loop).await { diff --git a/hook-worker/Cargo.toml b/hook-worker/Cargo.toml index f69489877b4ec..11da0a80a564c 100644 --- a/hook-worker/Cargo.toml +++ b/hook-worker/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +axum = { workspace = true } chrono = { workspace = true } envconfig = { workspace = true } futures = "0.3" diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index fbd4746fd000d..cc17169b2bd83 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -1,8 +1,9 @@ //! Consume `PgQueue` jobs to run webhook calls. +use axum::Router; use envconfig::Envconfig; use hook_common::{ - metrics::serve, metrics::setup_metrics_router, pgqueue::PgQueue, retry::RetryPolicy, + metrics::serve, metrics::setup_metrics_routes, pgqueue::PgQueue, retry::RetryPolicy, }; use hook_worker::config::Config; use hook_worker::error::WorkerError; @@ -36,7 +37,7 @@ async fn main() -> Result<(), WorkerError> { let bind = config.bind(); tokio::task::spawn(async move { - let router = setup_metrics_router(); + let router = setup_metrics_routes(Router::new()); serve(router, &bind) .await .expect("failed to start serving metrics"); From 9fa90fdf2358819c6a8e0f7e8a39f2c29c606eb6 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 16 Jan 2024 17:51:40 +0100 Subject: [PATCH 186/249] improve janitor metrics (#48) --- hook-janitor/src/webhooks.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 0c2941f99650f..4248a47281e52 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -412,15 +412,15 @@ impl WebhookCleaner { impl Cleaner for WebhookCleaner { async fn cleanup(&self) { let start_time = Instant::now(); + metrics::counter!("webhook_cleanup_attempts",).increment(1); match self.cleanup_impl().await { Ok(stats) => { - metrics::counter!("webhook_cleanup_runs",).increment(1); + metrics::counter!("webhook_cleanup_success",).increment(1); if stats.rows_processed > 0 { let elapsed_time = start_time.elapsed().as_secs_f64(); metrics::histogram!("webhook_cleanup_duration").record(elapsed_time); - metrics::counter!("webhook_cleanup_rows_processed",) .increment(stats.rows_processed); metrics::counter!("webhook_cleanup_completed_row_count",) From b5029a95b617ad6149514941321824a70f8295c6 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 16 Jan 2024 18:50:43 +0100 Subject: [PATCH 187/249] feat: add readiness and liveness endpoints to all roles (#47) --- Cargo.lock | 37 +++ Cargo.toml | 1 + hook-api/src/handlers/app.rs | 2 + hook-common/Cargo.toml | 2 + hook-common/src/health.rs | 346 +++++++++++++++++++++++++++++ hook-common/src/lib.rs | 1 + hook-janitor/Cargo.toml | 1 + hook-janitor/src/handlers/app.rs | 11 +- hook-janitor/src/kafka_producer.rs | 21 +- hook-janitor/src/main.rs | 25 ++- hook-janitor/src/webhooks.rs | 7 +- hook-worker/Cargo.toml | 1 + hook-worker/src/main.rs | 19 +- hook-worker/src/worker.rs | 17 +- 14 files changed, 477 insertions(+), 14 deletions(-) create mode 100644 hook-common/src/health.rs diff --git a/Cargo.lock b/Cargo.lock index 5157ea14869fa..17b608c879f05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -355,6 +355,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.10.7" @@ -793,7 +802,9 @@ dependencies = [ "serde_json", "sqlx", "thiserror", + "time", "tokio", + "tracing", "uuid", ] @@ -816,6 +827,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", + "time", "tokio", "tower", "tracing", @@ -839,6 +851,7 @@ dependencies = [ "serde_derive", "sqlx", "thiserror", + "time", "tokio", "tracing", "tracing-subscriber", @@ -1542,6 +1555,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2313,6 +2332,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "tinyvec" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 0b48fb23b015b..b4005931a5bed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ sqlx = { version = "0.7", features = [ "tls-native-tls", "uuid", ] } +time = { version = "0.3.20" } thiserror = { version = "1.0" } tokio = { version = "1.34.0", features = ["full"] } tower = "0.4.13" diff --git a/hook-api/src/handlers/app.rs b/hook-api/src/handlers/app.rs index 73c29e1225842..7b1e840094473 100644 --- a/hook-api/src/handlers/app.rs +++ b/hook-api/src/handlers/app.rs @@ -7,6 +7,8 @@ use super::webhook; pub fn add_routes(router: Router, pg_pool: PgQueue) -> Router { router .route("/", routing::get(index)) + .route("/_readiness", routing::get(index)) + .route("/_liveness", routing::get(index)) // No async loop for now, just check axum health .route("/webhook", routing::post(webhook::post).with_state(pg_pool)) } diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 00c7bd2296a49..ea7ce2fbb9cac 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -18,8 +18,10 @@ serde = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } +time = { workspace = true } tokio = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } uuid = { workspace = true } [dev-dependencies] diff --git a/hook-common/src/health.rs b/hook-common/src/health.rs new file mode 100644 index 0000000000000..c5c79c904c950 --- /dev/null +++ b/hook-common/src/health.rs @@ -0,0 +1,346 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use std::collections::HashMap; +use std::ops::Add; +use std::sync::{Arc, RwLock}; + +use time::Duration; +use tokio::sync::mpsc; +use tracing::{info, warn}; + +/// Health reporting for components of the service. +/// +/// FIXME: copied over from capture, make sure to keep in sync until we share the crate +/// +/// The capture server contains several asynchronous loops, and +/// the process can only be trusted with user data if all the +/// loops are properly running and reporting. +/// +/// HealthRegistry allows an arbitrary number of components to +/// be registered and report their health. The process' health +/// status is the combination of these individual health status: +/// - if any component is unhealthy, the process is unhealthy +/// - if all components recently reported healthy, the process is healthy +/// - if a component failed to report healthy for its defined deadline, +/// it is considered unhealthy, and the check fails. +/// +/// Trying to merge the k8s concepts of liveness and readiness in +/// a single state is full of foot-guns, so HealthRegistry does not +/// try to do it. Each probe should have its separate instance of +/// the registry to avoid confusions. + +#[derive(Default, Debug)] +pub struct HealthStatus { + /// The overall status: true of all components are healthy + pub healthy: bool, + /// Current status of each registered component, for display + pub components: HashMap, +} +impl IntoResponse for HealthStatus { + /// Computes the axum status code based on the overall health status, + /// and prints each component status in the body for debugging. + fn into_response(self) -> Response { + let body = format!("{:?}", self); + match self.healthy { + true => (StatusCode::OK, body), + false => (StatusCode::INTERNAL_SERVER_ERROR, body), + } + .into_response() + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum ComponentStatus { + /// Automatically set when a component is newly registered + Starting, + /// Recently reported healthy, will need to report again before the date + HealthyUntil(time::OffsetDateTime), + /// Reported unhealthy + Unhealthy, + /// Automatically set when the HealthyUntil deadline is reached + Stalled, +} +struct HealthMessage { + component: String, + status: ComponentStatus, +} + +pub struct HealthHandle { + component: String, + deadline: Duration, + sender: mpsc::Sender, +} + +impl HealthHandle { + /// Asynchronously report healthy, returns when the message is queued. + /// Must be called more frequently than the configured deadline. + pub async fn report_healthy(&self) { + self.report_status(ComponentStatus::HealthyUntil( + time::OffsetDateTime::now_utc().add(self.deadline), + )) + .await + } + + /// Asynchronously report component status, returns when the message is queued. + pub async fn report_status(&self, status: ComponentStatus) { + let message = HealthMessage { + component: self.component.clone(), + status, + }; + if let Err(err) = self.sender.send(message).await { + warn!("failed to report heath status: {}", err) + } + } + + /// Synchronously report as healthy, returns when the message is queued. + /// Must be called more frequently than the configured deadline. + pub fn report_healthy_blocking(&self) { + self.report_status_blocking(ComponentStatus::HealthyUntil( + time::OffsetDateTime::now_utc().add(self.deadline), + )) + } + + /// Asynchronously report component status, returns when the message is queued. + pub fn report_status_blocking(&self, status: ComponentStatus) { + let message = HealthMessage { + component: self.component.clone(), + status, + }; + if let Err(err) = self.sender.blocking_send(message) { + warn!("failed to report heath status: {}", err) + } + } +} + +#[derive(Clone)] +pub struct HealthRegistry { + name: String, + components: Arc>>, + sender: mpsc::Sender, +} + +impl HealthRegistry { + pub fn new(name: &str) -> Self { + let (tx, mut rx) = mpsc::channel::(16); + let registry = Self { + name: name.to_owned(), + components: Default::default(), + sender: tx, + }; + + let components = registry.components.clone(); + tokio::spawn(async move { + while let Some(message) = rx.recv().await { + if let Ok(mut map) = components.write() { + _ = map.insert(message.component, message.status); + } else { + // Poisoned mutex: Just warn, the probes will fail and the process restart + warn!("poisoned HeathRegistry mutex") + } + } + }); + + registry + } + + /// Registers a new component in the registry. The returned handle should be passed + /// to the component, to allow it to frequently report its health status. + pub async fn register(&self, component: String, deadline: time::Duration) -> HealthHandle { + let handle = HealthHandle { + component, + deadline, + sender: self.sender.clone(), + }; + handle.report_status(ComponentStatus::Starting).await; + handle + } + + /// Returns the overall process status, computed from the status of all the components + /// currently registered. Can be used as an axum handler. + pub fn get_status(&self) -> HealthStatus { + let components = self + .components + .read() + .expect("poisoned HeathRegistry mutex"); + + let result = HealthStatus { + healthy: !components.is_empty(), // unhealthy if no component has registered yet + components: Default::default(), + }; + let now = time::OffsetDateTime::now_utc(); + + let result = components + .iter() + .fold(result, |mut result, (name, status)| { + match status { + ComponentStatus::HealthyUntil(until) => { + if until.gt(&now) { + _ = result.components.insert(name.clone(), status.clone()) + } else { + result.healthy = false; + _ = result + .components + .insert(name.clone(), ComponentStatus::Stalled) + } + } + _ => { + result.healthy = false; + _ = result.components.insert(name.clone(), status.clone()) + } + } + result + }); + match result.healthy { + true => info!("{} health check ok", self.name), + false => warn!("{} health check failed: {:?}", self.name, result.components), + } + result + } +} + +#[cfg(test)] +mod tests { + use crate::health::{ComponentStatus, HealthRegistry, HealthStatus}; + use axum::http::StatusCode; + use axum::response::IntoResponse; + use std::ops::{Add, Sub}; + use time::{Duration, OffsetDateTime}; + + async fn assert_or_retry(check: F) + where + F: Fn() -> bool, + { + assert_or_retry_for_duration(check, Duration::seconds(5)).await + } + + async fn assert_or_retry_for_duration(check: F, timeout: Duration) + where + F: Fn() -> bool, + { + let deadline = OffsetDateTime::now_utc().add(timeout); + while !check() && OffsetDateTime::now_utc().lt(&deadline) { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + } + assert!(check()) + } + #[tokio::test] + async fn defaults_to_unhealthy() { + let registry = HealthRegistry::new("liveness"); + assert!(!registry.get_status().healthy); + } + + #[tokio::test] + async fn one_component() { + let registry = HealthRegistry::new("liveness"); + + // New components are registered in Starting + let handle = registry + .register("one".to_string(), Duration::seconds(30)) + .await; + assert_or_retry(|| registry.get_status().components.len() == 1).await; + let mut status = registry.get_status(); + assert!(!status.healthy); + assert_eq!( + status.components.get("one"), + Some(&ComponentStatus::Starting) + ); + + // Status goes healthy once the component reports + handle.report_healthy().await; + assert_or_retry(|| registry.get_status().healthy).await; + status = registry.get_status(); + assert_eq!(status.components.len(), 1); + + // Status goes unhealthy if the components says so + handle.report_status(ComponentStatus::Unhealthy).await; + assert_or_retry(|| !registry.get_status().healthy).await; + status = registry.get_status(); + assert_eq!(status.components.len(), 1); + assert_eq!( + status.components.get("one"), + Some(&ComponentStatus::Unhealthy) + ); + } + + #[tokio::test] + async fn staleness_check() { + let registry = HealthRegistry::new("liveness"); + let handle = registry + .register("one".to_string(), Duration::seconds(30)) + .await; + + // Status goes healthy once the component reports + handle.report_healthy().await; + assert_or_retry(|| registry.get_status().healthy).await; + let mut status = registry.get_status(); + assert_eq!(status.components.len(), 1); + + // If the component's ping is too old, it is considered stalled and the healthcheck fails + // FIXME: we should mock the time instead + handle + .report_status(ComponentStatus::HealthyUntil( + OffsetDateTime::now_utc().sub(Duration::seconds(1)), + )) + .await; + assert_or_retry(|| !registry.get_status().healthy).await; + status = registry.get_status(); + assert_eq!(status.components.len(), 1); + assert_eq!( + status.components.get("one"), + Some(&ComponentStatus::Stalled) + ); + } + + #[tokio::test] + async fn several_components() { + let registry = HealthRegistry::new("liveness"); + let handle1 = registry + .register("one".to_string(), Duration::seconds(30)) + .await; + let handle2 = registry + .register("two".to_string(), Duration::seconds(30)) + .await; + assert_or_retry(|| registry.get_status().components.len() == 2).await; + + // First component going healthy is not enough + handle1.report_healthy().await; + assert_or_retry(|| { + registry.get_status().components.get("one").unwrap() != &ComponentStatus::Starting + }) + .await; + assert!(!registry.get_status().healthy); + + // Second component going healthy brings the health to green + handle2.report_healthy().await; + assert_or_retry(|| { + registry.get_status().components.get("two").unwrap() != &ComponentStatus::Starting + }) + .await; + assert!(registry.get_status().healthy); + + // First component going unhealthy takes down the health to red + handle1.report_status(ComponentStatus::Unhealthy).await; + assert_or_retry(|| !registry.get_status().healthy).await; + + // First component recovering returns the health to green + handle1.report_healthy().await; + assert_or_retry(|| registry.get_status().healthy).await; + + // Second component going unhealthy takes down the health to red + handle2.report_status(ComponentStatus::Unhealthy).await; + assert_or_retry(|| !registry.get_status().healthy).await; + } + + #[tokio::test] + async fn into_response() { + let nok = HealthStatus::default().into_response(); + assert_eq!(nok.status(), StatusCode::INTERNAL_SERVER_ERROR); + + let ok = HealthStatus { + healthy: true, + components: Default::default(), + } + .into_response(); + assert_eq!(ok.status(), StatusCode::OK); + } +} diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs index 8e63ded5a7bf2..7f49049add362 100644 --- a/hook-common/src/lib.rs +++ b/hook-common/src/lib.rs @@ -1,3 +1,4 @@ +pub mod health; pub mod kafka_messages; pub mod metrics; pub mod pgqueue; diff --git a/hook-janitor/Cargo.toml b/hook-janitor/Cargo.toml index f23626bea05a6..96a80ebd9c3d5 100644 --- a/hook-janitor/Cargo.toml +++ b/hook-janitor/Cargo.toml @@ -20,6 +20,7 @@ serde = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } +time = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tower = { workspace = true } diff --git a/hook-janitor/src/handlers/app.rs b/hook-janitor/src/handlers/app.rs index cab0e0d27991a..507a1cba48d46 100644 --- a/hook-janitor/src/handlers/app.rs +++ b/hook-janitor/src/handlers/app.rs @@ -1,7 +1,12 @@ -use axum::{routing, Router}; +use axum::{routing::get, Router}; +use hook_common::health::HealthRegistry; +use std::future::ready; -pub fn app() -> Router { - Router::new().route("/", routing::get(index)) +pub fn app(liveness: HealthRegistry) -> Router { + Router::new() + .route("/", get(index)) + .route("/_readiness", get(index)) + .route("/_liveness", get(move || ready(liveness.get_status()))) } pub async fn index() -> &'static str { diff --git a/hook-janitor/src/kafka_producer.rs b/hook-janitor/src/kafka_producer.rs index 1d0144cb75436..ba368663072c3 100644 --- a/hook-janitor/src/kafka_producer.rs +++ b/hook-janitor/src/kafka_producer.rs @@ -1,17 +1,27 @@ use crate::config::KafkaConfig; +use hook_common::health::HealthHandle; use rdkafka::error::KafkaError; use rdkafka::producer::FutureProducer; use rdkafka::ClientConfig; use tracing::debug; -// TODO: Take stats recording pieces that we want from `capture-rs`. -pub struct KafkaContext {} +pub struct KafkaContext { + liveness: HealthHandle, +} + +impl rdkafka::ClientContext for KafkaContext { + fn stats(&self, _: rdkafka::Statistics) { + // Signal liveness, as the main rdkafka loop is running and calling us + self.liveness.report_healthy_blocking(); -impl rdkafka::ClientContext for KafkaContext {} + // TODO: Take stats recording pieces that we want from `capture-rs`. + } +} pub async fn create_kafka_producer( config: &KafkaConfig, + liveness: HealthHandle, ) -> Result, KafkaError> { let mut client_config = ClientConfig::new(); client_config @@ -38,7 +48,10 @@ pub async fn create_kafka_producer( }; debug!("rdkafka configuration: {:?}", client_config); - let api: FutureProducer = client_config.create_with_context(KafkaContext {})?; + let api: FutureProducer = + client_config.create_with_context(KafkaContext { liveness })?; + + // TODO: ping the kafka brokers to confirm configuration is OK (copy capture) Ok(api) } diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs index 63068c35262bf..15d0068ad693a 100644 --- a/hook-janitor/src/main.rs +++ b/hook-janitor/src/main.rs @@ -4,6 +4,7 @@ use config::Config; use envconfig::Envconfig; use eyre::Result; use futures::future::{select, Either}; +use hook_common::health::{HealthHandle, HealthRegistry}; use kafka_producer::create_kafka_producer; use std::{str::FromStr, time::Duration}; use tokio::sync::Semaphore; @@ -25,13 +26,14 @@ async fn listen(app: Router, bind: String) -> Result<()> { Ok(()) } -async fn cleanup_loop(cleaner: Box, interval_secs: u64) { +async fn cleanup_loop(cleaner: Box, interval_secs: u64, liveness: HealthHandle) { let semaphore = Semaphore::new(1); let mut interval = tokio::time::interval(Duration::from_secs(interval_secs)); loop { let _permit = semaphore.acquire().await; interval.tick().await; + liveness.report_healthy().await; cleaner.cleanup().await; drop(_permit); } @@ -46,9 +48,14 @@ async fn main() { let mode_name = CleanerModeName::from_str(&config.mode) .unwrap_or_else(|_| panic!("invalid cleaner mode: {}", config.mode)); + let liveness = HealthRegistry::new("liveness"); + let cleaner = match mode_name { CleanerModeName::Webhooks => { - let kafka_producer = create_kafka_producer(&config.kafka) + let kafka_liveness = liveness + .register("rdkafka".to_string(), time::Duration::seconds(30)) + .await; + let kafka_producer = create_kafka_producer(&config.kafka, kafka_liveness) .await .expect("failed to create kafka producer"); @@ -64,9 +71,19 @@ async fn main() { } }; - let cleanup_loop = Box::pin(cleanup_loop(cleaner, config.cleanup_interval_secs)); + let cleanup_liveness = liveness + .register( + "cleanup_loop".to_string(), + time::Duration::seconds(config.cleanup_interval_secs as i64 * 2), + ) + .await; + let cleanup_loop = Box::pin(cleanup_loop( + cleaner, + config.cleanup_interval_secs, + cleanup_liveness, + )); - let app = setup_metrics_routes(handlers::app()); + let app = setup_metrics_routes(handlers::app(liveness)); let http_server = Box::pin(listen(app, config.bind())); match select(http_server, cleanup_loop).await { diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 4248a47281e52..7aa9aa353750f 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -457,6 +457,7 @@ mod tests { use super::*; use crate::config; use crate::kafka_producer::{create_kafka_producer, KafkaContext}; + use hook_common::health::HealthRegistry; use hook_common::kafka_messages::app_metrics::{ Error as WebhookError, ErrorDetails, ErrorType, }; @@ -476,6 +477,10 @@ mod tests { MockCluster<'static, DefaultProducerContext>, FutureProducer, ) { + let registry = HealthRegistry::new("liveness"); + let handle = registry + .register("one".to_string(), time::Duration::seconds(30)) + .await; let cluster = MockCluster::new(1).expect("failed to create mock brokers"); let config = config::KafkaConfig { @@ -491,7 +496,7 @@ mod tests { ( cluster, - create_kafka_producer(&config) + create_kafka_producer(&config, handle) .await .expect("failed to create mocked kafka producer"), ) diff --git a/hook-worker/Cargo.toml b/hook-worker/Cargo.toml index 11da0a80a564c..6ed5796efd1f0 100644 --- a/hook-worker/Cargo.toml +++ b/hook-worker/Cargo.toml @@ -15,6 +15,7 @@ reqwest = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } sqlx = { workspace = true } +time = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index cc17169b2bd83..d036d546a94e8 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -1,7 +1,10 @@ //! Consume `PgQueue` jobs to run webhook calls. +use axum::routing::get; use axum::Router; use envconfig::Envconfig; +use std::future::ready; +use hook_common::health::HealthRegistry; use hook_common::{ metrics::serve, metrics::setup_metrics_routes, pgqueue::PgQueue, retry::RetryPolicy, }; @@ -15,6 +18,11 @@ async fn main() -> Result<(), WorkerError> { let config = Config::init_from_env().expect("Invalid configuration:"); + let liveness = HealthRegistry::new("liveness"); + let worker_liveness = liveness + .register("worker".to_string(), time::Duration::seconds(60)) // TODO: compute the value from worker params + .await; + let retry_policy = RetryPolicy::build( config.retry_policy.backoff_coefficient, config.retry_policy.initial_interval.0, @@ -33,11 +41,16 @@ async fn main() -> Result<(), WorkerError> { config.request_timeout.0, config.max_concurrent_jobs, retry_policy, + worker_liveness, ); + let router = Router::new() + .route("/", get(index)) + .route("/_readiness", get(index)) + .route("/_liveness", get(move || ready(liveness.get_status()))); + let router = setup_metrics_routes(router); let bind = config.bind(); tokio::task::spawn(async move { - let router = setup_metrics_routes(Router::new()); serve(router, &bind) .await .expect("failed to start serving metrics"); @@ -47,3 +60,7 @@ async fn main() -> Result<(), WorkerError> { Ok(()) } + +pub async fn index() -> &'static str { + "rusty-hook worker" +} diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 77c6adb4b13f8..1041422fec17b 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -2,6 +2,7 @@ use std::collections; use std::sync::Arc; use std::time; +use hook_common::health::HealthHandle; use hook_common::{ pgqueue::{Job, PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, retry::RetryPolicy, @@ -75,6 +76,8 @@ pub struct WebhookWorker<'p> { max_concurrent_jobs: usize, /// The retry policy used to calculate retry intervals when a job fails with a retryable error. retry_policy: RetryPolicy, + /// The liveness check handle, to call on a schedule to report healthy + liveness: HealthHandle, } impl<'p> WebhookWorker<'p> { @@ -85,6 +88,7 @@ impl<'p> WebhookWorker<'p> { request_timeout: time::Duration, max_concurrent_jobs: usize, retry_policy: RetryPolicy, + liveness: HealthHandle, ) -> Self { let mut headers = header::HeaderMap::new(); headers.insert( @@ -106,6 +110,7 @@ impl<'p> WebhookWorker<'p> { client, max_concurrent_jobs, retry_policy, + liveness, } } @@ -117,6 +122,7 @@ impl<'p> WebhookWorker<'p> { loop { interval.tick().await; + self.liveness.report_healthy().await; if let Some(job) = self.queue.dequeue(&self.name).await? { return Ok(job); @@ -132,6 +138,7 @@ impl<'p> WebhookWorker<'p> { loop { interval.tick().await; + self.liveness.report_healthy().await; if let Some(job) = self.queue.dequeue_tx(&self.name).await? { return Ok(job); @@ -157,7 +164,6 @@ impl<'p> WebhookWorker<'p> { } else { loop { let webhook_job = self.wait_for_job().await?; - spawn_webhook_job_processing_task( self.client.clone(), semaphore.clone(), @@ -430,6 +436,8 @@ mod tests { // This is due to a long-standing cargo bug that reports imports and helper functions as unused. // See: https://github.com/rust-lang/rust/issues/46379. #[allow(unused_imports)] + use hook_common::health::HealthRegistry; + #[allow(unused_imports)] use hook_common::pgqueue::{JobStatus, NewJob}; #[allow(unused_imports)] use sqlx::PgPool; @@ -502,6 +510,10 @@ mod tests { plugin_id: 2, plugin_config_id: 3, }; + let registry = HealthRegistry::new("liveness"); + let liveness = registry + .register("worker".to_string(), ::time::Duration::seconds(30)) + .await; // enqueue takes ownership of the job enqueued to avoid bugs that can cause duplicate jobs. // Normally, a separate application would be enqueueing jobs for us to consume, so no ownership // conflicts would arise. However, in this test we need to do the enqueueing ourselves. @@ -521,6 +533,7 @@ mod tests { time::Duration::from_millis(5000), 10, RetryPolicy::default(), + liveness, ); let consumed_job = worker @@ -543,6 +556,8 @@ mod tests { .complete() .await .expect("job not successfully completed"); + + assert!(registry.get_status().healthy) } #[sqlx::test(migrations = "../migrations")] From c181c50580625699a077b81fcfada9a7d5c1dd5b Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 16 Jan 2024 12:56:04 -0700 Subject: [PATCH 188/249] Add queue depth gauges (#49) --- hook-janitor/src/webhooks.rs | 49 +++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 7aa9aa353750f..4b4f9a44f3bcc 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -24,6 +24,8 @@ pub enum WebhookCleanerError { PoolCreationError { error: sqlx::Error }, #[error("failed to acquire conn and start txn: {error}")] StartTxnError { error: sqlx::Error }, + #[error("failed to get queue depth: {error}")] + GetQueueDepthError { error: sqlx::Error }, #[error("failed to get row count: {error}")] GetRowCountError { error: sqlx::Error }, #[error("failed to get completed rows: {error}")] @@ -107,6 +109,14 @@ struct FailedRow { failures: u32, } +#[derive(sqlx::FromRow, Debug)] +struct QueueDepth { + oldest_created_at_untried: DateTime, + count_untried: i64, + oldest_created_at_retries: DateTime, + count_retries: i64, +} + impl From for AppMetric { fn from(row: FailedRow) -> Self { AppMetric { @@ -175,6 +185,33 @@ impl WebhookCleaner { }) } + async fn get_queue_depth(&self) -> Result { + let mut conn = self + .pg_pool + .acquire() + .await + .map_err(|e| WebhookCleanerError::StartTxnError { error: e })?; + + let base_query = r#" + SELECT + COALESCE(MIN(CASE WHEN attempt = 0 THEN created_at END), now()) AS oldest_created_at_untried, + SUM(CASE WHEN attempt = 0 THEN 1 ELSE 0 END) AS count_untried, + COALESCE(MIN(CASE WHEN attempt > 0 THEN created_at END), now()) AS oldest_created_at_retries, + SUM(CASE WHEN attempt > 0 THEN 1 ELSE 0 END) AS count_retries + FROM job_queue + WHERE status = 'available' + AND queue = $1; + "#; + + let row = sqlx::query_as::<_, QueueDepth>(base_query) + .bind(&self.queue_name) + .fetch_one(&mut *conn) + .await + .map_err(|e| WebhookCleanerError::GetQueueDepthError { error: e })?; + + Ok(row) + } + async fn start_serializable_txn(&self) -> Result { let mut tx = self .pg_pool @@ -229,7 +266,7 @@ impl WebhookCleaner { count(*) as successes FROM job_queue WHERE status = 'completed' - AND queue = $1 + AND queue = $1 GROUP BY hour, team_id, plugin_config_id ORDER BY hour, team_id, plugin_config_id; "#; @@ -354,6 +391,16 @@ impl WebhookCleaner { // of rows in memory. It seems unlikely we'll need to paginate, but that can be added in the // future if necessary. + let queue_depth = self.get_queue_depth().await?; + metrics::gauge!("queue_depth_oldest_created_at_untried") + .set(queue_depth.oldest_created_at_untried.timestamp() as f64); + metrics::gauge!("queue_depth", &[("status", "untried")]) + .set(queue_depth.count_untried as f64); + metrics::gauge!("queue_depth_oldest_created_at_retries") + .set(queue_depth.oldest_created_at_retries.timestamp() as f64); + metrics::gauge!("queue_depth", &[("status", "retries")]) + .set(queue_depth.count_retries as f64); + let mut tx = self.start_serializable_txn().await?; let (completed_row_count, completed_agg_row_count) = { From 1422683d87708eb5e4261f3ebf92594b92ea8793 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 17 Jan 2024 10:53:51 +0100 Subject: [PATCH 189/249] feat: add webhook_worker_saturation_percent metric for autoscaling (#50) --- hook-worker/src/worker.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 1041422fec17b..7fe6808d02b6f 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -149,9 +149,14 @@ impl<'p> WebhookWorker<'p> { /// Run this worker to continuously process any jobs that become available. pub async fn run(&self, transactional: bool) -> Result<(), WorkerError> { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); + let report_semaphore_utilization = || { + metrics::gauge!("webhook_worker_saturation_percent") + .set(1f64 - semaphore.available_permits() as f64 / self.max_concurrent_jobs as f64); + }; if transactional { loop { + report_semaphore_utilization(); let webhook_job = self.wait_for_job_tx().await?; spawn_webhook_job_processing_task( self.client.clone(), @@ -163,6 +168,7 @@ impl<'p> WebhookWorker<'p> { } } else { loop { + report_semaphore_utilization(); let webhook_job = self.wait_for_job().await?; spawn_webhook_job_processing_task( self.client.clone(), From 23ef5e276d1dc85333500aaa20bafa62a3594f5f Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 17 Jan 2024 12:43:36 +0100 Subject: [PATCH 190/249] fix:fix janitor get_queue_depth when queue is empty (#54) --- hook-janitor/src/webhooks.rs | 46 ++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 4b4f9a44f3bcc..1d536c0e7267b 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -195,9 +195,9 @@ impl WebhookCleaner { let base_query = r#" SELECT COALESCE(MIN(CASE WHEN attempt = 0 THEN created_at END), now()) AS oldest_created_at_untried, - SUM(CASE WHEN attempt = 0 THEN 1 ELSE 0 END) AS count_untried, + COALESCE(SUM(CASE WHEN attempt = 0 THEN 1 ELSE 0 END), 0) AS count_untried, COALESCE(MIN(CASE WHEN attempt > 0 THEN created_at END), now()) AS oldest_created_at_retries, - SUM(CASE WHEN attempt > 0 THEN 1 ELSE 0 END) AS count_retries + COALESCE(SUM(CASE WHEN attempt > 0 THEN 1 ELSE 0 END), 0) AS count_retries FROM job_queue WHERE status = 'available' AND queue = $1; @@ -513,6 +513,7 @@ mod tests { use rdkafka::consumer::{Consumer, StreamConsumer}; use rdkafka::mocking::MockCluster; use rdkafka::producer::{DefaultProducerContext, FutureProducer}; + use rdkafka::types::{RDKafkaApiKey, RDKafkaRespErr}; use rdkafka::{ClientConfig, Message}; use sqlx::{PgPool, Row}; use std::collections::HashMap; @@ -754,6 +755,47 @@ mod tests { check_app_metric_vector_equality(&expected_app_metrics, &received_app_metrics); } + #[sqlx::test(migrations = "../migrations")] + async fn test_cleanup_impl_empty_queue(db: PgPool) { + let (mock_cluster, mock_producer) = create_mock_kafka().await; + mock_cluster + .create_topic(APP_METRICS_TOPIC, 1, 1) + .expect("failed to create mock app_metrics topic"); + + // No payload should be produced to kafka as the queue is empty. + // Set a non-retriable produce error that would bubble-up when cleanup_impl is called. + let err = [RDKafkaRespErr::RD_KAFKA_RESP_ERR_MSG_SIZE_TOO_LARGE; 1]; + mock_cluster.request_errors(RDKafkaApiKey::Produce, &err); + + let consumer: StreamConsumer = ClientConfig::new() + .set("bootstrap.servers", mock_cluster.bootstrap_servers()) + .set("group.id", "mock") + .set("auto.offset.reset", "earliest") + .create() + .expect("failed to create mock consumer"); + consumer.subscribe(&[APP_METRICS_TOPIC]).unwrap(); + + let webhook_cleaner = WebhookCleaner::new_from_pool( + &"webhooks", + db, + mock_producer, + APP_METRICS_TOPIC.to_owned(), + ) + .expect("unable to create webhook cleaner"); + + let cleanup_stats = webhook_cleaner + .cleanup_impl() + .await + .expect("webbook cleanup_impl failed"); + + // Reported metrics are all zeroes + assert_eq!(cleanup_stats.rows_processed, 0); + assert_eq!(cleanup_stats.completed_row_count, 0); + assert_eq!(cleanup_stats.completed_agg_row_count, 0); + assert_eq!(cleanup_stats.failed_row_count, 0); + assert_eq!(cleanup_stats.failed_agg_row_count, 0); + } + #[sqlx::test(migrations = "../migrations", fixtures("webhook_cleanup"))] async fn test_serializable_isolation(db: PgPool) { let (_, mock_producer) = create_mock_kafka().await; From 9c9ebd5033ac8dcb1c283a5a31203ea682a8a26f Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 17 Jan 2024 07:56:37 -0700 Subject: [PATCH 191/249] Remove queue filters from janitor (#52) --- hook-janitor/src/config.rs | 3 -- hook-janitor/src/main.rs | 1 - hook-janitor/src/webhooks.rs | 82 ++++++++++++------------------------ 3 files changed, 27 insertions(+), 59 deletions(-) diff --git a/hook-janitor/src/config.rs b/hook-janitor/src/config.rs index 252a67011460c..389de0342e03a 100644 --- a/hook-janitor/src/config.rs +++ b/hook-janitor/src/config.rs @@ -11,9 +11,6 @@ pub struct Config { #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] pub database_url: String, - #[envconfig(default = "default")] - pub queue_name: String, - #[envconfig(default = "30")] pub cleanup_interval_secs: u64, diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs index 15d0068ad693a..46ee37560bb90 100644 --- a/hook-janitor/src/main.rs +++ b/hook-janitor/src/main.rs @@ -61,7 +61,6 @@ async fn main() { Box::new( WebhookCleaner::new( - &config.queue_name, &config.database_url, kafka_producer, config.kafka.app_metrics_topic.to_owned(), diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 1d536c0e7267b..5ac9d558fa812 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -51,7 +51,6 @@ pub enum WebhookCleanerError { type Result = std::result::Result; pub struct WebhookCleaner { - queue_name: String, pg_pool: PgPool, kafka_producer: FutureProducer, app_metrics_topic: String, @@ -149,19 +148,16 @@ struct CleanupStats { impl WebhookCleaner { pub fn new( - queue_name: &str, database_url: &str, kafka_producer: FutureProducer, app_metrics_topic: String, ) -> Result { - let queue_name = queue_name.to_owned(); let pg_pool = PgPoolOptions::new() .acquire_timeout(Duration::from_secs(10)) .connect_lazy(database_url) .map_err(|error| WebhookCleanerError::PoolCreationError { error })?; Ok(Self { - queue_name, pg_pool, kafka_producer, app_metrics_topic, @@ -170,15 +166,11 @@ impl WebhookCleaner { #[allow(dead_code)] // This is used in tests. pub fn new_from_pool( - queue_name: &str, pg_pool: PgPool, kafka_producer: FutureProducer, app_metrics_topic: String, ) -> Result { - let queue_name = queue_name.to_owned(); - Ok(Self { - queue_name, pg_pool, kafka_producer, app_metrics_topic, @@ -199,12 +191,10 @@ impl WebhookCleaner { COALESCE(MIN(CASE WHEN attempt > 0 THEN created_at END), now()) AS oldest_created_at_retries, COALESCE(SUM(CASE WHEN attempt > 0 THEN 1 ELSE 0 END), 0) AS count_retries FROM job_queue - WHERE status = 'available' - AND queue = $1; + WHERE status = 'available'; "#; let row = sqlx::query_as::<_, QueueDepth>(base_query) - .bind(&self.queue_name) .fetch_one(&mut *conn) .await .map_err(|e| WebhookCleanerError::GetQueueDepthError { error: e })?; @@ -240,12 +230,10 @@ impl WebhookCleaner { ) -> Result { let base_query = r#" SELECT count(*) FROM job_queue - WHERE queue = $1 - AND status = $2::job_status; + WHERE status = $1::job_status; "#; let count: i64 = sqlx::query(base_query) - .bind(&self.queue_name) .bind(status) .fetch_one(&mut *tx.0) .await @@ -266,13 +254,11 @@ impl WebhookCleaner { count(*) as successes FROM job_queue WHERE status = 'completed' - AND queue = $1 GROUP BY hour, team_id, plugin_config_id ORDER BY hour, team_id, plugin_config_id; "#; let rows = sqlx::query_as::<_, CompletedRow>(base_query) - .bind(&self.queue_name) .fetch_all(&mut *tx.0) .await .map_err(|e| WebhookCleanerError::GetCompletedRowsError { error: e })?; @@ -289,13 +275,11 @@ impl WebhookCleaner { count(*) as failures FROM job_queue WHERE status = 'failed' - AND queue = $1 GROUP BY hour, team_id, plugin_config_id, last_error ORDER BY hour, team_id, plugin_config_id, last_error; "#; let rows = sqlx::query_as::<_, FailedRow>(base_query) - .bind(&self.queue_name) .fetch_all(&mut *tx.0) .await .map_err(|e| WebhookCleanerError::GetFailedRowsError { error: e })?; @@ -352,11 +336,9 @@ impl WebhookCleaner { let base_query = r#" DELETE FROM job_queue WHERE status IN ('failed', 'completed') - AND queue = $1; "#; let result = sqlx::query(base_query) - .bind(&self.queue_name) .execute(&mut *tx.0) .await .map_err(|e| WebhookCleanerError::DeleteRowsError { error: e })?; @@ -577,22 +559,17 @@ mod tests { .expect("failed to create mock consumer"); consumer.subscribe(&[APP_METRICS_TOPIC]).unwrap(); - let webhook_cleaner = WebhookCleaner::new_from_pool( - &"webhooks", - db, - mock_producer, - APP_METRICS_TOPIC.to_owned(), - ) - .expect("unable to create webhook cleaner"); + let webhook_cleaner = + WebhookCleaner::new_from_pool(db, mock_producer, APP_METRICS_TOPIC.to_owned()) + .expect("unable to create webhook cleaner"); let cleanup_stats = webhook_cleaner .cleanup_impl() .await .expect("webbook cleanup_impl failed"); - // Rows from other queues and rows that are not 'completed' or 'failed' should not be - // processed. - assert_eq!(cleanup_stats.rows_processed, 11); + // Rows that are not 'completed' or 'failed' should not be processed. + assert_eq!(cleanup_stats.rows_processed, 13); let mut received_app_metrics = Vec::new(); for _ in 0..(cleanup_stats.completed_agg_row_count + cleanup_stats.failed_agg_row_count) { @@ -609,7 +586,7 @@ mod tests { plugin_config_id: 2, job_id: None, category: AppMetricCategory::Webhook, - successes: 2, + successes: 3, successes_on_retry: 0, failures: 0, error_uuid: None, @@ -682,7 +659,7 @@ mod tests { category: AppMetricCategory::Webhook, successes: 0, successes_on_retry: 0, - failures: 2, + failures: 3, error_uuid: Some(Uuid::parse_str("018c8935-d038-714a-957c-0df43d42e377").unwrap()), error_type: Some(ErrorType::TimeoutError), error_details: Some(ErrorDetails { @@ -799,13 +776,9 @@ mod tests { #[sqlx::test(migrations = "../migrations", fixtures("webhook_cleanup"))] async fn test_serializable_isolation(db: PgPool) { let (_, mock_producer) = create_mock_kafka().await; - let webhook_cleaner = WebhookCleaner::new_from_pool( - &"webhooks", - db.clone(), - mock_producer, - APP_METRICS_TOPIC.to_owned(), - ) - .expect("unable to create webhook cleaner"); + let webhook_cleaner = + WebhookCleaner::new_from_pool(db.clone(), mock_producer, APP_METRICS_TOPIC.to_owned()) + .expect("unable to create webhook cleaner"); let queue = PgQueue::new_from_pool("webhooks", db.clone()) .await @@ -813,14 +786,13 @@ mod tests { async fn get_count_from_new_conn(db: &PgPool, status: &str) -> i64 { let mut conn = db.acquire().await.unwrap(); - let count: i64 = sqlx::query( - "SELECT count(*) FROM job_queue WHERE queue = 'webhooks' AND status = $1::job_status", - ) - .bind(&status) - .fetch_one(&mut *conn) - .await - .unwrap() - .get(0); + let count: i64 = + sqlx::query("SELECT count(*) FROM job_queue WHERE status = $1::job_status") + .bind(&status) + .fetch_one(&mut *conn) + .await + .unwrap() + .get(0); count } @@ -832,10 +804,10 @@ mod tests { .unwrap(); webhook_cleaner.get_failed_agg_rows(&mut tx).await.unwrap(); - // All 13 rows in the queue are visible from outside the txn. - // The 11 the cleaner will process, plus 1 available and 1 running. - assert_eq!(get_count_from_new_conn(&db, "completed").await, 5); - assert_eq!(get_count_from_new_conn(&db, "failed").await, 6); + // All 15 rows in the DB are visible from outside the txn. + // The 13 the cleaner will process, plus 1 available and 1 running. + assert_eq!(get_count_from_new_conn(&db, "completed").await, 6); + assert_eq!(get_count_from_new_conn(&db, "failed").await, 7); assert_eq!(get_count_from_new_conn(&db, "available").await, 1); assert_eq!(get_count_from_new_conn(&db, "running").await, 1); @@ -896,15 +868,15 @@ mod tests { } // There are now 2 more completed rows (jobs added above) than before, visible from outside the txn. - assert_eq!(get_count_from_new_conn(&db, "completed").await, 7); + assert_eq!(get_count_from_new_conn(&db, "completed").await, 8); assert_eq!(get_count_from_new_conn(&db, "available").await, 1); let rows_processed = webhook_cleaner.delete_observed_rows(&mut tx).await.unwrap(); - // The 11 rows that were in the queue when the txn started should be deleted. - assert_eq!(rows_processed, 11); + // The 13 rows in the DB when the txn started should be deleted. + assert_eq!(rows_processed, 13); // We haven't committed, so the rows are still visible from outside the txn. - assert_eq!(get_count_from_new_conn(&db, "completed").await, 7); + assert_eq!(get_count_from_new_conn(&db, "completed").await, 8); assert_eq!(get_count_from_new_conn(&db, "available").await, 1); webhook_cleaner.commit_txn(tx).await.unwrap(); From 099053e0b1891be416ea39967589a64d3e4349db Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 17 Jan 2024 08:27:37 -0700 Subject: [PATCH 192/249] Make retry_queue_name truly optional from env to main (#53) --- hook-janitor/src/webhooks.rs | 10 +++------- hook-worker/src/config.rs | 3 +-- hook-worker/src/main.rs | 14 +++++++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 5ac9d558fa812..bc016055d3b3c 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -752,13 +752,9 @@ mod tests { .expect("failed to create mock consumer"); consumer.subscribe(&[APP_METRICS_TOPIC]).unwrap(); - let webhook_cleaner = WebhookCleaner::new_from_pool( - &"webhooks", - db, - mock_producer, - APP_METRICS_TOPIC.to_owned(), - ) - .expect("unable to create webhook cleaner"); + let webhook_cleaner = + WebhookCleaner::new_from_pool(db, mock_producer, APP_METRICS_TOPIC.to_owned()) + .expect("unable to create webhook cleaner"); let cleanup_stats = webhook_cleaner .cleanup_impl() diff --git a/hook-worker/src/config.rs b/hook-worker/src/config.rs index 8b2b4ba49f205..8484671047fc2 100644 --- a/hook-worker/src/config.rs +++ b/hook-worker/src/config.rs @@ -70,6 +70,5 @@ pub struct RetryPolicyConfig { #[envconfig(default = "100000")] pub maximum_interval: EnvMsDuration, - #[envconfig(default = "default")] - pub retry_queue_name: String, + pub retry_queue_name: Option, } diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index d036d546a94e8..bf6b4fd1e1468 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -23,13 +23,17 @@ async fn main() -> Result<(), WorkerError> { .register("worker".to_string(), time::Duration::seconds(60)) // TODO: compute the value from worker params .await; - let retry_policy = RetryPolicy::build( + let mut retry_policy_builder = RetryPolicy::build( config.retry_policy.backoff_coefficient, config.retry_policy.initial_interval.0, ) - .maximum_interval(config.retry_policy.maximum_interval.0) - .queue(&config.retry_policy.retry_queue_name) - .provide(); + .maximum_interval(config.retry_policy.maximum_interval.0); + + retry_policy_builder = match &config.retry_policy.retry_queue_name { + Some(retry_queue_name) => retry_policy_builder.queue(retry_queue_name), + _ => retry_policy_builder, + }; + let queue = PgQueue::new(&config.queue_name, &config.database_url) .await .expect("failed to initialize queue"); @@ -40,7 +44,7 @@ async fn main() -> Result<(), WorkerError> { config.poll_interval.0, config.request_timeout.0, config.max_concurrent_jobs, - retry_policy, + retry_policy_builder.provide(), worker_liveness, ); From 58474c06d22bb5d3a1a2df6289e9cdce847c6b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Wed, 17 Jan 2024 20:17:15 +0100 Subject: [PATCH 193/249] refactor: Use new NonEmptyString config type for queue names (#55) --- hook-worker/src/config.rs | 28 ++++++++++++++++++++++++++-- hook-worker/src/main.rs | 9 +++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/hook-worker/src/config.rs b/hook-worker/src/config.rs index 8484671047fc2..74342f71e0030 100644 --- a/hook-worker/src/config.rs +++ b/hook-worker/src/config.rs @@ -18,7 +18,7 @@ pub struct Config { pub worker_name: String, #[envconfig(default = "default")] - pub queue_name: String, + pub queue_name: NonEmptyString, #[envconfig(default = "100")] pub poll_interval: EnvMsDuration, @@ -70,5 +70,29 @@ pub struct RetryPolicyConfig { #[envconfig(default = "100000")] pub maximum_interval: EnvMsDuration, - pub retry_queue_name: Option, + pub retry_queue_name: Option, +} + +#[derive(Debug, Clone)] +pub struct NonEmptyString(pub String); + +impl NonEmptyString { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct StringIsEmptyError; + +impl FromStr for NonEmptyString { + type Err = StringIsEmptyError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Err(StringIsEmptyError) + } else { + Ok(NonEmptyString(s.to_owned())) + } + } } diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index bf6b4fd1e1468..bfee9add6271a 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -29,12 +29,13 @@ async fn main() -> Result<(), WorkerError> { ) .maximum_interval(config.retry_policy.maximum_interval.0); - retry_policy_builder = match &config.retry_policy.retry_queue_name { - Some(retry_queue_name) => retry_policy_builder.queue(retry_queue_name), - _ => retry_policy_builder, + retry_policy_builder = if let Some(retry_queue_name) = &config.retry_policy.retry_queue_name { + retry_policy_builder.queue(retry_queue_name.as_str()) + } else { + retry_policy_builder }; - let queue = PgQueue::new(&config.queue_name, &config.database_url) + let queue = PgQueue::new(config.queue_name.as_str(), &config.database_url) .await .expect("failed to initialize queue"); From 499f1432169f0fd8872e16c250c48500c3d10914 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 18 Jan 2024 10:10:33 -0700 Subject: [PATCH 194/249] Log error rather than exiting process on dequeue error (#51) --- hook-worker/src/main.rs | 2 +- hook-worker/src/worker.rs | 35 ++++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index bfee9add6271a..345fa3dd31c4a 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -61,7 +61,7 @@ async fn main() -> Result<(), WorkerError> { .expect("failed to start serving metrics"); }); - worker.run(config.transactional).await?; + worker.run(config.transactional).await; Ok(()) } diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 7fe6808d02b6f..c526c3f7cce1c 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -115,17 +115,20 @@ impl<'p> WebhookWorker<'p> { } /// Wait until a job becomes available in our queue. - async fn wait_for_job<'a>( - &self, - ) -> Result, WorkerError> { + async fn wait_for_job<'a>(&self) -> PgJob { let mut interval = tokio::time::interval(self.poll_interval); loop { interval.tick().await; self.liveness.report_healthy().await; - if let Some(job) = self.queue.dequeue(&self.name).await? { - return Ok(job); + match self.queue.dequeue(&self.name).await { + Ok(Some(job)) => return job, + Ok(None) => continue, + Err(error) => { + error!("error while trying to dequeue job: {}", error); + continue; + } } } } @@ -133,21 +136,26 @@ impl<'p> WebhookWorker<'p> { /// Wait until a job becomes available in our queue in transactional mode. async fn wait_for_job_tx<'a>( &self, - ) -> Result, WorkerError> { + ) -> PgTransactionJob<'a, WebhookJobParameters, WebhookJobMetadata> { let mut interval = tokio::time::interval(self.poll_interval); loop { interval.tick().await; self.liveness.report_healthy().await; - if let Some(job) = self.queue.dequeue_tx(&self.name).await? { - return Ok(job); + match self.queue.dequeue_tx(&self.name).await { + Ok(Some(job)) => return job, + Ok(None) => continue, + Err(error) => { + error!("error while trying to dequeue_tx job: {}", error); + continue; + } } } } /// Run this worker to continuously process any jobs that become available. - pub async fn run(&self, transactional: bool) -> Result<(), WorkerError> { + pub async fn run(&self, transactional: bool) { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); let report_semaphore_utilization = || { metrics::gauge!("webhook_worker_saturation_percent") @@ -157,7 +165,7 @@ impl<'p> WebhookWorker<'p> { if transactional { loop { report_semaphore_utilization(); - let webhook_job = self.wait_for_job_tx().await?; + let webhook_job = self.wait_for_job_tx().await; spawn_webhook_job_processing_task( self.client.clone(), semaphore.clone(), @@ -169,7 +177,7 @@ impl<'p> WebhookWorker<'p> { } else { loop { report_semaphore_utilization(); - let webhook_job = self.wait_for_job().await?; + let webhook_job = self.wait_for_job().await; spawn_webhook_job_processing_task( self.client.clone(), semaphore.clone(), @@ -542,10 +550,7 @@ mod tests { liveness, ); - let consumed_job = worker - .wait_for_job() - .await - .expect("failed to wait and read job"); + let consumed_job = worker.wait_for_job().await; assert_eq!(consumed_job.job.attempt, 1); assert!(consumed_job.job.attempted_by.contains(&worker_id)); From a4b9943d7f2e9ede94c6089658d4c1b869ca2579 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 19 Jan 2024 12:28:03 +0100 Subject: [PATCH 195/249] add webhook_cleanup_last_success_timestamp metric for alerting (#59) --- hook-common/src/metrics.rs | 11 ++++++++++- hook-janitor/src/webhooks.rs | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/hook-common/src/metrics.rs b/hook-common/src/metrics.rs index 0e1ef2db72a33..66bcfc95ceeec 100644 --- a/hook-common/src/metrics.rs +++ b/hook-common/src/metrics.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Instant, SystemTime}; use axum::{ body::Body, extract::MatchedPath, http::Request, middleware::Next, response::IntoResponse, @@ -71,3 +71,12 @@ pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse response } + +/// Returns the number of seconds since the Unix epoch, to use in prom gauges. +/// Saturates to zero if the system time is set before epoch. +pub fn get_current_timestamp_seconds() -> f64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as f64 +} diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index bc016055d3b3c..705e1b394f415 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -17,6 +17,7 @@ use crate::cleanup::Cleaner; use crate::kafka_producer::KafkaContext; use hook_common::kafka_messages::app_metrics::{AppMetric, AppMetricCategory}; +use hook_common::metrics::get_current_timestamp_seconds; #[derive(Error, Debug)] pub enum WebhookCleanerError { @@ -446,6 +447,8 @@ impl Cleaner for WebhookCleaner { match self.cleanup_impl().await { Ok(stats) => { metrics::counter!("webhook_cleanup_success",).increment(1); + metrics::gauge!("webhook_cleanup_last_success_timestamp",) + .set(get_current_timestamp_seconds()); if stats.rows_processed > 0 { let elapsed_time = start_time.elapsed().as_secs_f64(); From e2b5dcb3e02f80f261e2691ce6fff64edc7717bc Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 19 Jan 2024 05:28:51 -0700 Subject: [PATCH 196/249] Change created_at to scheduled_at for metrics (#58) --- hook-janitor/src/webhooks.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 705e1b394f415..9c33c5e487b5c 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -111,9 +111,9 @@ struct FailedRow { #[derive(sqlx::FromRow, Debug)] struct QueueDepth { - oldest_created_at_untried: DateTime, + oldest_scheduled_at_untried: DateTime, count_untried: i64, - oldest_created_at_retries: DateTime, + oldest_scheduled_at_retries: DateTime, count_retries: i64, } @@ -187,9 +187,9 @@ impl WebhookCleaner { let base_query = r#" SELECT - COALESCE(MIN(CASE WHEN attempt = 0 THEN created_at END), now()) AS oldest_created_at_untried, + COALESCE(MIN(CASE WHEN attempt = 0 THEN scheduled_at END), now()) AS oldest_scheduled_at_untried, COALESCE(SUM(CASE WHEN attempt = 0 THEN 1 ELSE 0 END), 0) AS count_untried, - COALESCE(MIN(CASE WHEN attempt > 0 THEN created_at END), now()) AS oldest_created_at_retries, + COALESCE(MIN(CASE WHEN attempt > 0 THEN scheduled_at END), now()) AS oldest_scheduled_at_retries, COALESCE(SUM(CASE WHEN attempt > 0 THEN 1 ELSE 0 END), 0) AS count_retries FROM job_queue WHERE status = 'available'; @@ -374,15 +374,16 @@ impl WebhookCleaner { // of rows in memory. It seems unlikely we'll need to paginate, but that can be added in the // future if necessary. + let untried_status = [("status", "untried")]; + let retries_status = [("status", "retries")]; + let queue_depth = self.get_queue_depth().await?; - metrics::gauge!("queue_depth_oldest_created_at_untried") - .set(queue_depth.oldest_created_at_untried.timestamp() as f64); - metrics::gauge!("queue_depth", &[("status", "untried")]) - .set(queue_depth.count_untried as f64); - metrics::gauge!("queue_depth_oldest_created_at_retries") - .set(queue_depth.oldest_created_at_retries.timestamp() as f64); - metrics::gauge!("queue_depth", &[("status", "retries")]) - .set(queue_depth.count_retries as f64); + metrics::gauge!("queue_depth_oldest_scheduled", &untried_status) + .set(queue_depth.oldest_scheduled_at_untried.timestamp() as f64); + metrics::gauge!("queue_depth", &untried_status).set(queue_depth.count_untried as f64); + metrics::gauge!("queue_depth_oldest_scheduled", &retries_status) + .set(queue_depth.oldest_scheduled_at_retries.timestamp() as f64); + metrics::gauge!("queue_depth", &retries_status).set(queue_depth.count_retries as f64); let mut tx = self.start_serializable_txn().await?; From 6729401db21f737c45bc672af12d57321b8e6b26 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 30 Jan 2024 09:40:34 -0700 Subject: [PATCH 197/249] =?UTF-8?q?Add=20very=20basic=20version=20of=20job?= =?UTF-8?q?=20unstuck-ing=20for=20non-txn=20jobs=20that=20hang=20=E2=80=A6?= =?UTF-8?q?=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hook-janitor/src/fixtures/webhook_cleanup.sql | 16 ++++++ hook-janitor/src/webhooks.rs | 55 ++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql index bddaf269763d5..5dfa8272ef168 100644 --- a/hook-janitor/src/fixtures/webhook_cleanup.sql +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -2,6 +2,7 @@ INSERT INTO job_queue ( errors, metadata, + attempted_at, last_attempt_finished_at, parameters, queue, @@ -14,6 +15,7 @@ VALUES NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'completed', @@ -24,6 +26,7 @@ VALUES NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'completed', @@ -34,6 +37,7 @@ VALUES NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 21:01:18.799371+00', + '2023-12-19 21:01:18.799371+00', '{}', 'webhooks', 'completed', @@ -44,6 +48,7 @@ VALUES NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', '2023-12-19 20:01:18.80335+00', + '2023-12-19 20:01:18.80335+00', '{}', 'webhooks', 'completed', @@ -54,6 +59,7 @@ VALUES NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'not-webhooks', 'completed', @@ -64,6 +70,7 @@ VALUES NULL, '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'completed', @@ -74,6 +81,7 @@ VALUES ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'failed', @@ -84,6 +92,7 @@ VALUES ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'failed', @@ -94,6 +103,7 @@ VALUES ARRAY ['{"type":"ConnectionError","details":{"error":{"name":"Connection Error"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'failed', @@ -104,6 +114,7 @@ VALUES ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 21:01:18.799371+00', + '2023-12-19 21:01:18.799371+00', '{}', 'webhooks', 'failed', @@ -114,6 +125,7 @@ VALUES ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'failed', @@ -124,6 +136,7 @@ VALUES ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'not-webhooks', 'failed', @@ -134,6 +147,7 @@ VALUES ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{}', 'webhooks', 'failed', @@ -144,6 +158,7 @@ VALUES NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + '2023-12-19 20:01:18.799371+00', '{"body": "hello world", "headers": {}, "method": "POST", "url": "https://myhost/endpoint"}', 'webhooks', 'available', @@ -154,6 +169,7 @@ VALUES NULL, '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', + now() - '1 hour' :: interval, '{}', 'webhooks', 'running', diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 9c33c5e487b5c..ee8ff434c71a3 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -23,8 +23,12 @@ use hook_common::metrics::get_current_timestamp_seconds; pub enum WebhookCleanerError { #[error("failed to create postgres pool: {error}")] PoolCreationError { error: sqlx::Error }, + #[error("failed to acquire conn: {error}")] + AcquireConnError { error: sqlx::Error }, #[error("failed to acquire conn and start txn: {error}")] StartTxnError { error: sqlx::Error }, + #[error("failed to reschedule stuck jobs: {error}")] + RescheduleStuckJobsError { error: sqlx::Error }, #[error("failed to get queue depth: {error}")] GetQueueDepthError { error: sqlx::Error }, #[error("failed to get row count: {error}")] @@ -140,6 +144,7 @@ impl From for AppMetric { struct SerializableTxn<'a>(Transaction<'a, Postgres>); struct CleanupStats { + jobs_unstuck_count: u64, rows_processed: u64, completed_row_count: u64, completed_agg_row_count: u64, @@ -178,12 +183,51 @@ impl WebhookCleaner { }) } + async fn reschedule_stuck_jobs(&self) -> Result { + let mut conn = self + .pg_pool + .acquire() + .await + .map_err(|e| WebhookCleanerError::AcquireConnError { error: e })?; + + // The "non-transactional" worker runs the risk of crashing and leaving jobs permanently in + // the `running` state. This query will reschedule any jobs that have been in the running + // state for more than 2 minutes (which is *much* longer than we expect any Webhook job to + // take). + // + // We don't need to increment the `attempt` counter here because the worker already did that + // when it moved the job into `running`. + // + // If the previous worker was somehow stalled for 2 minutes and completes the task, that + // will mean we sent duplicate Webhooks. Success stats should not be affected, since both + // will update the same job row, which will only be processed once by the janitor. + + let base_query = r#" + UPDATE + job_queue + SET + status = 'available'::job_status, + last_attempt_finished_at = NOW(), + scheduled_at = NOW() + WHERE + status = 'running'::job_status + AND attempted_at < NOW() - INTERVAL '2 minutes' + "#; + + let result = sqlx::query(base_query) + .execute(&mut *conn) + .await + .map_err(|e| WebhookCleanerError::RescheduleStuckJobsError { error: e })?; + + Ok(result.rows_affected()) + } + async fn get_queue_depth(&self) -> Result { let mut conn = self .pg_pool .acquire() .await - .map_err(|e| WebhookCleanerError::StartTxnError { error: e })?; + .map_err(|e| WebhookCleanerError::AcquireConnError { error: e })?; let base_query = r#" SELECT @@ -377,6 +421,8 @@ impl WebhookCleaner { let untried_status = [("status", "untried")]; let retries_status = [("status", "retries")]; + let jobs_unstuck_count = self.reschedule_stuck_jobs().await?; + let queue_depth = self.get_queue_depth().await?; metrics::gauge!("queue_depth_oldest_scheduled", &untried_status) .set(queue_depth.oldest_scheduled_at_untried.timestamp() as f64); @@ -430,6 +476,7 @@ impl WebhookCleaner { } Ok(CleanupStats { + jobs_unstuck_count, rows_processed: rows_deleted, completed_row_count, completed_agg_row_count, @@ -450,6 +497,8 @@ impl Cleaner for WebhookCleaner { metrics::counter!("webhook_cleanup_success",).increment(1); metrics::gauge!("webhook_cleanup_last_success_timestamp",) .set(get_current_timestamp_seconds()); + metrics::counter!("webhook_cleanup_jobs_unstuck") + .increment(stats.jobs_unstuck_count); if stats.rows_processed > 0 { let elapsed_time = start_time.elapsed().as_secs_f64(); @@ -572,6 +621,9 @@ mod tests { .await .expect("webbook cleanup_impl failed"); + // The one 'running' job is transitioned to 'available'. + assert_eq!(cleanup_stats.jobs_unstuck_count, 1); + // Rows that are not 'completed' or 'failed' should not be processed. assert_eq!(cleanup_stats.rows_processed, 13); @@ -766,6 +818,7 @@ mod tests { .expect("webbook cleanup_impl failed"); // Reported metrics are all zeroes + assert_eq!(cleanup_stats.jobs_unstuck_count, 0); assert_eq!(cleanup_stats.rows_processed, 0); assert_eq!(cleanup_stats.completed_row_count, 0); assert_eq!(cleanup_stats.completed_agg_row_count, 0); From da6250b783b28a4570398997df9bfd2645f4cb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 2 Feb 2024 13:45:41 +0000 Subject: [PATCH 198/249] fix: Use a good index for dequeue (#61) --- migrations/20240202003133_better_dequeue_index.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 migrations/20240202003133_better_dequeue_index.sql diff --git a/migrations/20240202003133_better_dequeue_index.sql b/migrations/20240202003133_better_dequeue_index.sql new file mode 100644 index 0000000000000..a619fb1ac8c9d --- /dev/null +++ b/migrations/20240202003133_better_dequeue_index.sql @@ -0,0 +1,10 @@ +-- Dequeue is not hitting this index, so dropping is safe this time. +DROP INDEX idx_queue_scheduled_at; + +/* +Partial index used for dequeuing from job_queue. + +Dequeue only looks at available jobs so a partial index serves us well. +Moreover, dequeue sorts jobs by attempt and scheduled_at, which matches this index. +*/ +CREATE INDEX idx_queue_dequeue_partial ON job_queue(queue, attempt, scheduled_at) WHERE status = 'available' :: job_status; From 8559b127ef1d05d14185621c11afe1533ec6299b Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 5 Feb 2024 11:24:56 +0100 Subject: [PATCH 199/249] declare a PG application name visible in PG stats (#62) Co-authored-by: Brett Hoerner --- hook-api/src/config.rs | 3 +++ hook-api/src/main.rs | 2 ++ hook-common/src/pgqueue.rs | 16 ++++++++++++---- hook-janitor/src/webhooks.rs | 9 ++++++--- hook-worker/src/config.rs | 3 +++ hook-worker/src/main.rs | 11 ++++++++--- 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/hook-api/src/config.rs b/hook-api/src/config.rs index 3fe88b3e436c0..55fa404e5149d 100644 --- a/hook-api/src/config.rs +++ b/hook-api/src/config.rs @@ -13,6 +13,9 @@ pub struct Config { #[envconfig(default = "default")] pub queue_name: String, + + #[envconfig(default = "100")] + pub max_pg_connections: u32, } impl Config { diff --git a/hook-api/src/main.rs b/hook-api/src/main.rs index 4fbbdfbb7d4ed..9a9a9fd41c0c2 100644 --- a/hook-api/src/main.rs +++ b/hook-api/src/main.rs @@ -28,6 +28,8 @@ async fn main() { // side, but we don't need more than one queue for now. &config.queue_name, &config.database_url, + config.max_pg_connections, + "hook-api", ) .await .expect("failed to initialize queue"); diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index fa2b5eb0c38be..4dab9183421a4 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -7,7 +7,7 @@ use std::time; use async_trait::async_trait; use chrono; use serde; -use sqlx::postgres::{PgPool, PgPoolOptions}; +use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions}; use thiserror::Error; /// Enumeration of errors for operations with PgQueue. @@ -524,11 +524,19 @@ impl PgQueue { /// /// * `queue_name`: A name for the queue we are going to initialize. /// * `url`: A URL pointing to where the PostgreSQL database is hosted. - pub async fn new(queue_name: &str, url: &str) -> PgQueueResult { + pub async fn new( + queue_name: &str, + url: &str, + max_connections: u32, + app_name: &'static str, + ) -> PgQueueResult { let name = queue_name.to_owned(); + let options = PgConnectOptions::from_str(url) + .map_err(|error| PgQueueError::PoolCreationError { error })? + .application_name(app_name); let pool = PgPoolOptions::new() - .connect_lazy(url) - .map_err(|error| PgQueueError::PoolCreationError { error })?; + .max_connections(max_connections) + .connect_lazy_with(options); Ok(Self { name, pool }) } diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index ee8ff434c71a3..5cdf4318f69b0 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use std::time::{Duration, Instant}; use async_trait::async_trait; @@ -7,7 +8,7 @@ use hook_common::webhook::WebhookJobError; use rdkafka::error::KafkaError; use rdkafka::producer::{FutureProducer, FutureRecord}; use serde_json::error::Error as SerdeError; -use sqlx::postgres::{PgPool, PgPoolOptions, Postgres}; +use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions, Postgres}; use sqlx::types::{chrono, Uuid}; use sqlx::{Row, Transaction}; use thiserror::Error; @@ -158,10 +159,12 @@ impl WebhookCleaner { kafka_producer: FutureProducer, app_metrics_topic: String, ) -> Result { + let options = PgConnectOptions::from_str(database_url) + .map_err(|error| WebhookCleanerError::PoolCreationError { error })? + .application_name("hook-janitor"); let pg_pool = PgPoolOptions::new() .acquire_timeout(Duration::from_secs(10)) - .connect_lazy(database_url) - .map_err(|error| WebhookCleanerError::PoolCreationError { error })?; + .connect_lazy_with(options); Ok(Self { pg_pool, diff --git a/hook-worker/src/config.rs b/hook-worker/src/config.rs index 74342f71e0030..477ff74349242 100644 --- a/hook-worker/src/config.rs +++ b/hook-worker/src/config.rs @@ -29,6 +29,9 @@ pub struct Config { #[envconfig(default = "1024")] pub max_concurrent_jobs: usize, + #[envconfig(default = "100")] + pub max_pg_connections: u32, + #[envconfig(nested = true)] pub retry_policy: RetryPolicyConfig, diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index 345fa3dd31c4a..6cad3fdd01167 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -35,9 +35,14 @@ async fn main() -> Result<(), WorkerError> { retry_policy_builder }; - let queue = PgQueue::new(config.queue_name.as_str(), &config.database_url) - .await - .expect("failed to initialize queue"); + let queue = PgQueue::new( + config.queue_name.as_str(), + &config.database_url, + config.max_pg_connections, + "hook-worker", + ) + .await + .expect("failed to initialize queue"); let worker = WebhookWorker::new( &config.worker_name, From 615c61d9f617028ae6ab1fd3f34d40536f886363 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Mon, 5 Feb 2024 10:31:56 -0700 Subject: [PATCH 200/249] Add proper e2e histrogram based on metadata created_at (#64) --- Cargo.lock | 1 + hook-api/Cargo.toml | 1 + hook-api/src/handlers/webhook.rs | 11 ++++--- hook-common/src/kafka_messages/mod.rs | 9 ++++-- hook-common/src/webhook.rs | 7 +++++ hook-janitor/src/fixtures/webhook_cleanup.sql | 30 +++++++++---------- hook-janitor/src/webhooks.rs | 2 ++ hook-worker/src/worker.rs | 8 +++++ 8 files changed, 47 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17b608c879f05..836810ef1c5e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,7 @@ name = "hook-api" version = "0.1.0" dependencies = [ "axum", + "chrono", "envconfig", "eyre", "hook-common", diff --git a/hook-api/Cargo.toml b/hook-api/Cargo.toml index 96c897cd3ab1d..5e3530ef15bde 100644 --- a/hook-api/Cargo.toml +++ b/hook-api/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" axum = { workspace = true } envconfig = { workspace = true } eyre = { workspace = true } +chrono = { workspace = true } hook-common = { path = "../hook-common" } http-body-util = { workspace = true } metrics = { workspace = true } diff --git a/hook-api/src/handlers/webhook.rs b/hook-api/src/handlers/webhook.rs index 16ebc6dc57179..3712aa2882d7c 100644 --- a/hook-api/src/handlers/webhook.rs +++ b/hook-api/src/handlers/webhook.rs @@ -1,13 +1,12 @@ use std::time::Instant; use axum::{extract::State, http::StatusCode, Json}; -use hook_common::webhook::{WebhookJobMetadata, WebhookJobParameters}; -use serde_derive::Deserialize; -use url::Url; - use hook_common::pgqueue::{NewJob, PgQueue}; +use hook_common::webhook::{WebhookJobMetadata, WebhookJobParameters}; use serde::Serialize; +use serde_derive::Deserialize; use tracing::{debug, error}; +use url::Url; const MAX_BODY_SIZE: usize = 1_000_000; @@ -116,6 +115,7 @@ mod tests { http::{self, Request, StatusCode}, Router, }; + use chrono::Utc; use hook_common::pgqueue::PgQueue; use hook_common::webhook::{HttpMethod, WebhookJobParameters}; use http_body_util::BodyExt; @@ -153,6 +153,7 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, + created_at: Utc::now(), }, max_attempts: 1, }) @@ -195,6 +196,7 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, + created_at: Utc::now(), }, max_attempts: 1, }) @@ -283,6 +285,7 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, + created_at: Utc::now(), }, max_attempts: 1, }) diff --git a/hook-common/src/kafka_messages/mod.rs b/hook-common/src/kafka_messages/mod.rs index f548563af5ba1..a1de9d5301bce 100644 --- a/hook-common/src/kafka_messages/mod.rs +++ b/hook-common/src/kafka_messages/mod.rs @@ -16,9 +16,12 @@ where D: Deserializer<'de>, { let formatted: String = Deserialize::deserialize(deserializer)?; - let datetime = match NaiveDateTime::parse_from_str(&formatted, "%Y-%m-%d %H:%M:%S") { - Ok(d) => d.and_utc(), - Err(_) => return Err(serde::de::Error::custom("Invalid datetime format")), + let datetime = match DateTime::parse_from_rfc3339(&formatted) { + Ok(d) => d.with_timezone(&Utc), + Err(_) => match NaiveDateTime::parse_from_str(&formatted, "%Y-%m-%d %H:%M:%S") { + Ok(d) => d.and_utc(), + Err(_) => return Err(serde::de::Error::custom("Invalid datetime format")), + }, }; Ok(datetime) diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 11e02856703eb..4122c20f68527 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -3,9 +3,11 @@ use std::convert::From; use std::fmt; use std::str::FromStr; +use chrono::{DateTime, Utc}; use serde::{de::Visitor, Deserialize, Serialize}; use crate::kafka_messages::app_metrics; +use crate::kafka_messages::{deserialize_datetime, serialize_datetime}; use crate::pgqueue::PgQueueError; /// Supported HTTP methods for webhooks. @@ -135,6 +137,11 @@ pub struct WebhookJobMetadata { pub team_id: u32, pub plugin_id: i32, pub plugin_config_id: i32, + #[serde( + serialize_with = "serialize_datetime", + deserialize_with = "deserialize_datetime" + )] + pub created_at: DateTime, } /// An error originating during a Webhook Job invocation. diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql index 5dfa8272ef168..5f2f6c11dc67c 100644 --- a/hook-janitor/src/fixtures/webhook_cleanup.sql +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -13,7 +13,7 @@ VALUES -- team:1, plugin_config:2, completed in hour 20 ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -24,7 +24,7 @@ VALUES -- team:1, plugin_config:2, completed in hour 20 (purposeful duplicate) ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -35,7 +35,7 @@ VALUES -- team:1, plugin_config:2, completed in hour 21 (different hour) ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 21:01:18.799371+00', '2023-12-19 21:01:18.799371+00', '{}', @@ -46,7 +46,7 @@ VALUES -- team:1, plugin_config:3, completed in hour 20 (different plugin_config) ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.80335+00', '2023-12-19 20:01:18.80335+00', '{}', @@ -57,7 +57,7 @@ VALUES -- team:1, plugin_config:2, completed but in a different queue ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -68,7 +68,7 @@ VALUES -- team:2, plugin_config:4, completed in hour 20 (different team) ( NULL, - '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', + '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -79,7 +79,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 20 ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -90,7 +90,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 20 (purposeful duplicate) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -101,7 +101,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 20 (different error) ( ARRAY ['{"type":"ConnectionError","details":{"error":{"name":"Connection Error"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -112,7 +112,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 21 (different hour) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 21:01:18.799371+00', '2023-12-19 21:01:18.799371+00', '{}', @@ -123,7 +123,7 @@ VALUES -- team:1, plugin_config:3, failed in hour 20 (different plugin_config) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -134,7 +134,7 @@ VALUES -- team:1, plugin_config:2, failed but in a different queue ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -145,7 +145,7 @@ VALUES -- team:2, plugin_config:4, failed in hour 20 (purposeful duplicate) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', + '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -156,7 +156,7 @@ VALUES -- team:1, plugin_config:2, available ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{"body": "hello world", "headers": {}, "method": "POST", "url": "https://myhost/endpoint"}', @@ -167,7 +167,7 @@ VALUES -- team:1, plugin_config:2, running ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', '2023-12-19 20:01:18.799371+00', now() - '1 hour' :: interval, '{}', diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 5cdf4318f69b0..c1390a7e0e1f7 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -892,6 +892,7 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, + created_at: Utc::now(), }; let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); queue.enqueue(new_job).await.expect("failed to enqueue job"); @@ -918,6 +919,7 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, + created_at: Utc::now(), }; let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); queue.enqueue(new_job).await.expect("failed to enqueue job"); diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index c526c3f7cce1c..14edea8b1275c 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -2,6 +2,7 @@ use std::collections; use std::sync::Arc; use std::time; +use chrono::Utc; use hook_common::health::HealthHandle; use hook_common::{ pgqueue::{Job, PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, @@ -264,6 +265,10 @@ async fn process_webhook_job( match send_result { Ok(_) => { + let end_to_end_duration = Utc::now() - webhook_job.metadata().created_at; + metrics::histogram!("webhook_jobs_end_to_end_duration_seconds", &labels) + .record((end_to_end_duration.num_milliseconds() as f64) / 1_000_f64); + webhook_job .complete() .await @@ -450,6 +455,8 @@ mod tests { // This is due to a long-standing cargo bug that reports imports and helper functions as unused. // See: https://github.com/rust-lang/rust/issues/46379. #[allow(unused_imports)] + use chrono::Utc; + #[allow(unused_imports)] use hook_common::health::HealthRegistry; #[allow(unused_imports)] use hook_common::pgqueue::{JobStatus, NewJob}; @@ -523,6 +530,7 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, + created_at: Utc::now(), }; let registry = HealthRegistry::new("liveness"); let liveness = registry From 304852cf2b92626e382a43af5076ca4e9a225729 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Mon, 5 Feb 2024 11:48:49 -0700 Subject: [PATCH 201/249] Revert "Add proper e2e histrogram based on metadata created_at (#64)" (#66) --- Cargo.lock | 1 - hook-api/Cargo.toml | 1 - hook-api/src/handlers/webhook.rs | 11 +++---- hook-common/src/kafka_messages/mod.rs | 9 ++---- hook-common/src/webhook.rs | 7 ----- hook-janitor/src/fixtures/webhook_cleanup.sql | 30 +++++++++---------- hook-janitor/src/webhooks.rs | 2 -- hook-worker/src/worker.rs | 8 ----- 8 files changed, 22 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 836810ef1c5e3..17b608c879f05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,7 +768,6 @@ name = "hook-api" version = "0.1.0" dependencies = [ "axum", - "chrono", "envconfig", "eyre", "hook-common", diff --git a/hook-api/Cargo.toml b/hook-api/Cargo.toml index 5e3530ef15bde..96c897cd3ab1d 100644 --- a/hook-api/Cargo.toml +++ b/hook-api/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" axum = { workspace = true } envconfig = { workspace = true } eyre = { workspace = true } -chrono = { workspace = true } hook-common = { path = "../hook-common" } http-body-util = { workspace = true } metrics = { workspace = true } diff --git a/hook-api/src/handlers/webhook.rs b/hook-api/src/handlers/webhook.rs index 3712aa2882d7c..16ebc6dc57179 100644 --- a/hook-api/src/handlers/webhook.rs +++ b/hook-api/src/handlers/webhook.rs @@ -1,13 +1,14 @@ use std::time::Instant; use axum::{extract::State, http::StatusCode, Json}; -use hook_common::pgqueue::{NewJob, PgQueue}; use hook_common::webhook::{WebhookJobMetadata, WebhookJobParameters}; -use serde::Serialize; use serde_derive::Deserialize; -use tracing::{debug, error}; use url::Url; +use hook_common::pgqueue::{NewJob, PgQueue}; +use serde::Serialize; +use tracing::{debug, error}; + const MAX_BODY_SIZE: usize = 1_000_000; #[derive(Serialize, Deserialize)] @@ -115,7 +116,6 @@ mod tests { http::{self, Request, StatusCode}, Router, }; - use chrono::Utc; use hook_common::pgqueue::PgQueue; use hook_common::webhook::{HttpMethod, WebhookJobParameters}; use http_body_util::BodyExt; @@ -153,7 +153,6 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, - created_at: Utc::now(), }, max_attempts: 1, }) @@ -196,7 +195,6 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, - created_at: Utc::now(), }, max_attempts: 1, }) @@ -285,7 +283,6 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, - created_at: Utc::now(), }, max_attempts: 1, }) diff --git a/hook-common/src/kafka_messages/mod.rs b/hook-common/src/kafka_messages/mod.rs index a1de9d5301bce..f548563af5ba1 100644 --- a/hook-common/src/kafka_messages/mod.rs +++ b/hook-common/src/kafka_messages/mod.rs @@ -16,12 +16,9 @@ where D: Deserializer<'de>, { let formatted: String = Deserialize::deserialize(deserializer)?; - let datetime = match DateTime::parse_from_rfc3339(&formatted) { - Ok(d) => d.with_timezone(&Utc), - Err(_) => match NaiveDateTime::parse_from_str(&formatted, "%Y-%m-%d %H:%M:%S") { - Ok(d) => d.and_utc(), - Err(_) => return Err(serde::de::Error::custom("Invalid datetime format")), - }, + let datetime = match NaiveDateTime::parse_from_str(&formatted, "%Y-%m-%d %H:%M:%S") { + Ok(d) => d.and_utc(), + Err(_) => return Err(serde::de::Error::custom("Invalid datetime format")), }; Ok(datetime) diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 4122c20f68527..11e02856703eb 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -3,11 +3,9 @@ use std::convert::From; use std::fmt; use std::str::FromStr; -use chrono::{DateTime, Utc}; use serde::{de::Visitor, Deserialize, Serialize}; use crate::kafka_messages::app_metrics; -use crate::kafka_messages::{deserialize_datetime, serialize_datetime}; use crate::pgqueue::PgQueueError; /// Supported HTTP methods for webhooks. @@ -137,11 +135,6 @@ pub struct WebhookJobMetadata { pub team_id: u32, pub plugin_id: i32, pub plugin_config_id: i32, - #[serde( - serialize_with = "serialize_datetime", - deserialize_with = "deserialize_datetime" - )] - pub created_at: DateTime, } /// An error originating during a Webhook Job invocation. diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql index 5f2f6c11dc67c..5dfa8272ef168 100644 --- a/hook-janitor/src/fixtures/webhook_cleanup.sql +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -13,7 +13,7 @@ VALUES -- team:1, plugin_config:2, completed in hour 20 ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -24,7 +24,7 @@ VALUES -- team:1, plugin_config:2, completed in hour 20 (purposeful duplicate) ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -35,7 +35,7 @@ VALUES -- team:1, plugin_config:2, completed in hour 21 (different hour) ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 21:01:18.799371+00', '2023-12-19 21:01:18.799371+00', '{}', @@ -46,7 +46,7 @@ VALUES -- team:1, plugin_config:3, completed in hour 20 (different plugin_config) ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', '2023-12-19 20:01:18.80335+00', '2023-12-19 20:01:18.80335+00', '{}', @@ -57,7 +57,7 @@ VALUES -- team:1, plugin_config:2, completed but in a different queue ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -68,7 +68,7 @@ VALUES -- team:2, plugin_config:4, completed in hour 20 (different team) ( NULL, - '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -79,7 +79,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 20 ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -90,7 +90,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 20 (purposeful duplicate) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -101,7 +101,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 20 (different error) ( ARRAY ['{"type":"ConnectionError","details":{"error":{"name":"Connection Error"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -112,7 +112,7 @@ VALUES -- team:1, plugin_config:2, failed in hour 21 (different hour) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 21:01:18.799371+00', '2023-12-19 21:01:18.799371+00', '{}', @@ -123,7 +123,7 @@ VALUES -- team:1, plugin_config:3, failed in hour 20 (different plugin_config) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 3}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -134,7 +134,7 @@ VALUES -- team:1, plugin_config:2, failed but in a different queue ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -145,7 +145,7 @@ VALUES -- team:2, plugin_config:4, failed in hour 20 (purposeful duplicate) ( ARRAY ['{"type":"TimeoutError","details":{"error":{"name":"Timeout"}}}'::jsonb], - '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 2, "plugin_id": 99, "plugin_config_id": 4}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{}', @@ -156,7 +156,7 @@ VALUES -- team:1, plugin_config:2, available ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', '2023-12-19 20:01:18.799371+00', '{"body": "hello world", "headers": {}, "method": "POST", "url": "https://myhost/endpoint"}', @@ -167,7 +167,7 @@ VALUES -- team:1, plugin_config:2, running ( NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2, "created_at": "2023-02-05T16:35:06.650Z"}', + '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', '2023-12-19 20:01:18.799371+00', now() - '1 hour' :: interval, '{}', diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index c1390a7e0e1f7..5cdf4318f69b0 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -892,7 +892,6 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, - created_at: Utc::now(), }; let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); queue.enqueue(new_job).await.expect("failed to enqueue job"); @@ -919,7 +918,6 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, - created_at: Utc::now(), }; let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); queue.enqueue(new_job).await.expect("failed to enqueue job"); diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 14edea8b1275c..c526c3f7cce1c 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -2,7 +2,6 @@ use std::collections; use std::sync::Arc; use std::time; -use chrono::Utc; use hook_common::health::HealthHandle; use hook_common::{ pgqueue::{Job, PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, @@ -265,10 +264,6 @@ async fn process_webhook_job( match send_result { Ok(_) => { - let end_to_end_duration = Utc::now() - webhook_job.metadata().created_at; - metrics::histogram!("webhook_jobs_end_to_end_duration_seconds", &labels) - .record((end_to_end_duration.num_milliseconds() as f64) / 1_000_f64); - webhook_job .complete() .await @@ -455,8 +450,6 @@ mod tests { // This is due to a long-standing cargo bug that reports imports and helper functions as unused. // See: https://github.com/rust-lang/rust/issues/46379. #[allow(unused_imports)] - use chrono::Utc; - #[allow(unused_imports)] use hook_common::health::HealthRegistry; #[allow(unused_imports)] use hook_common::pgqueue::{JobStatus, NewJob}; @@ -530,7 +523,6 @@ mod tests { team_id: 1, plugin_id: 2, plugin_config_id: 3, - created_at: Utc::now(), }; let registry = HealthRegistry::new("liveness"); let liveness = registry From 54bf761a35333d2ffb915922d2abf52bdac89f3f Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 6 Feb 2024 08:27:34 -0700 Subject: [PATCH 202/249] Dequeue multiple items at a time (#60) --- hook-common/src/pgqueue.rs | 428 ++++++++++++++++++++++++++--------- hook-janitor/src/webhooks.rs | 14 +- hook-worker/src/config.rs | 3 + hook-worker/src/main.rs | 1 + hook-worker/src/worker.rs | 175 +++++++++----- 5 files changed, 453 insertions(+), 168 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 4dab9183421a4..af91fbd1cf7ca 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -1,14 +1,17 @@ //! # PgQueue //! //! A job queue implementation backed by a PostgreSQL table. -use std::str::FromStr; use std::time; +use std::{str::FromStr, sync::Arc}; use async_trait::async_trait; use chrono; use serde; +use sqlx::postgres::any::AnyConnectionBackend; use sqlx::postgres::{PgConnectOptions, PgPool, PgPoolOptions}; use thiserror::Error; +use tokio::sync::Mutex; +use tracing::error; /// Enumeration of errors for operations with PgQueue. /// Errors that can originate from sqlx and are wrapped by us to provide additional context. @@ -24,16 +27,22 @@ pub enum PgQueueError { ParseJobStatusError(String), #[error("{0} is not a valid HttpMethod")] ParseHttpMethodError(String), + #[error("transaction was already closed")] + TransactionAlreadyClosedError, } #[derive(Error, Debug)] pub enum PgJobError { #[error("retry is an invalid state for this PgJob: {error}")] RetryInvalidError { job: T, error: String }, + #[error("connection failed with: {error}")] + ConnectionError { error: sqlx::Error }, #[error("{command} query failed with: {error}")] QueryError { command: String, error: sqlx::Error }, #[error("transaction {command} failed with: {error}")] TransactionError { command: String, error: sqlx::Error }, + #[error("transaction was already closed")] + TransactionAlreadyClosedError, } /// Enumeration of possible statuses for a Job. @@ -217,20 +226,39 @@ pub trait PgQueueJob { #[derive(Debug)] pub struct PgJob { pub job: Job, - pub connection: sqlx::pool::PoolConnection, + pub pool: PgPool, +} + +// Container struct for a batch of PgJobs. +pub struct PgBatch { + pub jobs: Vec>, +} + +impl PgJob { + async fn acquire_conn( + &mut self, + ) -> Result, PgJobError>>> + { + self.pool + .acquire() + .await + .map_err(|error| PgJobError::ConnectionError { error }) + } } #[async_trait] impl PgQueueJob for PgJob { async fn complete(mut self) -> Result>>> { - let completed_job = self - .job - .complete(&mut *self.connection) - .await - .map_err(|error| PgJobError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; + let mut connection = self.acquire_conn().await?; + + let completed_job = + self.job + .complete(&mut *connection) + .await + .map_err(|error| PgJobError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; Ok(completed_job) } @@ -239,9 +267,11 @@ impl PgQueueJob for PgJob { mut self, error: E, ) -> Result, PgJobError>>> { + let mut connection = self.acquire_conn().await?; + let failed_job = self .job - .fail(error, &mut *self.connection) + .fail(error, &mut *connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -264,11 +294,13 @@ impl PgQueueJob for PgJob { }); } + let mut connection = self.acquire_conn().await?; + let retried_job = self .job .retryable() .queue(queue) - .retry(error, retry_interval, &mut *self.connection) + .retry(error, retry_interval, &mut *connection) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), @@ -284,7 +316,39 @@ impl PgQueueJob for PgJob { #[derive(Debug)] pub struct PgTransactionJob<'c, J, M> { pub job: Job, - pub transaction: sqlx::Transaction<'c, sqlx::postgres::Postgres>, + + /// The open transaction this job came from. If multiple jobs were queried at once, then this + /// transaction will be shared between them (across async tasks and threads as necessary). See + /// below for more information. + shared_txn: Arc>>>, +} + +// Container struct for a batch of PgTransactionJob. Includes a reference to the shared transaction +// for committing the work when all of the jobs are finished. +pub struct PgTransactionBatch<'c, J, M> { + pub jobs: Vec>, + + /// The open transaction the jobs in the Vec came from. This should be used to commit or + /// rollback when all of the work is finished. + shared_txn: Arc>>>, +} + +impl<'c, J, M> PgTransactionBatch<'_, J, M> { + pub async fn commit(self) -> PgQueueResult<()> { + let mut txn_guard = self.shared_txn.lock().await; + + txn_guard + .as_deref_mut() + .ok_or(PgQueueError::TransactionAlreadyClosedError)? + .commit() + .await + .map_err(|e| PgQueueError::QueryError { + command: "COMMIT".to_owned(), + error: e, + })?; + + Ok(()) + } } #[async_trait] @@ -292,22 +356,20 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio async fn complete( mut self, ) -> Result>>> { - let completed_job = self - .job - .complete(&mut *self.transaction) - .await - .map_err(|error| PgJobError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; + let mut txn_guard = self.shared_txn.lock().await; - self.transaction - .commit() - .await - .map_err(|error| PgJobError::TransactionError { - command: "COMMIT".to_owned(), - error, - })?; + let txn_ref = txn_guard + .as_deref_mut() + .ok_or(PgJobError::TransactionAlreadyClosedError)?; + + let completed_job = + self.job + .complete(txn_ref) + .await + .map_err(|error| PgJobError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; Ok(completed_job) } @@ -316,22 +378,20 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio mut self, error: S, ) -> Result, PgJobError>>> { - let failed_job = self - .job - .fail(error, &mut *self.transaction) - .await - .map_err(|error| PgJobError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; + let mut txn_guard = self.shared_txn.lock().await; - self.transaction - .commit() - .await - .map_err(|error| PgJobError::TransactionError { - command: "COMMIT".to_owned(), - error, - })?; + let txn_ref = txn_guard + .as_deref_mut() + .ok_or(PgJobError::TransactionAlreadyClosedError)?; + + let failed_job = + self.job + .fail(error, txn_ref) + .await + .map_err(|error| PgJobError::QueryError { + command: "UPDATE".to_owned(), + error, + })?; Ok(failed_job) } @@ -351,25 +411,23 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio }); } + let mut txn_guard = self.shared_txn.lock().await; + + let txn_ref = txn_guard + .as_deref_mut() + .ok_or(PgJobError::TransactionAlreadyClosedError)?; + let retried_job = self .job .retryable() .queue(queue) - .retry(error, retry_interval, &mut *self.transaction) + .retry(error, retry_interval, txn_ref) .await .map_err(|error| PgJobError::QueryError { command: "UPDATE".to_owned(), error, })?; - self.transaction - .commit() - .await - .map_err(|error| PgJobError::TransactionError { - command: "COMMIT".to_owned(), - error, - })?; - Ok(retried_job) } } @@ -553,15 +611,16 @@ impl PgQueue { Ok(Self { name, pool }) } - /// Dequeue a `Job` from this `PgQueue`. - /// The `Job` will be updated to `'running'` status, so any other `dequeue` calls will skip it. + /// Dequeue up to `limit` `Job`s from this `PgQueue`. + /// The `Job`s will be updated to `'running'` status, so any other `dequeue` calls will skip it. pub async fn dequeue< J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, M: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, >( &self, attempted_by: &str, - ) -> PgQueueResult>> { + limit: u32, + ) -> PgQueueResult>> { let mut connection = self .pool .acquire() @@ -583,7 +642,7 @@ WITH available_in_queue AS ( ORDER BY attempt, scheduled_at - LIMIT 1 + LIMIT $2 FOR UPDATE SKIP LOCKED ) UPDATE @@ -592,7 +651,7 @@ SET attempted_at = NOW(), status = 'running'::job_status, attempt = attempt + 1, - attempted_by = array_append(attempted_by, $2::text) + attempted_by = array_append(attempted_by, $3::text) FROM available_in_queue WHERE @@ -601,14 +660,29 @@ RETURNING job_queue.* "#; - let query_result: Result, sqlx::Error> = sqlx::query_as(base_query) + let query_result: Result>, sqlx::Error> = sqlx::query_as(base_query) .bind(&self.name) + .bind(limit as i64) .bind(attempted_by) - .fetch_one(&mut *connection) + .fetch_all(&mut *connection) .await; match query_result { - Ok(job) => Ok(Some(PgJob { job, connection })), + Ok(jobs) => { + if jobs.is_empty() { + return Ok(None); + } + + let pg_jobs: Vec> = jobs + .into_iter() + .map(|job| PgJob { + job, + pool: self.pool.clone(), + }) + .collect(); + + Ok(Some(PgBatch { jobs: pg_jobs })) + } // Although connection would be closed once it goes out of scope, sqlx recommends explicitly calling close(). // See: https://docs.rs/sqlx/latest/sqlx/postgres/any/trait.AnyConnectionBackend.html#tymethod.close. @@ -616,6 +690,7 @@ RETURNING let _ = connection.close().await; Ok(None) } + Err(e) => { let _ = connection.close().await; Err(PgQueueError::QueryError { @@ -626,9 +701,10 @@ RETURNING } } - /// Dequeue a `Job` from this `PgQueue` and hold the transaction. - /// Any other `dequeue_tx` calls will skip rows locked, so by holding a transaction we ensure only one worker can dequeue a job. - /// Holding a transaction open can have performance implications, but it means no `'running'` state is required. + /// Dequeue up to `limit` `Job`s from this `PgQueue` and hold the transaction. Any other + /// `dequeue_tx` calls will skip rows locked, so by holding a transaction we ensure only one + /// worker can dequeue a job. Holding a transaction open can have performance implications, but + /// it means no `'running'` state is required. pub async fn dequeue_tx< 'a, J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, @@ -636,7 +712,8 @@ RETURNING >( &self, attempted_by: &str, - ) -> PgQueueResult>> { + limit: u32, + ) -> PgQueueResult>> { let mut tx = self .pool .begin() @@ -658,7 +735,7 @@ WITH available_in_queue AS ( ORDER BY attempt, scheduled_at - LIMIT 1 + LIMIT $2 FOR UPDATE SKIP LOCKED ) UPDATE @@ -667,7 +744,7 @@ SET attempted_at = NOW(), status = 'running'::job_status, attempt = attempt + 1, - attempted_by = array_append(attempted_by, $2::text) + attempted_by = array_append(attempted_by, $3::text) FROM available_in_queue WHERE @@ -676,20 +753,38 @@ RETURNING job_queue.* "#; - let query_result: Result, sqlx::Error> = sqlx::query_as(base_query) + let query_result: Result>, sqlx::Error> = sqlx::query_as(base_query) .bind(&self.name) + .bind(limit as i64) .bind(attempted_by) - .fetch_one(&mut *tx) + .fetch_all(&mut *tx) .await; match query_result { - Ok(job) => Ok(Some(PgTransactionJob { - job, - transaction: tx, - })), + Ok(jobs) => { + if jobs.is_empty() { + return Ok(None); + } + + let shared_txn = Arc::new(Mutex::new(Some(tx))); + + let pg_jobs: Vec> = jobs + .into_iter() + .map(|job| PgTransactionJob { + job, + shared_txn: shared_txn.clone(), + }) + .collect(); + + Ok(Some(PgTransactionBatch { + jobs: pg_jobs, + shared_txn: shared_txn.clone(), + })) + } - // Transaction is rolledback on drop. + // Transaction is rolled back on drop. Err(sqlx::Error::RowNotFound) => Ok(None), + Err(e) => Err(PgQueueError::QueryError { command: "UPDATE".to_owned(), error: e, @@ -736,7 +831,7 @@ mod tests { use super::*; use crate::retry::RetryPolicy; - #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug, Clone)] struct JobMetadata { team_id: u32, plugin_config_id: i32, @@ -753,7 +848,7 @@ mod tests { } } - #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug, Clone)] struct JobParameters { method: String, body: String, @@ -795,10 +890,13 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let pg_job: PgJob = queue - .dequeue(&worker_id) + .dequeue(&worker_id, 1) .await - .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); + .expect("failed to dequeue jobs") + .expect("didn't find any jobs to dequeue") + .jobs + .pop() + .unwrap(); assert_eq!(pg_job.job.attempt, 1); assert!(pg_job.job.attempted_by.contains(&worker_id)); @@ -816,12 +914,62 @@ mod tests { .await .expect("failed to connect to local test postgresql database"); - let pg_job: Option> = queue - .dequeue(&worker_id) + let pg_jobs: Option> = queue + .dequeue(&worker_id, 1) .await - .expect("failed to dequeue job"); + .expect("failed to dequeue jobs"); + + assert!(pg_jobs.is_none()); + } + + #[sqlx::test(migrations = "../migrations")] + async fn test_can_dequeue_multiple_jobs(db: PgPool) { + let job_target = job_target(); + let job_metadata = JobMetadata::default(); + let job_parameters = JobParameters::default(); + let worker_id = worker_id(); - assert!(pg_job.is_none()); + let queue = PgQueue::new_from_pool("test_can_dequeue_multiple_jobs", db) + .await + .expect("failed to connect to local test postgresql database"); + + for _ in 0..5 { + queue + .enqueue(NewJob::new( + 1, + job_metadata.clone(), + job_parameters.clone(), + &job_target, + )) + .await + .expect("failed to enqueue job"); + } + + // Only get 4 jobs, leaving one in the queue. + let limit = 4; + let batch: PgBatch = queue + .dequeue(&worker_id, limit) + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + + // Complete those 4. + assert_eq!(batch.jobs.len(), limit as usize); + for job in batch.jobs { + job.complete().await.expect("failed to complete job"); + } + + // Try to get up to 4 jobs, but only 1 remains. + let batch: PgBatch = queue + .dequeue(&worker_id, limit) + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + + assert_eq!(batch.jobs.len(), 1); // Only one job should have been left in the queue. + for job in batch.jobs { + job.complete().await.expect("failed to complete job"); + } } #[sqlx::test(migrations = "../migrations")] @@ -830,19 +978,21 @@ mod tests { let job_metadata = JobMetadata::default(); let job_parameters = JobParameters::default(); let worker_id = worker_id(); - let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); let queue = PgQueue::new_from_pool("test_can_dequeue_tx_job", db) .await .expect("failed to connect to local test postgresql database"); + let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let tx_job: PgTransactionJob<'_, JobParameters, JobMetadata> = queue - .dequeue_tx(&worker_id) + let mut batch: PgTransactionBatch<'_, JobParameters, JobMetadata> = queue + .dequeue_tx(&worker_id, 1) .await - .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); + .expect("failed to dequeue jobs") + .expect("didn't find any jobs to dequeue"); + + let tx_job = batch.jobs.pop().unwrap(); assert_eq!(tx_job.job.attempt, 1); assert!(tx_job.job.attempted_by.contains(&worker_id)); @@ -852,6 +1002,65 @@ mod tests { assert_eq!(*tx_job.job.parameters.as_ref(), JobParameters::default()); assert_eq!(tx_job.job.status, JobStatus::Running); assert_eq!(tx_job.job.target, job_target); + + // Transactional jobs must be completed, failed or retried before being dropped. This is + // to prevent logic bugs when using the shared txn. + tx_job.complete().await.expect("failed to complete job"); + + batch.commit().await.expect("failed to commit transaction"); + } + + #[sqlx::test(migrations = "../migrations")] + async fn test_can_dequeue_multiple_tx_jobs(db: PgPool) { + let job_target = job_target(); + let job_metadata = JobMetadata::default(); + let job_parameters = JobParameters::default(); + let worker_id = worker_id(); + + let queue = PgQueue::new_from_pool("test_can_dequeue_multiple_tx_jobs", db) + .await + .expect("failed to connect to local test postgresql database"); + + for _ in 0..5 { + queue + .enqueue(NewJob::new( + 1, + job_metadata.clone(), + job_parameters.clone(), + &job_target, + )) + .await + .expect("failed to enqueue job"); + } + + // Only get 4 jobs, leaving one in the queue. + let limit = 4; + let mut batch: PgTransactionBatch<'_, JobParameters, JobMetadata> = queue + .dequeue_tx(&worker_id, limit) + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + + assert_eq!(batch.jobs.len(), limit as usize); + + // Complete those 4 and commit. + for job in std::mem::take(&mut batch.jobs) { + job.complete().await.expect("failed to complete job"); + } + batch.commit().await.expect("failed to commit transaction"); + + // Try to get up to 4 jobs, but only 1 remains. + let mut batch: PgTransactionBatch<'_, JobParameters, JobMetadata> = queue + .dequeue_tx(&worker_id, limit) + .await + .expect("failed to dequeue job") + .expect("didn't find a job to dequeue"); + assert_eq!(batch.jobs.len(), 1); // Only one job should have been left in the queue. + + for job in std::mem::take(&mut batch.jobs) { + job.complete().await.expect("failed to complete job"); + } + batch.commit().await.expect("failed to commit transaction"); } #[sqlx::test(migrations = "../migrations")] @@ -861,12 +1070,12 @@ mod tests { .await .expect("failed to connect to local test postgresql database"); - let tx_job: Option> = queue - .dequeue_tx(&worker_id) + let batch: Option> = queue + .dequeue_tx(&worker_id, 1) .await .expect("failed to dequeue job"); - assert!(tx_job.is_none()); + assert!(batch.is_none()); } #[sqlx::test(migrations = "../migrations")] @@ -888,10 +1097,13 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let job: PgJob = queue - .dequeue(&worker_id) + .dequeue(&worker_id, 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); + .expect("didn't find a job to dequeue") + .jobs + .pop() + .unwrap(); let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); @@ -905,10 +1117,13 @@ mod tests { .expect("failed to retry job"); let retried_job: PgJob = queue - .dequeue(&worker_id) + .dequeue(&worker_id, 1) .await .expect("failed to dequeue job") - .expect("didn't find retried job to dequeue"); + .expect("didn't find retried job to dequeue") + .jobs + .pop() + .unwrap(); assert_eq!(retried_job.job.attempt, 2); assert!(retried_job.job.attempted_by.contains(&worker_id)); @@ -942,10 +1157,13 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let job: PgJob = queue - .dequeue(&worker_id) + .dequeue(&worker_id, 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); + .expect("didn't find a job to dequeue") + .jobs + .pop() + .unwrap(); let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); @@ -958,8 +1176,8 @@ mod tests { .await .expect("failed to retry job"); - let retried_job_not_found: Option> = queue - .dequeue(&worker_id) + let retried_job_not_found: Option> = queue + .dequeue(&worker_id, 1) .await .expect("failed to dequeue job"); @@ -970,10 +1188,13 @@ mod tests { .expect("failed to connect to retry queue in local test postgresql database"); let retried_job: PgJob = queue - .dequeue(&worker_id) + .dequeue(&worker_id, 1) .await .expect("failed to dequeue job") - .expect("job not found in retry queue"); + .expect("job not found in retry queue") + .jobs + .pop() + .unwrap(); assert_eq!(retried_job.job.attempt, 2); assert!(retried_job.job.attempted_by.contains(&worker_id)); @@ -1004,10 +1225,13 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); let job: PgJob = queue - .dequeue(&worker_id) + .dequeue(&worker_id, 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); + .expect("didn't find a job to dequeue") + .jobs + .pop() + .unwrap(); let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 5cdf4318f69b0..e3b137c343155 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -870,10 +870,13 @@ mod tests { { // The fixtures include an available job, so let's complete it while the txn is open. let webhook_job: PgJob = queue - .dequeue(&"worker_id") + .dequeue(&"worker_id", 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); + .expect("didn't find a job to dequeue") + .jobs + .pop() + .unwrap(); webhook_job .complete() .await @@ -896,10 +899,13 @@ mod tests { let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); queue.enqueue(new_job).await.expect("failed to enqueue job"); let webhook_job: PgJob = queue - .dequeue(&"worker_id") + .dequeue(&"worker_id", 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); + .expect("didn't find a job to dequeue") + .jobs + .pop() + .unwrap(); webhook_job .complete() .await diff --git a/hook-worker/src/config.rs b/hook-worker/src/config.rs index 477ff74349242..32e49f7e9b49c 100644 --- a/hook-worker/src/config.rs +++ b/hook-worker/src/config.rs @@ -37,6 +37,9 @@ pub struct Config { #[envconfig(default = "true")] pub transactional: bool, + + #[envconfig(default = "1")] + pub dequeue_batch_size: u32, } impl Config { diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index 6cad3fdd01167..fede7d288d95f 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -47,6 +47,7 @@ async fn main() -> Result<(), WorkerError> { let worker = WebhookWorker::new( &config.worker_name, &queue, + config.dequeue_batch_size, config.poll_interval.0, config.request_timeout.0, config.max_concurrent_jobs, diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index c526c3f7cce1c..437a1d352f32c 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -2,7 +2,9 @@ use std::collections; use std::sync::Arc; use std::time; +use futures::future::join_all; use hook_common::health::HealthHandle; +use hook_common::pgqueue::{PgBatch, PgTransactionBatch}; use hook_common::{ pgqueue::{Job, PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, retry::RetryPolicy, @@ -68,6 +70,8 @@ pub struct WebhookWorker<'p> { name: String, /// The queue we will be dequeuing jobs from. queue: &'p PgQueue, + /// The maximum number of jobs to dequeue in one query. + dequeue_batch_size: u32, /// The interval for polling the queue. poll_interval: time::Duration, /// The client used for HTTP requests. @@ -81,9 +85,11 @@ pub struct WebhookWorker<'p> { } impl<'p> WebhookWorker<'p> { + #[allow(clippy::too_many_arguments)] pub fn new( name: &str, queue: &'p PgQueue, + dequeue_batch_size: u32, poll_interval: time::Duration, request_timeout: time::Duration, max_concurrent_jobs: usize, @@ -106,6 +112,7 @@ impl<'p> WebhookWorker<'p> { Self { name: name.to_owned(), queue, + dequeue_batch_size, poll_interval, client, max_concurrent_jobs, @@ -114,16 +121,20 @@ impl<'p> WebhookWorker<'p> { } } - /// Wait until a job becomes available in our queue. - async fn wait_for_job<'a>(&self) -> PgJob { + /// Wait until at least one job becomes available in our queue. + async fn wait_for_jobs<'a>(&self) -> PgBatch { let mut interval = tokio::time::interval(self.poll_interval); loop { interval.tick().await; self.liveness.report_healthy().await; - match self.queue.dequeue(&self.name).await { - Ok(Some(job)) => return job, + match self + .queue + .dequeue(&self.name, self.dequeue_batch_size) + .await + { + Ok(Some(batch)) => return batch, Ok(None) => continue, Err(error) => { error!("error while trying to dequeue job: {}", error); @@ -133,18 +144,22 @@ impl<'p> WebhookWorker<'p> { } } - /// Wait until a job becomes available in our queue in transactional mode. - async fn wait_for_job_tx<'a>( + /// Wait until at least one job becomes available in our queue in transactional mode. + async fn wait_for_jobs_tx<'a>( &self, - ) -> PgTransactionJob<'a, WebhookJobParameters, WebhookJobMetadata> { + ) -> PgTransactionBatch<'a, WebhookJobParameters, WebhookJobMetadata> { let mut interval = tokio::time::interval(self.poll_interval); loop { interval.tick().await; self.liveness.report_healthy().await; - match self.queue.dequeue_tx(&self.name).await { - Ok(Some(job)) => return job, + match self + .queue + .dequeue_tx(&self.name, self.dequeue_batch_size) + .await + { + Ok(Some(batch)) => return batch, Ok(None) => continue, Err(error) => { error!("error while trying to dequeue_tx job: {}", error); @@ -162,70 +177,104 @@ impl<'p> WebhookWorker<'p> { .set(1f64 - semaphore.available_permits() as f64 / self.max_concurrent_jobs as f64); }; + let dequeue_batch_size_histogram = metrics::histogram!("webhook_dequeue_batch_size"); + if transactional { loop { report_semaphore_utilization(); - let webhook_job = self.wait_for_job_tx().await; - spawn_webhook_job_processing_task( - self.client.clone(), - semaphore.clone(), - self.retry_policy.clone(), - webhook_job, - ) - .await; + // TODO: We could grab semaphore permits here using something like: + // `min(semaphore.available_permits(), dequeue_batch_size)` + // And then dequeue only up to that many jobs. We'd then need to hand back the + // difference in permits based on how many jobs were dequeued. + let mut batch = self.wait_for_jobs_tx().await; + dequeue_batch_size_histogram.record(batch.jobs.len() as f64); + + // Get enough permits for the jobs before spawning a task. + let permits = semaphore + .clone() + .acquire_many_owned(batch.jobs.len() as u32) + .await + .expect("semaphore has been closed"); + + let client = self.client.clone(); + let retry_policy = self.retry_policy.clone(); + + tokio::spawn(async move { + let mut futures = Vec::new(); + + // We have to `take` the Vec of jobs from the batch to avoid a borrow checker + // error below when we commit. + for job in std::mem::take(&mut batch.jobs) { + let client = client.clone(); + let retry_policy = retry_policy.clone(); + + let future = + async move { process_webhook_job(client, job, &retry_policy).await }; + + futures.push(future); + } + + let results = join_all(futures).await; + for result in results { + if let Err(e) = result { + error!("error processing webhook job: {}", e); + } + } + + let _ = batch.commit().await.map_err(|e| { + error!("error committing transactional batch: {}", e); + }); + + drop(permits); + }); } } else { loop { report_semaphore_utilization(); - let webhook_job = self.wait_for_job().await; - spawn_webhook_job_processing_task( - self.client.clone(), - semaphore.clone(), - self.retry_policy.clone(), - webhook_job, - ) - .await; + // TODO: We could grab semaphore permits here using something like: + // `min(semaphore.available_permits(), dequeue_batch_size)` + // And then dequeue only up to that many jobs. We'd then need to hand back the + // difference in permits based on how many jobs were dequeued. + let batch = self.wait_for_jobs().await; + dequeue_batch_size_histogram.record(batch.jobs.len() as f64); + + // Get enough permits for the jobs before spawning a task. + let permits = semaphore + .clone() + .acquire_many_owned(batch.jobs.len() as u32) + .await + .expect("semaphore has been closed"); + + let client = self.client.clone(); + let retry_policy = self.retry_policy.clone(); + + tokio::spawn(async move { + let mut futures = Vec::new(); + + for job in batch.jobs { + let client = client.clone(); + let retry_policy = retry_policy.clone(); + + let future = + async move { process_webhook_job(client, job, &retry_policy).await }; + + futures.push(future); + } + + let results = join_all(futures).await; + for result in results { + if let Err(e) = result { + error!("error processing webhook job: {}", e); + } + } + + drop(permits); + }); } } } } -/// Spawn a Tokio task to process a Webhook Job once we successfully acquire a permit. -/// -/// # Arguments -/// -/// * `client`: An HTTP client to execute the webhook job request. -/// * `semaphore`: A semaphore used for rate limiting purposes. This function will panic if this semaphore is closed. -/// * `retry_policy`: The retry policy used to set retry parameters if a job fails and has remaining attempts. -/// * `webhook_job`: The webhook job to process as dequeued from `hook_common::pgqueue::PgQueue`. -async fn spawn_webhook_job_processing_task( - client: reqwest::Client, - semaphore: Arc, - retry_policy: RetryPolicy, - webhook_job: W, -) -> tokio::task::JoinHandle> { - let permit = semaphore - .acquire_owned() - .await - .expect("semaphore has been closed"); - - let labels = [("queue", webhook_job.queue())]; - - metrics::counter!("webhook_jobs_total", &labels).increment(1); - - tokio::spawn(async move { - let result = process_webhook_job(client, webhook_job, &retry_policy).await; - drop(permit); - match result { - Ok(_) => Ok(()), - Err(error) => { - error!("failed to process webhook job: {}", error); - Err(error) - } - } - }) -} - /// Process a webhook job by transitioning it to its appropriate state after its request is sent. /// After we finish, the webhook job will be set as completed (if the request was successful), retryable (if the request /// was unsuccessful but we can still attempt a retry), or failed (if the request was unsuccessful and no more retries @@ -248,6 +297,7 @@ async fn process_webhook_job( let parameters = webhook_job.parameters(); let labels = [("queue", webhook_job.queue())]; + metrics::counter!("webhook_jobs_total", &labels).increment(1); let now = tokio::time::Instant::now(); @@ -543,6 +593,7 @@ mod tests { let worker = WebhookWorker::new( &worker_id, &queue, + 1, time::Duration::from_millis(100), time::Duration::from_millis(5000), 10, @@ -550,7 +601,7 @@ mod tests { liveness, ); - let consumed_job = worker.wait_for_job().await; + let consumed_job = worker.wait_for_jobs().await.jobs.pop().unwrap(); assert_eq!(consumed_job.job.attempt, 1); assert!(consumed_job.job.attempted_by.contains(&worker_id)); From 26672aeb2c8d114e3f82c41916e935a3d753ee8a Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 6 Feb 2024 08:36:10 -0700 Subject: [PATCH 203/249] Remove non-transactional mode (#65) --- hook-common/src/pgqueue.rs | 321 +----------------- hook-janitor/src/fixtures/webhook_cleanup.sql | 11 - hook-janitor/src/webhooks.rs | 78 +---- hook-worker/src/config.rs | 3 - hook-worker/src/main.rs | 2 +- hook-worker/src/worker.rs | 173 +++------- 6 files changed, 77 insertions(+), 511 deletions(-) diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index af91fbd1cf7ca..4a8b489716a7d 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -60,8 +60,6 @@ pub enum JobStatus { Discarded, /// A job that was unsuccessfully completed by a worker. Failed, - /// A job that was picked up by a worker and it's currentlly being run. - Running, } /// Allow casting JobStatus from strings. @@ -73,7 +71,6 @@ impl FromStr for JobStatus { "available" => Ok(JobStatus::Available), "completed" => Ok(JobStatus::Completed), "failed" => Ok(JobStatus::Failed), - "running" => Ok(JobStatus::Running), invalid => Err(PgQueueError::ParseJobStatusError(invalid.to_owned())), } } @@ -222,95 +219,6 @@ pub trait PgQueueJob { ) -> Result>>; } -/// A Job that can be updated in PostgreSQL. -#[derive(Debug)] -pub struct PgJob { - pub job: Job, - pub pool: PgPool, -} - -// Container struct for a batch of PgJobs. -pub struct PgBatch { - pub jobs: Vec>, -} - -impl PgJob { - async fn acquire_conn( - &mut self, - ) -> Result, PgJobError>>> - { - self.pool - .acquire() - .await - .map_err(|error| PgJobError::ConnectionError { error }) - } -} - -#[async_trait] -impl PgQueueJob for PgJob { - async fn complete(mut self) -> Result>>> { - let mut connection = self.acquire_conn().await?; - - let completed_job = - self.job - .complete(&mut *connection) - .await - .map_err(|error| PgJobError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; - - Ok(completed_job) - } - - async fn fail( - mut self, - error: E, - ) -> Result, PgJobError>>> { - let mut connection = self.acquire_conn().await?; - - let failed_job = self - .job - .fail(error, &mut *connection) - .await - .map_err(|error| PgJobError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; - - Ok(failed_job) - } - - async fn retry( - mut self, - error: E, - retry_interval: time::Duration, - queue: &str, - ) -> Result>>> { - if self.job.is_gte_max_attempts() { - return Err(PgJobError::RetryInvalidError { - job: Box::new(self), - error: "Maximum attempts reached".to_owned(), - }); - } - - let mut connection = self.acquire_conn().await?; - - let retried_job = self - .job - .retryable() - .queue(queue) - .retry(error, retry_interval, &mut *connection) - .await - .map_err(|error| PgJobError::QueryError { - command: "UPDATE".to_owned(), - error, - })?; - - Ok(retried_job) - } -} - /// A Job within an open PostgreSQL transaction. /// This implementation allows 'hiding' the job from any other workers running SKIP LOCKED queries. #[derive(Debug)] @@ -611,96 +519,6 @@ impl PgQueue { Ok(Self { name, pool }) } - /// Dequeue up to `limit` `Job`s from this `PgQueue`. - /// The `Job`s will be updated to `'running'` status, so any other `dequeue` calls will skip it. - pub async fn dequeue< - J: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, - M: for<'d> serde::Deserialize<'d> + std::marker::Send + std::marker::Unpin + 'static, - >( - &self, - attempted_by: &str, - limit: u32, - ) -> PgQueueResult>> { - let mut connection = self - .pool - .acquire() - .await - .map_err(|error| PgQueueError::ConnectionError { error })?; - - // The query that follows uses a FOR UPDATE SKIP LOCKED clause. - // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. - let base_query = r#" -WITH available_in_queue AS ( - SELECT - id - FROM - job_queue - WHERE - status = 'available' - AND scheduled_at <= NOW() - AND queue = $1 - ORDER BY - attempt, - scheduled_at - LIMIT $2 - FOR UPDATE SKIP LOCKED -) -UPDATE - job_queue -SET - attempted_at = NOW(), - status = 'running'::job_status, - attempt = attempt + 1, - attempted_by = array_append(attempted_by, $3::text) -FROM - available_in_queue -WHERE - job_queue.id = available_in_queue.id -RETURNING - job_queue.* - "#; - - let query_result: Result>, sqlx::Error> = sqlx::query_as(base_query) - .bind(&self.name) - .bind(limit as i64) - .bind(attempted_by) - .fetch_all(&mut *connection) - .await; - - match query_result { - Ok(jobs) => { - if jobs.is_empty() { - return Ok(None); - } - - let pg_jobs: Vec> = jobs - .into_iter() - .map(|job| PgJob { - job, - pool: self.pool.clone(), - }) - .collect(); - - Ok(Some(PgBatch { jobs: pg_jobs })) - } - - // Although connection would be closed once it goes out of scope, sqlx recommends explicitly calling close(). - // See: https://docs.rs/sqlx/latest/sqlx/postgres/any/trait.AnyConnectionBackend.html#tymethod.close. - Err(sqlx::Error::RowNotFound) => { - let _ = connection.close().await; - Ok(None) - } - - Err(e) => { - let _ = connection.close().await; - Err(PgQueueError::QueryError { - command: "UPDATE".to_owned(), - error: e, - }) - } - } - } - /// Dequeue up to `limit` `Job`s from this `PgQueue` and hold the transaction. Any other /// `dequeue_tx` calls will skip rows locked, so by holding a transaction we ensure only one /// worker can dequeue a job. Holding a transaction open can have performance implications, but @@ -742,7 +560,6 @@ UPDATE job_queue SET attempted_at = NOW(), - status = 'running'::job_status, attempt = attempt + 1, attempted_by = array_append(attempted_by, $3::text) FROM @@ -875,103 +692,6 @@ mod tests { "https://myhost/endpoint".to_owned() } - #[sqlx::test(migrations = "../migrations")] - async fn test_can_dequeue_job(db: PgPool) { - let job_target = job_target(); - let job_parameters = JobParameters::default(); - let job_metadata = JobMetadata::default(); - let worker_id = worker_id(); - let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); - - let queue = PgQueue::new_from_pool("test_can_dequeue_job", db) - .await - .expect("failed to connect to local test postgresql database"); - - queue.enqueue(new_job).await.expect("failed to enqueue job"); - - let pg_job: PgJob = queue - .dequeue(&worker_id, 1) - .await - .expect("failed to dequeue jobs") - .expect("didn't find any jobs to dequeue") - .jobs - .pop() - .unwrap(); - - assert_eq!(pg_job.job.attempt, 1); - assert!(pg_job.job.attempted_by.contains(&worker_id)); - assert_eq!(pg_job.job.attempted_by.len(), 1); - assert_eq!(pg_job.job.max_attempts, 1); - assert_eq!(*pg_job.job.parameters.as_ref(), JobParameters::default()); - assert_eq!(pg_job.job.status, JobStatus::Running); - assert_eq!(pg_job.job.target, job_target); - } - - #[sqlx::test(migrations = "../migrations")] - async fn test_dequeue_returns_none_on_no_jobs(db: PgPool) { - let worker_id = worker_id(); - let queue = PgQueue::new_from_pool("test_dequeue_returns_none_on_no_jobs", db) - .await - .expect("failed to connect to local test postgresql database"); - - let pg_jobs: Option> = queue - .dequeue(&worker_id, 1) - .await - .expect("failed to dequeue jobs"); - - assert!(pg_jobs.is_none()); - } - - #[sqlx::test(migrations = "../migrations")] - async fn test_can_dequeue_multiple_jobs(db: PgPool) { - let job_target = job_target(); - let job_metadata = JobMetadata::default(); - let job_parameters = JobParameters::default(); - let worker_id = worker_id(); - - let queue = PgQueue::new_from_pool("test_can_dequeue_multiple_jobs", db) - .await - .expect("failed to connect to local test postgresql database"); - - for _ in 0..5 { - queue - .enqueue(NewJob::new( - 1, - job_metadata.clone(), - job_parameters.clone(), - &job_target, - )) - .await - .expect("failed to enqueue job"); - } - - // Only get 4 jobs, leaving one in the queue. - let limit = 4; - let batch: PgBatch = queue - .dequeue(&worker_id, limit) - .await - .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); - - // Complete those 4. - assert_eq!(batch.jobs.len(), limit as usize); - for job in batch.jobs { - job.complete().await.expect("failed to complete job"); - } - - // Try to get up to 4 jobs, but only 1 remains. - let batch: PgBatch = queue - .dequeue(&worker_id, limit) - .await - .expect("failed to dequeue job") - .expect("didn't find a job to dequeue"); - - assert_eq!(batch.jobs.len(), 1); // Only one job should have been left in the queue. - for job in batch.jobs { - job.complete().await.expect("failed to complete job"); - } - } - #[sqlx::test(migrations = "../migrations")] async fn test_can_dequeue_tx_job(db: PgPool) { let job_target = job_target(); @@ -1000,7 +720,6 @@ mod tests { assert_eq!(tx_job.job.max_attempts, 1); assert_eq!(*tx_job.job.metadata.as_ref(), JobMetadata::default()); assert_eq!(*tx_job.job.parameters.as_ref(), JobParameters::default()); - assert_eq!(tx_job.job.status, JobStatus::Running); assert_eq!(tx_job.job.target, job_target); // Transactional jobs must be completed, failed or retried before being dropped. This is @@ -1096,14 +815,12 @@ mod tests { .expect("failed to connect to local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: PgJob = queue - .dequeue(&worker_id, 1) + let mut batch: PgTransactionBatch<'_, JobParameters, JobMetadata> = queue + .dequeue_tx(&worker_id, 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue") - .jobs - .pop() - .unwrap(); + .expect("didn't find a job to dequeue"); + let job = batch.jobs.pop().unwrap(); let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); @@ -1115,9 +832,10 @@ mod tests { ) .await .expect("failed to retry job"); + batch.commit().await.expect("failed to commit transaction"); - let retried_job: PgJob = queue - .dequeue(&worker_id, 1) + let retried_job: PgTransactionJob = queue + .dequeue_tx(&worker_id, 1) .await .expect("failed to dequeue job") .expect("didn't find retried job to dequeue") @@ -1133,7 +851,6 @@ mod tests { *retried_job.job.parameters.as_ref(), JobParameters::default() ); - assert_eq!(retried_job.job.status, JobStatus::Running); assert_eq!(retried_job.job.target, job_target); } @@ -1156,14 +873,12 @@ mod tests { .expect("failed to connect to queue in local test postgresql database"); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: PgJob = queue - .dequeue(&worker_id, 1) + let mut batch: PgTransactionBatch = queue + .dequeue_tx(&worker_id, 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue") - .jobs - .pop() - .unwrap(); + .expect("didn't find a job to dequeue"); + let job = batch.jobs.pop().unwrap(); let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); @@ -1175,9 +890,10 @@ mod tests { ) .await .expect("failed to retry job"); + batch.commit().await.expect("failed to commit transaction"); - let retried_job_not_found: Option> = queue - .dequeue(&worker_id, 1) + let retried_job_not_found: Option> = queue + .dequeue_tx(&worker_id, 1) .await .expect("failed to dequeue job"); @@ -1187,8 +903,8 @@ mod tests { .await .expect("failed to connect to retry queue in local test postgresql database"); - let retried_job: PgJob = queue - .dequeue(&worker_id, 1) + let retried_job: PgTransactionJob = queue + .dequeue_tx(&worker_id, 1) .await .expect("failed to dequeue job") .expect("job not found in retry queue") @@ -1204,7 +920,6 @@ mod tests { *retried_job.job.parameters.as_ref(), JobParameters::default() ); - assert_eq!(retried_job.job.status, JobStatus::Running); assert_eq!(retried_job.job.target, job_target); } @@ -1224,8 +939,8 @@ mod tests { queue.enqueue(new_job).await.expect("failed to enqueue job"); - let job: PgJob = queue - .dequeue(&worker_id, 1) + let job: PgTransactionJob = queue + .dequeue_tx(&worker_id, 1) .await .expect("failed to dequeue job") .expect("didn't find a job to dequeue") diff --git a/hook-janitor/src/fixtures/webhook_cleanup.sql b/hook-janitor/src/fixtures/webhook_cleanup.sql index 5dfa8272ef168..e0b9a7a9ea4d8 100644 --- a/hook-janitor/src/fixtures/webhook_cleanup.sql +++ b/hook-janitor/src/fixtures/webhook_cleanup.sql @@ -163,15 +163,4 @@ VALUES 'webhooks', 'available', 'https://myhost/endpoint' - ), - -- team:1, plugin_config:2, running - ( - NULL, - '{"team_id": 1, "plugin_id": 99, "plugin_config_id": 2}', - '2023-12-19 20:01:18.799371+00', - now() - '1 hour' :: interval, - '{}', - 'webhooks', - 'running', - 'https://myhost/endpoint' ); \ No newline at end of file diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index e3b137c343155..7f7fadd408565 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -28,8 +28,6 @@ pub enum WebhookCleanerError { AcquireConnError { error: sqlx::Error }, #[error("failed to acquire conn and start txn: {error}")] StartTxnError { error: sqlx::Error }, - #[error("failed to reschedule stuck jobs: {error}")] - RescheduleStuckJobsError { error: sqlx::Error }, #[error("failed to get queue depth: {error}")] GetQueueDepthError { error: sqlx::Error }, #[error("failed to get row count: {error}")] @@ -145,7 +143,6 @@ impl From for AppMetric { struct SerializableTxn<'a>(Transaction<'a, Postgres>); struct CleanupStats { - jobs_unstuck_count: u64, rows_processed: u64, completed_row_count: u64, completed_agg_row_count: u64, @@ -186,45 +183,6 @@ impl WebhookCleaner { }) } - async fn reschedule_stuck_jobs(&self) -> Result { - let mut conn = self - .pg_pool - .acquire() - .await - .map_err(|e| WebhookCleanerError::AcquireConnError { error: e })?; - - // The "non-transactional" worker runs the risk of crashing and leaving jobs permanently in - // the `running` state. This query will reschedule any jobs that have been in the running - // state for more than 2 minutes (which is *much* longer than we expect any Webhook job to - // take). - // - // We don't need to increment the `attempt` counter here because the worker already did that - // when it moved the job into `running`. - // - // If the previous worker was somehow stalled for 2 minutes and completes the task, that - // will mean we sent duplicate Webhooks. Success stats should not be affected, since both - // will update the same job row, which will only be processed once by the janitor. - - let base_query = r#" - UPDATE - job_queue - SET - status = 'available'::job_status, - last_attempt_finished_at = NOW(), - scheduled_at = NOW() - WHERE - status = 'running'::job_status - AND attempted_at < NOW() - INTERVAL '2 minutes' - "#; - - let result = sqlx::query(base_query) - .execute(&mut *conn) - .await - .map_err(|e| WebhookCleanerError::RescheduleStuckJobsError { error: e })?; - - Ok(result.rows_affected()) - } - async fn get_queue_depth(&self) -> Result { let mut conn = self .pg_pool @@ -424,8 +382,6 @@ impl WebhookCleaner { let untried_status = [("status", "untried")]; let retries_status = [("status", "retries")]; - let jobs_unstuck_count = self.reschedule_stuck_jobs().await?; - let queue_depth = self.get_queue_depth().await?; metrics::gauge!("queue_depth_oldest_scheduled", &untried_status) .set(queue_depth.oldest_scheduled_at_untried.timestamp() as f64); @@ -479,7 +435,6 @@ impl WebhookCleaner { } Ok(CleanupStats { - jobs_unstuck_count, rows_processed: rows_deleted, completed_row_count, completed_agg_row_count, @@ -500,8 +455,6 @@ impl Cleaner for WebhookCleaner { metrics::counter!("webhook_cleanup_success",).increment(1); metrics::gauge!("webhook_cleanup_last_success_timestamp",) .set(get_current_timestamp_seconds()); - metrics::counter!("webhook_cleanup_jobs_unstuck") - .increment(stats.jobs_unstuck_count); if stats.rows_processed > 0 { let elapsed_time = start_time.elapsed().as_secs_f64(); @@ -546,7 +499,8 @@ mod tests { use hook_common::kafka_messages::app_metrics::{ Error as WebhookError, ErrorDetails, ErrorType, }; - use hook_common::pgqueue::{NewJob, PgJob, PgQueue, PgQueueJob}; + use hook_common::pgqueue::PgQueueJob; + use hook_common::pgqueue::{NewJob, PgQueue, PgTransactionBatch}; use hook_common::webhook::{HttpMethod, WebhookJobMetadata, WebhookJobParameters}; use rdkafka::consumer::{Consumer, StreamConsumer}; use rdkafka::mocking::MockCluster; @@ -624,9 +578,6 @@ mod tests { .await .expect("webbook cleanup_impl failed"); - // The one 'running' job is transitioned to 'available'. - assert_eq!(cleanup_stats.jobs_unstuck_count, 1); - // Rows that are not 'completed' or 'failed' should not be processed. assert_eq!(cleanup_stats.rows_processed, 13); @@ -821,7 +772,6 @@ mod tests { .expect("webbook cleanup_impl failed"); // Reported metrics are all zeroes - assert_eq!(cleanup_stats.jobs_unstuck_count, 0); assert_eq!(cleanup_stats.rows_processed, 0); assert_eq!(cleanup_stats.completed_row_count, 0); assert_eq!(cleanup_stats.completed_agg_row_count, 0); @@ -865,22 +815,20 @@ mod tests { assert_eq!(get_count_from_new_conn(&db, "completed").await, 6); assert_eq!(get_count_from_new_conn(&db, "failed").await, 7); assert_eq!(get_count_from_new_conn(&db, "available").await, 1); - assert_eq!(get_count_from_new_conn(&db, "running").await, 1); { // The fixtures include an available job, so let's complete it while the txn is open. - let webhook_job: PgJob = queue - .dequeue(&"worker_id", 1) + let mut batch: PgTransactionBatch<'_, WebhookJobParameters, WebhookJobMetadata> = queue + .dequeue_tx(&"worker_id", 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue") - .jobs - .pop() - .unwrap(); + .expect("didn't find a job to dequeue"); + let webhook_job = batch.jobs.pop().unwrap(); webhook_job .complete() .await .expect("failed to complete job"); + batch.commit().await.expect("failed to commit batch"); } { @@ -898,18 +846,17 @@ mod tests { }; let new_job = NewJob::new(1, job_metadata, job_parameters, &"target"); queue.enqueue(new_job).await.expect("failed to enqueue job"); - let webhook_job: PgJob = queue - .dequeue(&"worker_id", 1) + let mut batch: PgTransactionBatch<'_, WebhookJobParameters, WebhookJobMetadata> = queue + .dequeue_tx(&"worker_id", 1) .await .expect("failed to dequeue job") - .expect("didn't find a job to dequeue") - .jobs - .pop() - .unwrap(); + .expect("didn't find a job to dequeue"); + let webhook_job = batch.jobs.pop().unwrap(); webhook_job .complete() .await .expect("failed to complete job"); + batch.commit().await.expect("failed to commit batch"); } { @@ -950,6 +897,5 @@ mod tests { assert_eq!(get_count_from_new_conn(&db, "completed").await, 2); assert_eq!(get_count_from_new_conn(&db, "failed").await, 0); assert_eq!(get_count_from_new_conn(&db, "available").await, 1); - assert_eq!(get_count_from_new_conn(&db, "running").await, 1); } } diff --git a/hook-worker/src/config.rs b/hook-worker/src/config.rs index 32e49f7e9b49c..ceb690f38846e 100644 --- a/hook-worker/src/config.rs +++ b/hook-worker/src/config.rs @@ -35,9 +35,6 @@ pub struct Config { #[envconfig(nested = true)] pub retry_policy: RetryPolicyConfig, - #[envconfig(default = "true")] - pub transactional: bool, - #[envconfig(default = "1")] pub dequeue_batch_size: u32, } diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index fede7d288d95f..2997dfc65ff50 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -67,7 +67,7 @@ async fn main() -> Result<(), WorkerError> { .expect("failed to start serving metrics"); }); - worker.run(config.transactional).await; + worker.run().await; Ok(()) } diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 437a1d352f32c..b83c909cb698e 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -4,9 +4,9 @@ use std::time; use futures::future::join_all; use hook_common::health::HealthHandle; -use hook_common::pgqueue::{PgBatch, PgTransactionBatch}; +use hook_common::pgqueue::PgTransactionBatch; use hook_common::{ - pgqueue::{Job, PgJob, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, + pgqueue::{Job, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, retry::RetryPolicy, webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}, }; @@ -50,20 +50,6 @@ impl WebhookJob for PgTransactionJob<'_, WebhookJobParameters, WebhookJobMetadat } } -impl WebhookJob for PgJob { - fn parameters(&self) -> &WebhookJobParameters { - &self.job.parameters - } - - fn metadata(&self) -> &WebhookJobMetadata { - &self.job.metadata - } - - fn job(&self) -> &Job { - &self.job - } -} - /// A worker to poll `PgQueue` and spawn tasks to process webhooks when a job becomes available. pub struct WebhookWorker<'p> { /// An identifier for this worker. Used to mark jobs we have consumed. @@ -121,29 +107,6 @@ impl<'p> WebhookWorker<'p> { } } - /// Wait until at least one job becomes available in our queue. - async fn wait_for_jobs<'a>(&self) -> PgBatch { - let mut interval = tokio::time::interval(self.poll_interval); - - loop { - interval.tick().await; - self.liveness.report_healthy().await; - - match self - .queue - .dequeue(&self.name, self.dequeue_batch_size) - .await - { - Ok(Some(batch)) => return batch, - Ok(None) => continue, - Err(error) => { - error!("error while trying to dequeue job: {}", error); - continue; - } - } - } - } - /// Wait until at least one job becomes available in our queue in transactional mode. async fn wait_for_jobs_tx<'a>( &self, @@ -170,7 +133,7 @@ impl<'p> WebhookWorker<'p> { } /// Run this worker to continuously process any jobs that become available. - pub async fn run(&self, transactional: bool) { + pub async fn run(&self) { let semaphore = Arc::new(sync::Semaphore::new(self.max_concurrent_jobs)); let report_semaphore_utilization = || { metrics::gauge!("webhook_worker_saturation_percent") @@ -179,98 +142,53 @@ impl<'p> WebhookWorker<'p> { let dequeue_batch_size_histogram = metrics::histogram!("webhook_dequeue_batch_size"); - if transactional { - loop { - report_semaphore_utilization(); - // TODO: We could grab semaphore permits here using something like: - // `min(semaphore.available_permits(), dequeue_batch_size)` - // And then dequeue only up to that many jobs. We'd then need to hand back the - // difference in permits based on how many jobs were dequeued. - let mut batch = self.wait_for_jobs_tx().await; - dequeue_batch_size_histogram.record(batch.jobs.len() as f64); - - // Get enough permits for the jobs before spawning a task. - let permits = semaphore - .clone() - .acquire_many_owned(batch.jobs.len() as u32) - .await - .expect("semaphore has been closed"); - - let client = self.client.clone(); - let retry_policy = self.retry_policy.clone(); - - tokio::spawn(async move { - let mut futures = Vec::new(); - - // We have to `take` the Vec of jobs from the batch to avoid a borrow checker - // error below when we commit. - for job in std::mem::take(&mut batch.jobs) { - let client = client.clone(); - let retry_policy = retry_policy.clone(); - - let future = - async move { process_webhook_job(client, job, &retry_policy).await }; - - futures.push(future); - } + loop { + report_semaphore_utilization(); + // TODO: We could grab semaphore permits here using something like: + // `min(semaphore.available_permits(), dequeue_batch_size)` + // And then dequeue only up to that many jobs. We'd then need to hand back the + // difference in permits based on how many jobs were dequeued. + let mut batch = self.wait_for_jobs_tx().await; + dequeue_batch_size_histogram.record(batch.jobs.len() as f64); + + // Get enough permits for the jobs before spawning a task. + let permits = semaphore + .clone() + .acquire_many_owned(batch.jobs.len() as u32) + .await + .expect("semaphore has been closed"); - let results = join_all(futures).await; - for result in results { - if let Err(e) = result { - error!("error processing webhook job: {}", e); - } - } + let client = self.client.clone(); + let retry_policy = self.retry_policy.clone(); - let _ = batch.commit().await.map_err(|e| { - error!("error committing transactional batch: {}", e); - }); + tokio::spawn(async move { + let mut futures = Vec::new(); - drop(permits); - }); - } - } else { - loop { - report_semaphore_utilization(); - // TODO: We could grab semaphore permits here using something like: - // `min(semaphore.available_permits(), dequeue_batch_size)` - // And then dequeue only up to that many jobs. We'd then need to hand back the - // difference in permits based on how many jobs were dequeued. - let batch = self.wait_for_jobs().await; - dequeue_batch_size_histogram.record(batch.jobs.len() as f64); - - // Get enough permits for the jobs before spawning a task. - let permits = semaphore - .clone() - .acquire_many_owned(batch.jobs.len() as u32) - .await - .expect("semaphore has been closed"); - - let client = self.client.clone(); - let retry_policy = self.retry_policy.clone(); - - tokio::spawn(async move { - let mut futures = Vec::new(); - - for job in batch.jobs { - let client = client.clone(); - let retry_policy = retry_policy.clone(); - - let future = - async move { process_webhook_job(client, job, &retry_policy).await }; - - futures.push(future); - } + // We have to `take` the Vec of jobs from the batch to avoid a borrow checker + // error below when we commit. + for job in std::mem::take(&mut batch.jobs) { + let client = client.clone(); + let retry_policy = retry_policy.clone(); + + let future = + async move { process_webhook_job(client, job, &retry_policy).await }; - let results = join_all(futures).await; - for result in results { - if let Err(e) = result { - error!("error processing webhook job: {}", e); - } + futures.push(future); + } + + let results = join_all(futures).await; + for result in results { + if let Err(e) = result { + error!("error processing webhook job: {}", e); } + } - drop(permits); + let _ = batch.commit().await.map_err(|e| { + error!("error committing transactional batch: {}", e); }); - } + + drop(permits); + }); } } } @@ -601,7 +519,8 @@ mod tests { liveness, ); - let consumed_job = worker.wait_for_jobs().await.jobs.pop().unwrap(); + let mut batch = worker.wait_for_jobs_tx().await; + let consumed_job = batch.jobs.pop().unwrap(); assert_eq!(consumed_job.job.attempt, 1); assert!(consumed_job.job.attempted_by.contains(&worker_id)); @@ -611,13 +530,13 @@ mod tests { *consumed_job.job.parameters.as_ref(), webhook_job_parameters ); - assert_eq!(consumed_job.job.status, JobStatus::Running); assert_eq!(consumed_job.job.target, webhook_job_parameters.url); consumed_job .complete() .await .expect("job not successfully completed"); + batch.commit().await.expect("failed to commit batch"); assert!(registry.get_status().healthy) } From bf7cccf26e42c421c2b984f22ba5737024e35e49 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 6 Feb 2024 10:32:13 -0700 Subject: [PATCH 204/249] Sync workflow style/naming --- .github/workflows/docker-capture.yml | 12 ++++++------ .github/workflows/docker-hook-api.yml | 2 +- .github/workflows/docker-hook-janitor.yml | 2 +- .github/workflows/docker-hook-worker.yml | 2 +- .github/workflows/rust.yml | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index 5bc5100b79e5f..4b4c1cd2a98eb 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -1,4 +1,4 @@ -name: Build docker image +name: Build capture docker image on: workflow_dispatch: @@ -37,7 +37,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/${{ github.repository }} + images: ghcr.io/posthog/capture tags: | type=ref,event=pr type=ref,event=branch @@ -49,8 +49,8 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Build and push - id: docker_build + - name: Build and push capture + id: docker_build_capture uses: docker/build-push-action@v4 with: context: ./ @@ -64,5 +64,5 @@ jobs: cache-to: type=gha,mode=max build-args: RUST_BACKTRACE=1 BIN=capture-server - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} + - name: Capture image digest + run: echo ${{ steps.docker_build_capture.outputs.digest }} diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 2cafd628a8765..0057d6cece253 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -62,7 +62,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: BIN=hook-api + build-args: RUST_BACKTRACE=1 BIN=hook-api - name: Hook-api image digest run: echo ${{ steps.docker_build_hook_api.outputs.digest }} diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index fc662bd8f38b1..32903baa44465 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -62,7 +62,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: BIN=hook-janitor + build-args: RUST_BACKTRACE=1 BIN=hook-janitor - name: Hook-janitor image digest run: echo ${{ steps.docker_build_hook_janitor.outputs.digest }} diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 77db4a7c18beb..05a1054f73805 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -62,7 +62,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: BIN=hook-worker + build-args: RUST_BACKTRACE=1 BIN=hook-worker - name: Hook-worker image digest run: echo ${{ steps.docker_build_hook_worker.outputs.digest }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ea3be4ec632e9..7ee5eb83a4c90 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -61,8 +61,8 @@ jobs: with: path: | ~/.cargo/registry - ~/.cargo/git - target + ~/.cargo/git + target key: ${ runner.os }-cargo-debug-${{ hashFiles('**/Cargo.lock') }} - name: Run cargo test From 5c64ef0b73a88836946f7d315825d7739485a035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Tue, 6 Feb 2024 18:13:27 +0100 Subject: [PATCH 205/249] fix: Bump failure counter on job error (#36) --- hook-api/src/handlers/app.rs | 4 +- hook-api/src/handlers/webhook.rs | 20 ++--- hook-common/src/pgqueue.rs | 124 +++++++++++++++---------------- hook-common/src/webhook.rs | 6 +- hook-janitor/src/webhooks.rs | 4 +- hook-worker/src/error.rs | 8 +- hook-worker/src/worker.rs | 52 ++++++++----- 7 files changed, 110 insertions(+), 108 deletions(-) diff --git a/hook-api/src/handlers/app.rs b/hook-api/src/handlers/app.rs index 7b1e840094473..f8d4b24695b99 100644 --- a/hook-api/src/handlers/app.rs +++ b/hook-api/src/handlers/app.rs @@ -30,9 +30,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn index(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", db) - .await - .expect("failed to construct pg_queue"); + let pg_queue = PgQueue::new_from_pool("test_index", db).await; let app = add_routes(Router::new(), pg_queue); diff --git a/hook-api/src/handlers/webhook.rs b/hook-api/src/handlers/webhook.rs index 16ebc6dc57179..e50b8b0241a51 100644 --- a/hook-api/src/handlers/webhook.rs +++ b/hook-api/src/handlers/webhook.rs @@ -127,9 +127,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_success(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", db) - .await - .expect("failed to construct pg_queue"); + let pg_queue = PgQueue::new_from_pool("test_index", db).await; let app = add_routes(Router::new(), pg_queue); @@ -171,9 +169,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_bad_url(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", db) - .await - .expect("failed to construct pg_queue"); + let pg_queue = PgQueue::new_from_pool("test_index", db).await; let app = add_routes(Router::new(), pg_queue); @@ -210,9 +206,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_payload_missing_fields(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", db) - .await - .expect("failed to construct pg_queue"); + let pg_queue = PgQueue::new_from_pool("test_index", db).await; let app = add_routes(Router::new(), pg_queue); @@ -233,9 +227,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_payload_not_json(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", db) - .await - .expect("failed to construct pg_queue"); + let pg_queue = PgQueue::new_from_pool("test_index", db).await; let app = add_routes(Router::new(), pg_queue); @@ -256,9 +248,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn webhook_payload_body_too_large(db: PgPool) { - let pg_queue = PgQueue::new_from_pool("test_index", db) - .await - .expect("failed to construct pg_queue"); + let pg_queue = PgQueue::new_from_pool("test_index", db).await; let app = add_routes(Router::new(), pg_queue); diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index 4a8b489716a7d..f155506123b35 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -13,16 +13,9 @@ use thiserror::Error; use tokio::sync::Mutex; use tracing::error; -/// Enumeration of errors for operations with PgQueue. -/// Errors that can originate from sqlx and are wrapped by us to provide additional context. +/// Enumeration of parsing errors in PgQueue. #[derive(Error, Debug)] -pub enum PgQueueError { - #[error("pool creation failed with: {error}")] - PoolCreationError { error: sqlx::Error }, - #[error("connection failed with: {error}")] - ConnectionError { error: sqlx::Error }, - #[error("{command} query failed with: {error}")] - QueryError { command: String, error: sqlx::Error }, +pub enum ParseError { #[error("{0} is not a valid JobStatus")] ParseJobStatusError(String), #[error("{0} is not a valid HttpMethod")] @@ -31,10 +24,12 @@ pub enum PgQueueError { TransactionAlreadyClosedError, } +/// Enumeration of database-related errors in PgQueue. +/// Errors that can originate from sqlx and are wrapped by us to provide additional context. #[derive(Error, Debug)] -pub enum PgJobError { - #[error("retry is an invalid state for this PgJob: {error}")] - RetryInvalidError { job: T, error: String }, +pub enum DatabaseError { + #[error("pool creation failed with: {error}")] + PoolCreationError { error: sqlx::Error }, #[error("connection failed with: {error}")] ConnectionError { error: sqlx::Error }, #[error("{command} query failed with: {error}")] @@ -45,6 +40,25 @@ pub enum PgJobError { TransactionAlreadyClosedError, } +/// An error that occurs when a job cannot be retried. +/// Returns the underlying job so that a client can fail it. +#[derive(Error, Debug)] +#[error("retry is an invalid state for this job: {error}")] +pub struct RetryInvalidError { + pub job: T, + pub error: String, +} + +/// Enumeration of errors that can occur when retrying a job. +/// They are in a separate enum a failed retry could be returning the underlying job. +#[derive(Error, Debug)] +pub enum RetryError { + #[error(transparent)] + DatabaseError(#[from] DatabaseError), + #[error(transparent)] + RetryInvalidError(#[from] RetryInvalidError), +} + /// Enumeration of possible statuses for a Job. #[derive(Debug, PartialEq, sqlx::Type)] #[sqlx(type_name = "job_status")] @@ -64,14 +78,14 @@ pub enum JobStatus { /// Allow casting JobStatus from strings. impl FromStr for JobStatus { - type Err = PgQueueError; + type Err = ParseError; fn from_str(s: &str) -> Result { match s { "available" => Ok(JobStatus::Available), "completed" => Ok(JobStatus::Completed), "failed" => Ok(JobStatus::Failed), - invalid => Err(PgQueueError::ParseJobStatusError(invalid.to_owned())), + invalid => Err(ParseError::ParseJobStatusError(invalid.to_owned())), } } } @@ -204,19 +218,19 @@ RETURNING #[async_trait] pub trait PgQueueJob { - async fn complete(mut self) -> Result>>; + async fn complete(mut self) -> Result; async fn fail( mut self, error: E, - ) -> Result, PgJobError>>; + ) -> Result, DatabaseError>; async fn retry( mut self, error: E, retry_interval: time::Duration, queue: &str, - ) -> Result>>; + ) -> Result>>; } /// A Job within an open PostgreSQL transaction. @@ -247,10 +261,10 @@ impl<'c, J, M> PgTransactionBatch<'_, J, M> { txn_guard .as_deref_mut() - .ok_or(PgQueueError::TransactionAlreadyClosedError)? + .ok_or(DatabaseError::TransactionAlreadyClosedError)? .commit() .await - .map_err(|e| PgQueueError::QueryError { + .map_err(|e| DatabaseError::QueryError { command: "COMMIT".to_owned(), error: e, })?; @@ -261,20 +275,18 @@ impl<'c, J, M> PgTransactionBatch<'_, J, M> { #[async_trait] impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactionJob<'c, J, M> { - async fn complete( - mut self, - ) -> Result>>> { + async fn complete(mut self) -> Result { let mut txn_guard = self.shared_txn.lock().await; let txn_ref = txn_guard .as_deref_mut() - .ok_or(PgJobError::TransactionAlreadyClosedError)?; + .ok_or(DatabaseError::TransactionAlreadyClosedError)?; let completed_job = self.job .complete(txn_ref) .await - .map_err(|error| PgJobError::QueryError { + .map_err(|error| DatabaseError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -285,18 +297,18 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio async fn fail( mut self, error: S, - ) -> Result, PgJobError>>> { + ) -> Result, DatabaseError> { let mut txn_guard = self.shared_txn.lock().await; let txn_ref = txn_guard .as_deref_mut() - .ok_or(PgJobError::TransactionAlreadyClosedError)?; + .ok_or(DatabaseError::TransactionAlreadyClosedError)?; let failed_job = self.job .fail(error, txn_ref) .await - .map_err(|error| PgJobError::QueryError { + .map_err(|error| DatabaseError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -309,21 +321,21 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio error: E, retry_interval: time::Duration, queue: &str, - ) -> Result>>> { + ) -> Result>>> { // Ideally, the transition to RetryableJob should be fallible. // But taking ownership of self when we return this error makes things difficult. if self.job.is_gte_max_attempts() { - return Err(PgJobError::RetryInvalidError { + return Err(RetryError::from(RetryInvalidError { job: Box::new(self), error: "Maximum attempts reached".to_owned(), - }); + })); } let mut txn_guard = self.shared_txn.lock().await; let txn_ref = txn_guard .as_deref_mut() - .ok_or(PgJobError::TransactionAlreadyClosedError)?; + .ok_or(DatabaseError::TransactionAlreadyClosedError)?; let retried_job = self .job @@ -331,7 +343,7 @@ impl<'c, J: std::marker::Send, M: std::marker::Send> PgQueueJob for PgTransactio .queue(queue) .retry(error, retry_interval, txn_ref) .await - .map_err(|error| PgJobError::QueryError { + .map_err(|error| DatabaseError::QueryError { command: "UPDATE".to_owned(), error, })?; @@ -481,7 +493,7 @@ pub struct PgQueue { pool: PgPool, } -pub type PgQueueResult = std::result::Result; +pub type PgQueueResult = std::result::Result; impl PgQueue { /// Initialize a new PgQueue backed by table in PostgreSQL by intializing a connection pool to the database in `url`. @@ -498,7 +510,7 @@ impl PgQueue { ) -> PgQueueResult { let name = queue_name.to_owned(); let options = PgConnectOptions::from_str(url) - .map_err(|error| PgQueueError::PoolCreationError { error })? + .map_err(|error| DatabaseError::PoolCreationError { error })? .application_name(app_name); let pool = PgPoolOptions::new() .max_connections(max_connections) @@ -513,14 +525,14 @@ impl PgQueue { /// /// * `queue_name`: A name for the queue we are going to initialize. /// * `pool`: A database connection pool to be used by this queue. - pub async fn new_from_pool(queue_name: &str, pool: PgPool) -> PgQueueResult { + pub async fn new_from_pool(queue_name: &str, pool: PgPool) -> PgQueue { let name = queue_name.to_owned(); - Ok(Self { name, pool }) + Self { name, pool } } - /// Dequeue up to `limit` `Job`s from this `PgQueue` and hold the transaction. Any other - /// `dequeue_tx` calls will skip rows locked, so by holding a transaction we ensure only one + /// Dequeue up to `limit` `Job`s from this `PgQueue` and hold the transaction. + /// Any other `dequeue_tx` calls will skip rows locked, so by holding a transaction we ensure only one /// worker can dequeue a job. Holding a transaction open can have performance implications, but /// it means no `'running'` state is required. pub async fn dequeue_tx< @@ -536,7 +548,7 @@ impl PgQueue { .pool .begin() .await - .map_err(|error| PgQueueError::ConnectionError { error })?; + .map_err(|error| DatabaseError::ConnectionError { error })?; // The query that follows uses a FOR UPDATE SKIP LOCKED clause. // For more details on this see: 2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5. @@ -601,8 +613,7 @@ RETURNING // Transaction is rolled back on drop. Err(sqlx::Error::RowNotFound) => Ok(None), - - Err(e) => Err(PgQueueError::QueryError { + Err(e) => Err(DatabaseError::QueryError { command: "UPDATE".to_owned(), error: e, }), @@ -634,7 +645,7 @@ VALUES .bind(&job.target) .execute(&self.pool) .await - .map_err(|error| PgQueueError::QueryError { + .map_err(|error| DatabaseError::QueryError { command: "INSERT".to_owned(), error, })?; @@ -699,9 +710,7 @@ mod tests { let job_parameters = JobParameters::default(); let worker_id = worker_id(); - let queue = PgQueue::new_from_pool("test_can_dequeue_tx_job", db) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new_from_pool("test_can_dequeue_tx_job", db).await; let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); queue.enqueue(new_job).await.expect("failed to enqueue job"); @@ -736,9 +745,7 @@ mod tests { let job_parameters = JobParameters::default(); let worker_id = worker_id(); - let queue = PgQueue::new_from_pool("test_can_dequeue_multiple_tx_jobs", db) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new_from_pool("test_can_dequeue_multiple_tx_jobs", db).await; for _ in 0..5 { queue @@ -785,9 +792,7 @@ mod tests { #[sqlx::test(migrations = "../migrations")] async fn test_dequeue_tx_returns_none_on_no_jobs(db: PgPool) { let worker_id = worker_id(); - let queue = PgQueue::new_from_pool("test_dequeue_tx_returns_none_on_no_jobs", db) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new_from_pool("test_dequeue_tx_returns_none_on_no_jobs", db).await; let batch: Option> = queue .dequeue_tx(&worker_id, 1) @@ -810,9 +815,7 @@ mod tests { .queue(&queue_name) .provide(); - let queue = PgQueue::new_from_pool(&queue_name, db) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new_from_pool(&queue_name, db).await; queue.enqueue(new_job).await.expect("failed to enqueue job"); let mut batch: PgTransactionBatch<'_, JobParameters, JobMetadata> = queue @@ -868,9 +871,7 @@ mod tests { .queue(&retry_queue_name) .provide(); - let queue = PgQueue::new_from_pool(&queue_name, db.clone()) - .await - .expect("failed to connect to queue in local test postgresql database"); + let queue = PgQueue::new_from_pool(&queue_name, db.clone()).await; queue.enqueue(new_job).await.expect("failed to enqueue job"); let mut batch: PgTransactionBatch = queue @@ -899,9 +900,7 @@ mod tests { assert!(retried_job_not_found.is_none()); - let queue = PgQueue::new_from_pool(&retry_queue_name, db) - .await - .expect("failed to connect to retry queue in local test postgresql database"); + let queue = PgQueue::new_from_pool(&retry_queue_name, db).await; let retried_job: PgTransactionJob = queue .dequeue_tx(&worker_id, 1) @@ -933,9 +932,8 @@ mod tests { let new_job = NewJob::new(1, job_metadata, job_parameters, &job_target); let retry_policy = RetryPolicy::build(0, time::Duration::from_secs(0)).provide(); - let queue = PgQueue::new_from_pool("test_cannot_retry_job_without_remaining_attempts", db) - .await - .expect("failed to connect to local test postgresql database"); + let queue = + PgQueue::new_from_pool("test_cannot_retry_job_without_remaining_attempts", db).await; queue.enqueue(new_job).await.expect("failed to enqueue job"); diff --git a/hook-common/src/webhook.rs b/hook-common/src/webhook.rs index 11e02856703eb..5286629978931 100644 --- a/hook-common/src/webhook.rs +++ b/hook-common/src/webhook.rs @@ -6,7 +6,7 @@ use std::str::FromStr; use serde::{de::Visitor, Deserialize, Serialize}; use crate::kafka_messages::app_metrics; -use crate::pgqueue::PgQueueError; +use crate::pgqueue::ParseError; /// Supported HTTP methods for webhooks. #[derive(Debug, PartialEq, Clone, Copy)] @@ -20,7 +20,7 @@ pub enum HttpMethod { /// Allow casting `HttpMethod` from strings. impl FromStr for HttpMethod { - type Err = PgQueueError; + type Err = ParseError; fn from_str(s: &str) -> Result { match s.to_ascii_uppercase().as_ref() { @@ -29,7 +29,7 @@ impl FromStr for HttpMethod { "PATCH" => Ok(HttpMethod::PATCH), "POST" => Ok(HttpMethod::POST), "PUT" => Ok(HttpMethod::PUT), - invalid => Err(PgQueueError::ParseHttpMethodError(invalid.to_owned())), + invalid => Err(ParseError::ParseHttpMethodError(invalid.to_owned())), } } } diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 7f7fadd408565..0e3900c045d84 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -786,9 +786,7 @@ mod tests { WebhookCleaner::new_from_pool(db.clone(), mock_producer, APP_METRICS_TOPIC.to_owned()) .expect("unable to create webhook cleaner"); - let queue = PgQueue::new_from_pool("webhooks", db.clone()) - .await - .expect("failed to connect to local test postgresql database"); + let queue = PgQueue::new_from_pool("webhooks", db.clone()).await; async fn get_count_from_new_conn(db: &PgPool, status: &str) -> i64 { let mut conn = db.acquire().await.unwrap(); diff --git a/hook-worker/src/error.rs b/hook-worker/src/error.rs index 614fe721957e4..914ffb1b2e2ee 100644 --- a/hook-worker/src/error.rs +++ b/hook-worker/src/error.rs @@ -24,10 +24,10 @@ pub enum WebhookError { /// Enumeration of errors related to initialization and consumption of webhook jobs. #[derive(Error, Debug)] pub enum WorkerError { + #[error("a database error occurred when executing a job")] + DatabaseError(#[from] pgqueue::DatabaseError), + #[error("a parsing error occurred in the underlying queue")] + QueueParseError(#[from] pgqueue::ParseError), #[error("timed out while waiting for jobs to be available")] TimeoutError, - #[error("an error occurred in the underlying queue")] - QueueError(#[from] pgqueue::PgQueueError), - #[error("an error occurred in the underlying job: {0}")] - PgJobError(String), } diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index b83c909cb698e..484edf7ff36a0 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -6,7 +6,9 @@ use futures::future::join_all; use hook_common::health::HealthHandle; use hook_common::pgqueue::PgTransactionBatch; use hook_common::{ - pgqueue::{Job, PgJobError, PgQueue, PgQueueError, PgQueueJob, PgTransactionJob}, + pgqueue::{ + DatabaseError, Job, PgQueue, PgQueueJob, PgTransactionJob, RetryError, RetryInvalidError, + }, retry::RetryPolicy, webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}, }; @@ -232,10 +234,10 @@ async fn process_webhook_job( match send_result { Ok(_) => { - webhook_job - .complete() - .await - .map_err(|error| WorkerError::PgJobError(error.to_string()))?; + webhook_job.complete().await.map_err(|error| { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + error + })?; metrics::counter!("webhook_jobs_completed", &labels).increment(1); metrics::histogram!("webhook_jobs_processing_duration_seconds", &labels) @@ -247,7 +249,10 @@ async fn process_webhook_job( webhook_job .fail(WebhookJobError::new_parse(&e.to_string())) .await - .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + job_error + })?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -257,7 +262,10 @@ async fn process_webhook_job( webhook_job .fail(WebhookJobError::new_parse(&e)) .await - .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + job_error + })?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -267,7 +275,10 @@ async fn process_webhook_job( webhook_job .fail(WebhookJobError::new_parse(&e.to_string())) .await - .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + job_error + })?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -288,26 +299,35 @@ async fn process_webhook_job( Ok(()) } - Err(PgJobError::RetryInvalidError { + Err(RetryError::RetryInvalidError(RetryInvalidError { job: webhook_job, .. - }) => { + })) => { webhook_job .fail(WebhookJobError::from(&error)) .await - .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + job_error + })?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); Ok(()) } - Err(job_error) => Err(WorkerError::PgJobError(job_error.to_string())), + Err(RetryError::DatabaseError(job_error)) => { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + Err(WorkerError::from(job_error)) + } } } Err(WebhookError::NonRetryableRetryableRequestError(error)) => { webhook_job .fail(WebhookJobError::from(&error)) .await - .map_err(|job_error| WorkerError::PgJobError(job_error.to_string()))?; + .map_err(|job_error| { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + job_error + })?; metrics::counter!("webhook_jobs_failed", &labels).increment(1); @@ -436,7 +456,7 @@ mod tests { max_attempts: i32, job_parameters: WebhookJobParameters, job_metadata: WebhookJobMetadata, - ) -> Result<(), PgQueueError> { + ) -> Result<(), DatabaseError> { let job_target = job_parameters.url.to_owned(); let new_job = NewJob::new(max_attempts, job_metadata, job_parameters, &job_target); queue.enqueue(new_job).await?; @@ -477,9 +497,7 @@ mod tests { async fn test_wait_for_job(db: PgPool) { let worker_id = worker_id(); let queue_name = "test_wait_for_job".to_string(); - let queue = PgQueue::new_from_pool(&queue_name, db) - .await - .expect("failed to connect to PG"); + let queue = PgQueue::new_from_pool(&queue_name, db).await; let webhook_job_parameters = WebhookJobParameters { body: "a webhook job body. much wow.".to_owned(), From fca80b06bc7b28c0a18005fec6eaefa827cbe0ee Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Tue, 6 Feb 2024 10:43:31 -0700 Subject: [PATCH 206/249] Use a single image type, namespace images under hog-rs --- .github/workflows/docker-capture.yml | 4 ++-- .github/workflows/docker-hook-api.yml | 2 +- .github/workflows/docker-hook-janitor.yml | 2 +- .github/workflows/docker-hook-worker.yml | 2 +- .github/workflows/docker-migrator.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index 4b4c1cd2a98eb..c810709a88ae9 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -12,7 +12,7 @@ permissions: jobs: build: name: build and publish capture image - runs-on: buildjet-8vcpu-ubuntu-2204-arm + runs-on: buildjet-4vcpu-ubuntu-2204-arm steps: - name: Check Out Repo uses: actions/checkout@v3 @@ -37,7 +37,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/capture + images: ghcr.io/posthog/hog-rs/capture tags: | type=ref,event=pr type=ref,event=branch diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 0057d6cece253..eba5b31124d92 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -37,7 +37,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hook-api + images: ghcr.io/posthog/hog-rs/hook-api tags: | type=ref,event=pr type=ref,event=branch diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 32903baa44465..5a362aa8757a2 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -37,7 +37,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hook-janitor + images: ghcr.io/posthog/hog-rs/hook-janitor tags: | type=ref,event=pr type=ref,event=branch diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 05a1054f73805..808bcd47cac97 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -37,7 +37,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hook-worker + images: ghcr.io/posthog/hog-rs/hook-worker tags: | type=ref,event=pr type=ref,event=branch diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index c186aabee3dd3..a7880d1ec8faa 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -37,7 +37,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hook-migrator + images: ghcr.io/posthog/hog-rs/hook-migrator tags: | type=ref,event=pr type=ref,event=branch From 58d777ff9c7bb196350b6dad53711eaefc2a4654 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 7 Feb 2024 07:50:09 -0700 Subject: [PATCH 207/249] Use Depot for build-push-action (#6) --- .github/workflows/docker-capture.yml | 5 ++++- .github/workflows/docker-hook-api.yml | 5 ++++- .github/workflows/docker-hook-janitor.yml | 5 ++++- .github/workflows/docker-hook-worker.yml | 5 ++++- .github/workflows/docker-migrator.yml | 5 ++++- depot.json | 1 + 6 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 depot.json diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index c810709a88ae9..d158b39e7d7f9 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -17,6 +17,9 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: Login to DockerHub uses: docker/login-action@v2 with: @@ -51,7 +54,7 @@ jobs: - name: Build and push capture id: docker_build_capture - uses: docker/build-push-action@v4 + uses: depot/build-push-action@v4 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index eba5b31124d92..efc5736f71e67 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -17,6 +17,9 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: Login to DockerHub uses: docker/login-action@v2 with: @@ -51,7 +54,7 @@ jobs: - name: Build and push api id: docker_build_hook_api - uses: docker/build-push-action@v4 + uses: depot/build-push-action@v4 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 5a362aa8757a2..ee4420516f83a 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -17,6 +17,9 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: Login to DockerHub uses: docker/login-action@v2 with: @@ -51,7 +54,7 @@ jobs: - name: Build and push janitor id: docker_build_hook_janitor - uses: docker/build-push-action@v4 + uses: depot/build-push-action@v4 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 808bcd47cac97..940a36c477e12 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -17,6 +17,9 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: Login to DockerHub uses: docker/login-action@v2 with: @@ -51,7 +54,7 @@ jobs: - name: Build and push worker id: docker_build_hook_worker - uses: docker/build-push-action@v4 + uses: depot/build-push-action@v4 with: context: ./ file: ./Dockerfile diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index a7880d1ec8faa..91e34a42e106f 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -17,6 +17,9 @@ jobs: - name: Check Out Repo uses: actions/checkout@v3 + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: Login to DockerHub uses: docker/login-action@v2 with: @@ -51,7 +54,7 @@ jobs: - name: Build and push migrator id: docker_build_hook_migrator - uses: docker/build-push-action@v4 + uses: depot/build-push-action@v4 with: context: ./ file: ./Dockerfile.migrate diff --git a/depot.json b/depot.json new file mode 100644 index 0000000000000..ea625b42fe589 --- /dev/null +++ b/depot.json @@ -0,0 +1 @@ +{"id":"zcszdgwzsw"} From 017ae0397e7e6954924201e84e9ad2dd581bb46f Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 7 Feb 2024 08:03:52 -0700 Subject: [PATCH 208/249] Fix depot build-push-action version and drop invalid builder argument (#7) --- .github/workflows/docker-capture.yml | 3 +-- .github/workflows/docker-hook-api.yml | 3 +-- .github/workflows/docker-hook-janitor.yml | 3 +-- .github/workflows/docker-hook-worker.yml | 3 +-- .github/workflows/docker-migrator.yml | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index d158b39e7d7f9..c6d1865222233 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -54,11 +54,10 @@ jobs: - name: Build and push capture id: docker_build_capture - uses: depot/build-push-action@v4 + uses: depot/build-push-action@v1 with: context: ./ file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index efc5736f71e67..0bf097c4e68e7 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -54,11 +54,10 @@ jobs: - name: Build and push api id: docker_build_hook_api - uses: depot/build-push-action@v4 + uses: depot/build-push-action@v1 with: context: ./ file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index ee4420516f83a..66b0eef7ba41b 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -54,11 +54,10 @@ jobs: - name: Build and push janitor id: docker_build_hook_janitor - uses: depot/build-push-action@v4 + uses: depot/build-push-action@v1 with: context: ./ file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 940a36c477e12..bb4998f2bbfc4 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -54,11 +54,10 @@ jobs: - name: Build and push worker id: docker_build_hook_worker - uses: depot/build-push-action@v4 + uses: depot/build-push-action@v1 with: context: ./ file: ./Dockerfile - builder: ${{ steps.buildx.outputs.name }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index 91e34a42e106f..b6bfad6d15e0f 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -54,11 +54,10 @@ jobs: - name: Build and push migrator id: docker_build_hook_migrator - uses: depot/build-push-action@v4 + uses: depot/build-push-action@v1 with: context: ./ file: ./Dockerfile.migrate - builder: ${{ steps.buildx.outputs.name }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 49fa92b7e9f1a4330d5f109679e484238cc8b072 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 7 Feb 2024 08:19:03 -0700 Subject: [PATCH 209/249] Configure Actions permissions for build runs (#8) --- .github/workflows/docker-capture.yml | 5 +++++ .github/workflows/docker-hook-api.yml | 5 +++++ .github/workflows/docker-hook-janitor.yml | 5 +++++ .github/workflows/docker-hook-worker.yml | 5 +++++ .github/workflows/docker-migrator.yml | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index c6d1865222233..4426828e8b49a 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -13,6 +13,11 @@ jobs: build: name: build and publish capture image runs-on: buildjet-4vcpu-ubuntu-2204-arm + permissions: + id-token: write # allow issuing OIDC tokens for this workflow run + contents: read # allow reading the repo contents + packages: write # allow push to ghcr.io + steps: - name: Check Out Repo uses: actions/checkout@v3 diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 0bf097c4e68e7..4df1eb6b0ad95 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -13,6 +13,11 @@ jobs: build: name: build and publish hook-api image runs-on: buildjet-4vcpu-ubuntu-2204-arm + permissions: + id-token: write # allow issuing OIDC tokens for this workflow run + contents: read # allow reading the repo contents + packages: write # allow push to ghcr.io + steps: - name: Check Out Repo uses: actions/checkout@v3 diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 66b0eef7ba41b..0894d4548d658 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -13,6 +13,11 @@ jobs: build: name: build and publish hook-janitor image runs-on: buildjet-4vcpu-ubuntu-2204-arm + permissions: + id-token: write # allow issuing OIDC tokens for this workflow run + contents: read # allow reading the repo contents + packages: write # allow push to ghcr.io + steps: - name: Check Out Repo uses: actions/checkout@v3 diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index bb4998f2bbfc4..2568f5eec9859 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -13,6 +13,11 @@ jobs: build: name: build and publish hook-worker image runs-on: buildjet-4vcpu-ubuntu-2204-arm + permissions: + id-token: write # allow issuing OIDC tokens for this workflow run + contents: read # allow reading the repo contents + packages: write # allow push to ghcr.io + steps: - name: Check Out Repo uses: actions/checkout@v3 diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index b6bfad6d15e0f..250fe9b34ccd4 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -13,6 +13,11 @@ jobs: build: name: build and publish hook-migrator image runs-on: buildjet-4vcpu-ubuntu-2204-arm + permissions: + id-token: write # allow issuing OIDC tokens for this workflow run + contents: read # allow reading the repo contents + packages: write # allow push to ghcr.io + steps: - name: Check Out Repo uses: actions/checkout@v3 From a42a056a952f22e7dccdff0bffd108421ee578cc Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 7 Feb 2024 08:44:14 -0700 Subject: [PATCH 210/249] Actions docker build: change build-args to a newline delimited string (#9) --- .github/workflows/docker-capture.yml | 4 +++- .github/workflows/docker-hook-api.yml | 4 +++- .github/workflows/docker-hook-janitor.yml | 4 +++- .github/workflows/docker-hook-worker.yml | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index 4426828e8b49a..75a94e5ee5d97 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -69,7 +69,9 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: RUST_BACKTRACE=1 BIN=capture-server + build-args: | + "RUST_BACKTRACE=1" + "BIN=capture-server" - name: Capture image digest run: echo ${{ steps.docker_build_capture.outputs.digest }} diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 4df1eb6b0ad95..57e603cfc9e3c 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -69,7 +69,9 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: RUST_BACKTRACE=1 BIN=hook-api + build-args: | + "RUST_BACKTRACE=1" + "BIN=hook-api" - name: Hook-api image digest run: echo ${{ steps.docker_build_hook_api.outputs.digest }} diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 0894d4548d658..98fc7371b0f8c 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -69,7 +69,9 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: RUST_BACKTRACE=1 BIN=hook-janitor + build-args: | + "RUST_BACKTRACE=1" + "BIN=hook-janitor" - name: Hook-janitor image digest run: echo ${{ steps.docker_build_hook_janitor.outputs.digest }} diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 2568f5eec9859..9604d0871f16a 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -69,7 +69,9 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: RUST_BACKTRACE=1 BIN=hook-worker + build-args: | + "RUST_BACKTRACE=1" + "BIN=hook-worker" - name: Hook-worker image digest run: echo ${{ steps.docker_build_hook_worker.outputs.digest }} From 6770bed595c592298d3a183e8a5f78c26bac3707 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 7 Feb 2024 09:47:36 -0700 Subject: [PATCH 211/249] Temporarily switch to a single build-args argument (#10) --- .github/workflows/docker-capture.yml | 4 +--- .github/workflows/docker-hook-api.yml | 4 +--- .github/workflows/docker-hook-janitor.yml | 4 +--- .github/workflows/docker-hook-worker.yml | 4 +--- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index 75a94e5ee5d97..468c5c2738331 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -69,9 +69,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: | - "RUST_BACKTRACE=1" - "BIN=capture-server" + build-args: BIN=capture-server - name: Capture image digest run: echo ${{ steps.docker_build_capture.outputs.digest }} diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 57e603cfc9e3c..ef22c69487c8a 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -69,9 +69,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: | - "RUST_BACKTRACE=1" - "BIN=hook-api" + build-args: BIN=hook-api - name: Hook-api image digest run: echo ${{ steps.docker_build_hook_api.outputs.digest }} diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 98fc7371b0f8c..54129c65f072c 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -69,9 +69,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: | - "RUST_BACKTRACE=1" - "BIN=hook-janitor" + build-args: BIN=hook-janitor - name: Hook-janitor image digest run: echo ${{ steps.docker_build_hook_janitor.outputs.digest }} diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 9604d0871f16a..1c3e17e2ae18a 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -69,9 +69,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: | - "RUST_BACKTRACE=1" - "BIN=hook-worker" + build-args: BIN=hook-worker - name: Hook-worker image digest run: echo ${{ steps.docker_build_hook_worker.outputs.digest }} From cb635eb6b88557e5fac079ac49fc4afd34390c03 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 7 Feb 2024 12:21:44 -0700 Subject: [PATCH 212/249] Fix Docker entrypoint (#11) --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7bbfda6fe2342..f34d1eeb31d6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,10 +29,9 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* ARG BIN -ENV ENTRYPOINT=/usr/local/bin/$BIN WORKDIR /app USER nobody COPY --from=builder /app/target/release/$BIN /usr/local/bin -ENTRYPOINT [ $ENTRYPOINT ] +ENTRYPOINT ["/bin/sh", "-c", "/usr/local/bin/$BIN"] From f9626e94dd9fe3c265e48e69aa2958d766e3b4f1 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Wed, 7 Feb 2024 12:37:10 -0700 Subject: [PATCH 213/249] Add ENV for BIN (#12) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index f34d1eeb31d6e..67aea7f210f44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* ARG BIN +ENV BIN=$BIN WORKDIR /app USER nobody From c856730f94d71e45612894e3c3d4c39d7929b517 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 8 Feb 2024 08:09:02 -0700 Subject: [PATCH 214/249] Add histogram for insertion time to completion time (#5) * Add histogram for insertion time to completion time * Handle feedback * Oops, spurious import --- hook-worker/src/worker.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 484edf7ff36a0..441e1ee609de8 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -2,6 +2,7 @@ use std::collections; use std::sync::Arc; use std::time; +use chrono::Utc; use futures::future::join_all; use hook_common::health::HealthHandle; use hook_common::pgqueue::PgTransactionBatch; @@ -234,11 +235,24 @@ async fn process_webhook_job( match send_result { Ok(_) => { + let created_at = webhook_job.job().created_at; + let retries = webhook_job.job().attempt - 1; + let labels_with_retries = [ + ("queue", webhook_job.queue()), + ("retries", retries.to_string()), + ]; + webhook_job.complete().await.map_err(|error| { metrics::counter!("webhook_jobs_database_error", &labels).increment(1); error })?; + let insert_to_complete_duration = Utc::now() - created_at; + metrics::histogram!( + "webhook_jobs_insert_to_complete_duration_seconds", + &labels_with_retries + ) + .record((insert_to_complete_duration.num_milliseconds() as f64) / 1_000_f64); metrics::counter!("webhook_jobs_completed", &labels).increment(1); metrics::histogram!("webhook_jobs_processing_duration_seconds", &labels) .record(elapsed); From 4214362ef0203ff82cd9d8aea5d77c59ec41b4d3 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Thu, 8 Feb 2024 08:23:50 -0700 Subject: [PATCH 215/249] Fix clippy complaint (#13) --- hook-common/src/kafka_messages/plugin_logs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hook-common/src/kafka_messages/plugin_logs.rs b/hook-common/src/kafka_messages/plugin_logs.rs index fb835804c687c..5a852e6aa3221 100644 --- a/hook-common/src/kafka_messages/plugin_logs.rs +++ b/hook-common/src/kafka_messages/plugin_logs.rs @@ -65,7 +65,7 @@ where serializer.serialize_str(type_str) } -fn serialize_message(msg: &String, serializer: S) -> Result +fn serialize_message(msg: &str, serializer: S) -> Result where S: Serializer, { From 3bcbb239dc5851aeb1ec0ec3b256d16984bd1a6e Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 9 Feb 2024 13:25:01 -0700 Subject: [PATCH 216/249] Drop debug symbols from release (#15) --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index db01334cdd8c3..3aeacb8bcf0a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,8 @@ members = [ "hook-janitor", ] -[profile.release] -debug = 2 # https://www.polarsignals.com/docs/rust +# [profile.release] +# debug = 2 # https://www.polarsignals.com/docs/rust [workspace.dependencies] anyhow = "1.0" From e3047a6ff7cbca2e0dc745e412ae6a178c62e1eb Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 12 Feb 2024 18:58:39 +0100 Subject: [PATCH 217/249] replace vestigial unwraps with proper error reporting (#14) --- capture/src/capture.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/capture/src/capture.rs b/capture/src/capture.rs index d7303fd20f892..7f90d575fcf1c 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -75,10 +75,16 @@ pub async fn event( "application/x-www-form-urlencoded" => { tracing::Span::current().record("content_type", "application/x-www-form-urlencoded"); - let input: EventFormData = serde_urlencoded::from_bytes(body.deref()).unwrap(); + let input: EventFormData = serde_urlencoded::from_bytes(body.deref()).map_err(|e| { + tracing::error!("failed to decode body: {}", e); + CaptureError::RequestDecodingError(String::from("invalid form data")) + })?; let payload = base64::engine::general_purpose::STANDARD .decode(input.data) - .unwrap(); + .map_err(|e| { + tracing::error!("failed to decode form data: {}", e); + CaptureError::RequestDecodingError(String::from("missing data field")) + })?; RawEvent::from_bytes(payload.into()) } ct => { From 1ac549d27d68f577bb982b30f78459b96b1111a0 Mon Sep 17 00:00:00 2001 From: Frank Hamand Date: Wed, 21 Feb 2024 15:50:32 +0000 Subject: [PATCH 218/249] Replace DOCKERHUB_USERNAME secret with plaintext (#18) This isn't a secret and it causes every instance of "posthog" in the action logs to be replaced by "***" because it's the value of a secret --- .github/workflows/docker-capture.yml | 2 +- .github/workflows/docker-hook-api.yml | 2 +- .github/workflows/docker-hook-janitor.yml | 2 +- .github/workflows/docker-hook-worker.yml | 2 +- .github/workflows/docker-migrator.yml | 2 +- .github/workflows/rust.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index 468c5c2738331..30232bc5c1a3c 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -28,7 +28,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: posthog password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to ghcr.io diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index ef22c69487c8a..2d9384529fb0a 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -28,7 +28,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: posthog password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to ghcr.io diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 54129c65f072c..31a4e2d86c5cf 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -28,7 +28,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: posthog password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to ghcr.io diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index 1c3e17e2ae18a..c3ba8ce888452 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -28,7 +28,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: posthog password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to ghcr.io diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index 250fe9b34ccd4..479d6f9126255 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -28,7 +28,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: posthog password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to ghcr.io diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7ee5eb83a4c90..72dfd0bc5093f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -43,7 +43,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} + username: posthog password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup dependencies From 92018ab4a97221db4a074c0d234c5a821aad362e Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 23 Feb 2024 07:47:38 -0700 Subject: [PATCH 219/249] Use Depot runners (#16) --- .github/workflows/docker-capture.yml | 2 +- .github/workflows/docker-hook-api.yml | 2 +- .github/workflows/docker-hook-janitor.yml | 2 +- .github/workflows/docker-hook-worker.yml | 2 +- .github/workflows/docker-migrator.yml | 2 +- .github/workflows/rust.yml | 8 ++++---- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index 30232bc5c1a3c..d0efac36b0852 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -12,7 +12,7 @@ permissions: jobs: build: name: build and publish capture image - runs-on: buildjet-4vcpu-ubuntu-2204-arm + runs-on: depot-ubuntu-22.04-4 permissions: id-token: write # allow issuing OIDC tokens for this workflow run contents: read # allow reading the repo contents diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml index 2d9384529fb0a..e6f83f1b6b3fb 100644 --- a/.github/workflows/docker-hook-api.yml +++ b/.github/workflows/docker-hook-api.yml @@ -12,7 +12,7 @@ permissions: jobs: build: name: build and publish hook-api image - runs-on: buildjet-4vcpu-ubuntu-2204-arm + runs-on: depot-ubuntu-22.04-4 permissions: id-token: write # allow issuing OIDC tokens for this workflow run contents: read # allow reading the repo contents diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml index 31a4e2d86c5cf..33706d0987637 100644 --- a/.github/workflows/docker-hook-janitor.yml +++ b/.github/workflows/docker-hook-janitor.yml @@ -12,7 +12,7 @@ permissions: jobs: build: name: build and publish hook-janitor image - runs-on: buildjet-4vcpu-ubuntu-2204-arm + runs-on: depot-ubuntu-22.04-4 permissions: id-token: write # allow issuing OIDC tokens for this workflow run contents: read # allow reading the repo contents diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml index c3ba8ce888452..dc5ca53abef88 100644 --- a/.github/workflows/docker-hook-worker.yml +++ b/.github/workflows/docker-hook-worker.yml @@ -12,7 +12,7 @@ permissions: jobs: build: name: build and publish hook-worker image - runs-on: buildjet-4vcpu-ubuntu-2204-arm + runs-on: depot-ubuntu-22.04-4 permissions: id-token: write # allow issuing OIDC tokens for this workflow run contents: read # allow reading the repo contents diff --git a/.github/workflows/docker-migrator.yml b/.github/workflows/docker-migrator.yml index 479d6f9126255..906a022391b58 100644 --- a/.github/workflows/docker-migrator.yml +++ b/.github/workflows/docker-migrator.yml @@ -12,7 +12,7 @@ permissions: jobs: build: name: build and publish hook-migrator image - runs-on: buildjet-4vcpu-ubuntu-2204-arm + runs-on: depot-ubuntu-22.04-4 permissions: id-token: write # allow issuing OIDC tokens for this workflow run contents: read # allow reading the repo contents diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 72dfd0bc5093f..30f17341d5f08 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,7 +12,7 @@ env: jobs: build: - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: depot-ubuntu-22.04-4 steps: - uses: actions/checkout@v3 @@ -34,7 +34,7 @@ jobs: run: cargo build --all --locked --release && find target/release/ -maxdepth 1 -executable -type f | xargs strip test: - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: depot-ubuntu-22.04-4 timeout-minutes: 10 steps: @@ -72,7 +72,7 @@ jobs: run: cargo check --all-features clippy: - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: depot-ubuntu-22.04-4 steps: - uses: actions/checkout@v3 @@ -95,7 +95,7 @@ jobs: run: cargo clippy -- -D warnings format: - runs-on: buildjet-4vcpu-ubuntu-2204 + runs-on: depot-ubuntu-22.04-4 steps: - uses: actions/checkout@v3 From 56d4615b3aa11ba4c7ad74c8c61ef786912e58c3 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 15 Apr 2024 16:20:52 +0200 Subject: [PATCH 220/249] cleanup and upgrade deps (#20) --- Cargo.lock | 545 ++++++++++++++++++--------------- Cargo.toml | 19 +- capture-server/Cargo.toml | 14 +- capture-server/src/main.rs | 5 +- capture-server/tests/common.rs | 17 +- capture-server/tests/events.rs | 10 +- capture/Cargo.toml | 9 +- capture/src/capture.rs | 3 +- capture/src/prometheus.rs | 3 +- capture/src/server.rs | 18 +- hook-api/Cargo.toml | 1 - hook-common/Cargo.toml | 2 - hook-janitor/Cargo.toml | 6 - hook-worker/Cargo.toml | 4 +- 14 files changed, 339 insertions(+), 317 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f611e201f067..71cbc8d51a5d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,28 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "async-trait" version = "0.1.77" @@ -140,11 +162,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", @@ -152,15 +170,16 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" dependencies = [ "async-trait", "axum-core 0.4.3", + "axum-macros", "bytes", "futures-util", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "http-body-util", "hyper 1.1.0", @@ -176,7 +195,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", "tower", "tower-layer", @@ -186,11 +205,11 @@ dependencies = [ [[package]] name = "axum-client-ip" -version = "0.4.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef117890a418b7832678d9ea1e1c08456dd7b2fd1dadb9676cd6f0fe7eb4b21" +checksum = "72188bed20deb981f3a4a9fe674e5980fd9e9c2bd880baa94715ad5d60d64c67" dependencies = [ - "axum 0.6.20", + "axum 0.7.5", "forwarded-header-value", "serde", ] @@ -221,30 +240,41 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower-layer", "tower-service", "tracing", ] [[package]] -name = "axum-test-helper" -version = "0.2.0" +name = "axum-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d349b3174ceac58442ea1f768233c817e59447c0343be2584fca9f0ed71d3a" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" dependencies = [ - "axum 0.6.20", + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "axum-test-helper" +version = "0.4.0" +source = "git+https://github.com/orphan-rs/axum-test-helper.git#8ca0aedaad5a6bdf351c34d5b80593ae1b7d2f3f" +dependencies = [ + "axum 0.7.5", "bytes", - "http 0.2.11", - "http-body 0.4.6", - "hyper 0.14.28", - "reqwest", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.1.0", + "reqwest 0.11.24", "serde", "tokio", "tower", @@ -272,6 +302,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "base64ct" version = "1.6.0" @@ -327,22 +363,19 @@ dependencies = [ "anyhow", "assert-json-diff", "async-trait", - "axum 0.6.20", + "axum 0.7.5", "axum-client-ip", "axum-test-helper", - "base64", + "base64 0.22.0", "bytes", - "dashmap", "envconfig", "flate2", "governor", "metrics", "metrics-exporter-prometheus", - "mockall", "rand", "rdkafka", "redis", - "redis-test", "serde", "serde_json", "serde_urlencoded", @@ -350,9 +383,7 @@ dependencies = [ "time", "tokio", "tower-http", - "tower_governor", "tracing", - "tracing-subscriber", "uuid", ] @@ -362,7 +393,6 @@ version = "0.1.0" dependencies = [ "anyhow", "assert-json-diff", - "axum 0.7.4", "capture", "envconfig", "futures", @@ -372,9 +402,8 @@ dependencies = [ "opentelemetry_sdk", "rand", "rdkafka", - "reqwest", + "reqwest 0.12.3", "serde_json", - "time", "tokio", "tracing", "tracing-opentelemetry", @@ -571,12 +600,6 @@ dependencies = [ "serde", ] -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - [[package]] name = "digest" version = "0.10.7" @@ -595,12 +618,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - [[package]] name = "either" version = "1.9.0" @@ -704,15 +721,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - [[package]] name = "flume" version = "0.11.0" @@ -764,12 +772,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "fragile" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" - [[package]] name = "futures" version = "0.3.30" @@ -957,7 +959,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 1.0.0", + "http 1.1.0", "indexmap 2.2.2", "slab", "tokio", @@ -1051,13 +1053,12 @@ dependencies = [ name = "hook-api" version = "0.1.0" dependencies = [ - "axum 0.7.4", + "axum 0.7.5", "envconfig", "eyre", "hook-common", "http-body-util", "metrics", - "metrics-exporter-prometheus", "serde", "serde_derive", "serde_json", @@ -1074,15 +1075,13 @@ name = "hook-common" version = "0.1.0" dependencies = [ "async-trait", - "axum 0.7.4", + "axum 0.7.5", "chrono", - "http 0.2.11", + "http 1.1.0", "metrics", "metrics-exporter-prometheus", - "regex", - "reqwest", + "reqwest 0.12.3", "serde", - "serde_derive", "serde_json", "sqlx", "thiserror", @@ -1097,42 +1096,34 @@ name = "hook-janitor" version = "0.1.0" dependencies = [ "async-trait", - "axum 0.7.4", + "axum 0.7.5", "envconfig", "eyre", "futures", "hook-common", - "http-body-util", "metrics", - "metrics-exporter-prometheus", "rdkafka", - "serde", - "serde_derive", "serde_json", "sqlx", "thiserror", "time", "tokio", - "tower", "tracing", "tracing-subscriber", - "url", ] [[package]] name = "hook-worker" version = "0.1.0" dependencies = [ - "axum 0.7.4", + "axum 0.7.5", "chrono", "envconfig", "futures", "hook-common", - "http 0.2.11", + "http 1.1.0", "metrics", - "reqwest", - "serde", - "serde_derive", + "reqwest 0.12.3", "sqlx", "thiserror", "time", @@ -1155,9 +1146,9 @@ dependencies = [ [[package]] name = "http" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1182,7 +1173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http 1.0.0", + "http 1.1.0", ] [[package]] @@ -1193,17 +1184,11 @@ checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" dependencies = [ "bytes", "futures-util", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - [[package]] name = "httparse" version = "1.8.0" @@ -1250,13 +1235,28 @@ dependencies = [ "futures-channel", "futures-util", "h2 0.4.2", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "httparse", "httpdate", "itoa", "pin-project-lite", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.11", + "hyper 0.14.28", + "rustls", + "tokio", + "tokio-rustls", ] [[package]] @@ -1273,15 +1273,18 @@ dependencies = [ [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.28", + "http-body-util", + "hyper 1.1.0", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] @@ -1291,13 +1294,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", + "futures-channel", "futures-util", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "hyper 1.1.0", "pin-project-lite", "socket2 0.5.5", "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -1365,15 +1372,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -1516,14 +1514,16 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a4c4718a371ddfb7806378f23617876eea8b82e5ff1324516bcd283249d9ea" +checksum = "5d58e362dc7206e9456ddbcdbd53c71ba441020e62104703075a69151e38d85f" dependencies = [ - "base64", - "hyper 0.14.28", + "base64 0.22.0", + "http-body-util", + "hyper 1.1.0", "hyper-tls", - "indexmap 1.9.3", + "hyper-util", + "indexmap 2.2.2", "ipnet", "metrics", "metrics-util", @@ -1590,33 +1590,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "mockall" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "lazy_static", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "native-tls" version = "0.2.11" @@ -1674,12 +1647,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1836,13 +1803,12 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" +checksum = "900d57987be3f2aeb70d385fff9b27fb74c5723cc9a52d904d4f9c807a0667bf" dependencies = [ "futures-core", "futures-sink", - "indexmap 2.2.2", "js-sys", "once_cell", "pin-project-lite", @@ -1852,9 +1818,9 @@ dependencies = [ [[package]] name = "opentelemetry-otlp" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f24cda83b20ed2433c68241f918d0f6fdec8b1d43b7a9590ab4420c5095ca930" +checksum = "1a016b8d9495c639af2145ac22387dcb88e44118e45320d9238fbf4e7889abcb" dependencies = [ "async-trait", "futures-core", @@ -1871,9 +1837,9 @@ dependencies = [ [[package]] name = "opentelemetry-proto" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e155ce5cc812ea3d1dffbd1539aed653de4bf4882d60e6e04dcf0901d674e1" +checksum = "3a8fddc9b68f5b80dae9d6f510b88e02396f006ad48cac349411fbecc80caae4" dependencies = [ "opentelemetry", "opentelemetry_sdk", @@ -1883,18 +1849,15 @@ dependencies = [ [[package]] name = "opentelemetry-semantic-conventions" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5774f1ef1f982ef2a447f6ee04ec383981a3ab99c8e77a1a7b30182e65bbc84" -dependencies = [ - "opentelemetry", -] +checksum = "f9ab5bd6c42fb9349dcf28af2ba9a0667f697f9bdcca045d39f2cec5543e2910" [[package]] name = "opentelemetry_sdk" -version = "0.21.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f16aec8a98a457a52664d69e0091bac3a0abd18ead9b641cb00202ba4e0efe4" +checksum = "9e90c7113be649e31e9a0f8b5ee24ed7a16923b322c3c5ab6367469c049d6b7e" dependencies = [ "async-trait", "crossbeam-channel", @@ -2048,36 +2011,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "predicates" -version = "2.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" -dependencies = [ - "difflib", - "float-cmp", - "itertools 0.10.5", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" - -[[package]] -name = "predicates-tree" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" -dependencies = [ - "predicates-core", - "termtree", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -2099,9 +2032,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.9" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "d0f5d036824e4761737860779c906171497f6d55681139d8312388f8fe398922" dependencies = [ "bytes", "prost-derive", @@ -2109,15 +2042,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -2266,15 +2199,6 @@ dependencies = [ "url", ] -[[package]] -name = "redis-test" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba266ca48ae66978bf439fd2ac0d7a36a8635823754e2bc73afaf9d2fc25272" -dependencies = [ - "redis", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -2334,7 +2258,7 @@ version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -2343,24 +2267,24 @@ dependencies = [ "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", - "hyper-tls", + "hyper-rustls", "ipnet", "js-sys", "log", "mime", "mime_guess", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", - "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower-service", "url", @@ -2368,7 +2292,65 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "winreg", + "webpki-roots", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" +dependencies = [ + "base64 0.22.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.2", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.2", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.52.0", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.52.0", ] [[package]] @@ -2410,13 +2392,51 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64", + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -2446,6 +2466,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -2650,7 +2680,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" dependencies = [ - "itertools 0.12.1", + "itertools", "nom", "unicode_categories", ] @@ -2758,7 +2788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "byteorder", "bytes", @@ -2802,7 +2832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.4.2", "byteorder", "chrono", @@ -2906,6 +2936,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2939,12 +2975,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "termtree" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" - [[package]] name = "thiserror" version = "1.0.56" @@ -3071,6 +3101,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -3115,16 +3155,15 @@ dependencies = [ [[package]] name = "tonic" -version = "0.9.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ + "async-stream", "async-trait", "axum 0.6.20", - "base64", + "base64 0.21.7", "bytes", - "futures-core", - "futures-util", "h2 0.3.24", "http 0.2.11", "http-body 0.4.6", @@ -3163,17 +3202,15 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "bitflags 2.4.2", "bytes", - "futures-core", - "futures-util", - "http 0.2.11", - "http-body 0.4.6", - "http-range-header", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", "pin-project-lite", "tower-layer", "tower-service", @@ -3192,26 +3229,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" -[[package]] -name = "tower_governor" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6be418f6d18863291f0a7fa1da1de71495a19a54b5fb44969136f731a47e86" -dependencies = [ - "axum 0.6.20", - "forwarded-header-value", - "futures", - "futures-core", - "governor", - "http 0.2.11", - "pin-project", - "thiserror", - "tokio", - "tower", - "tower-layer", - "tracing", -] - [[package]] name = "tracing" version = "0.1.40" @@ -3258,9 +3275,9 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c67ac25c5407e7b961fafc6f7e9aa5958fd297aada2d20fa2ae1737357e55596" +checksum = "a9be14ba1bbe4ab79e9229f7f89fab8d120b865859f10527f31c033e599d2284" dependencies = [ "js-sys", "once_cell", @@ -3346,6 +3363,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.0" @@ -3504,14 +3527,20 @@ dependencies = [ [[package]] name = "web-time" -version = "0.2.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "whoami" version = "1.4.1" @@ -3700,6 +3729,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 3aeacb8bcf0a5..f77c1fbd85ad2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,16 +10,13 @@ members = [ "hook-janitor", ] -# [profile.release] -# debug = 2 # https://www.polarsignals.com/docs/rust - [workspace.dependencies] anyhow = "1.0" assert-json-diff = "2.0.2" async-trait = "0.1.74" -axum = { version = "0.7.1", features = ["http2"] } -axum-client-ip = "0.4.1" -base64 = "0.21.1" +axum = { version = "0.7.5", features = ["http2", "macros"] } +axum-client-ip = "0.6.0" +base64 = "0.22.0" bytes = "1" chrono = { version = "0.4" } envconfig = "0.10.0" @@ -27,14 +24,13 @@ eyre = "0.6.9" flate2 = "1.0" futures = { version = "0.3.29" } governor = { version = "0.5.1", features = ["dashmap"] } -http = { version = "0.2" } +http = { version = "1.1.0" } http-body-util = "0.1.0" metrics = "0.22.0" -metrics-exporter-prometheus = "0.13.0" +metrics-exporter-prometheus = "0.14.0" rand = "0.8.5" rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl", "tracing"] } -regex = "1.10.2" -reqwest = { version = "0.11" } +reqwest = { version = "0.12.3" } serde = { version = "1.0", features = ["derive"] } serde_derive = { version = "1.0" } serde_json = { version = "1.0" } @@ -57,8 +53,7 @@ time = { version = "0.3.20", features = [ ] } tokio = { version = "1.34.0", features = ["full"] } tower = "0.4.13" -tower_governor = "0.0.4" -tower-http = { version = "0.4.0", features = ["cors", "trace"] } +tower-http = { version = "0.5.2", features = ["cors", "trace"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" url = { version = "2.5.0 " } diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml index e8aa5595486a6..ae06664ec96f9 100644 --- a/capture-server/Cargo.toml +++ b/capture-server/Cargo.toml @@ -4,16 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] -axum = { workspace = true } capture = { path = "../capture" } envconfig = { workspace = true } -opentelemetry = { version = "0.21.0", features = ["trace"]} -opentelemetry-otlp = "0.14.0" -opentelemetry_sdk = { version = "0.21.0", features = ["trace", "rt-tokio"] } -time = { workspace = true } +opentelemetry = { version = "0.22.0", features = ["trace"]} +opentelemetry-otlp = "0.15.0" +opentelemetry_sdk = { version = "0.22.1", features = ["trace", "rt-tokio"] } tokio = { workspace = true } tracing = { workspace = true } -tracing-opentelemetry = "0.22.0" +tracing-opentelemetry = "0.23.0" tracing-subscriber = { workspace = true, features = ["env-filter"] } [dev-dependencies] @@ -23,5 +21,5 @@ futures = "0.3.29" once_cell = "1.18.0" rand = { workspace = true } rdkafka = { workspace = true } -reqwest = "0.11.22" -serde_json = { workspace = true } \ No newline at end of file +reqwest = { workspace = true } +serde_json = { workspace = true } diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 402fc3245d883..97967ed5f0ae7 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -1,4 +1,3 @@ -use std::net::TcpListener; use std::time::Duration; use envconfig::Envconfig; @@ -76,6 +75,8 @@ async fn main() { .init(); // Open the TCP port and start the server - let listener = TcpListener::bind(config.address).unwrap(); + let listener = tokio::net::TcpListener::bind(config.address) + .await + .expect("could not bind port"); serve(config, listener, shutdown()).await } diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index fa8688156e650..e33ef4c20f561 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use std::default::Default; -use std::net::{SocketAddr, TcpListener}; +use std::net::SocketAddr; use std::num::NonZeroU32; use std::str::FromStr; use std::string::ToString; @@ -17,6 +17,7 @@ use rdkafka::config::{ClientConfig, FromClientConfig}; use rdkafka::consumer::{BaseConsumer, Consumer}; use rdkafka::util::Timeout; use rdkafka::{Message, TopicPartitionList}; +use tokio::net::TcpListener; use tokio::sync::Notify; use tokio::time::timeout; use tracing::{debug, warn}; @@ -59,20 +60,20 @@ pub struct ServerHandle { } impl ServerHandle { - pub fn for_topic(topic: &EphemeralTopic) -> Self { + pub async fn for_topic(topic: &EphemeralTopic) -> Self { let mut config = DEFAULT_CONFIG.clone(); config.kafka.kafka_topic = topic.topic_name().to_string(); - Self::for_config(config) + Self::for_config(config).await } - pub fn for_config(config: Config) -> Self { - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + pub async fn for_config(config: Config) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let notify = Arc::new(Notify::new()); let shutdown = notify.clone(); - tokio::spawn( - async move { serve(config, listener, async { notify.notified().await }).await }, - ); + tokio::spawn(async move { + serve(config, listener, async move { notify.notified().await }).await + }); Self { addr, shutdown } } diff --git a/capture-server/tests/events.rs b/capture-server/tests/events.rs index 56fcdf79bf15d..8a4220b8e3920 100644 --- a/capture-server/tests/events.rs +++ b/capture-server/tests/events.rs @@ -14,7 +14,7 @@ async fn it_captures_one_event() -> Result<()> { let token = random_string("token", 16); let distinct_id = random_string("id", 16); let topic = EphemeralTopic::new().await; - let server = ServerHandle::for_topic(&topic); + let server = ServerHandle::for_topic(&topic).await; let event = json!({ "token": token, @@ -44,7 +44,7 @@ async fn it_captures_a_batch() -> Result<()> { let distinct_id2 = random_string("id", 16); let topic = EphemeralTopic::new().await; - let server = ServerHandle::for_topic(&topic); + let server = ServerHandle::for_topic(&topic).await; let event = json!([{ "token": token, @@ -90,7 +90,7 @@ async fn it_overflows_events_on_burst() -> Result<()> { config.overflow_burst_limit = NonZeroU32::new(2).unwrap(); config.overflow_per_second_limit = NonZeroU32::new(1).unwrap(); - let server = ServerHandle::for_config(config); + let server = ServerHandle::for_config(config).await; let event = json!([{ "token": token, @@ -139,7 +139,7 @@ async fn it_does_not_overflow_team_with_different_ids() -> Result<()> { config.overflow_burst_limit = NonZeroU32::new(1).unwrap(); config.overflow_per_second_limit = NonZeroU32::new(1).unwrap(); - let server = ServerHandle::for_config(config); + let server = ServerHandle::for_config(config).await; let event = json!([{ "token": token, @@ -176,7 +176,7 @@ async fn it_trims_distinct_id() -> Result<()> { let (trimmed_distinct_id2, _) = distinct_id2.split_at(200); // works because ascii chars let topic = EphemeralTopic::new().await; - let server = ServerHandle::for_topic(&topic); + let server = ServerHandle::for_topic(&topic).await; let event = json!([{ "token": token, diff --git a/capture/Cargo.toml b/capture/Cargo.toml index bd8f79f57ff72..9ec0f97fb3ddb 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -6,15 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = { version = "0.6.15" } # TODO: Bring up to date with the workspace. +axum = { workspace = true } axum-client-ip = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } governor = { workspace = true } -tower_governor = { workspace = true } time = { workspace = true } tower-http = { workspace = true } bytes = { workspace = true } @@ -35,10 +33,7 @@ redis = { version = "0.23.3", features = [ "cluster-async", ] } envconfig = { workspace = true } -dashmap = "5.5.3" [dev-dependencies] assert-json-diff = { workspace = true } -axum-test-helper = "0.2.0" -mockall = "0.11.2" -redis-test = "0.2.3" +axum-test-helper = { git = "https://github.com/orphan-rs/axum-test-helper.git" } # TODO: remove, directly use reqwest like capture-server tests diff --git a/capture/src/capture.rs b/capture/src/capture.rs index 7f90d575fcf1c..622bc750f90c7 100644 --- a/capture/src/capture.rs +++ b/capture/src/capture.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use bytes::Bytes; -use axum::Json; +use axum::{debug_handler, Json}; // TODO: stream this instead use axum::extract::{Query, State}; use axum::http::{HeaderMap, Method}; @@ -38,6 +38,7 @@ use crate::{ compression ) )] +#[debug_handler] pub async fn event( state: State, InsecureClientIp(ip): InsecureClientIp, diff --git a/capture/src/prometheus.rs b/capture/src/prometheus.rs index 6f5dc12af28e4..b4e19974ab51f 100644 --- a/capture/src/prometheus.rs +++ b/capture/src/prometheus.rs @@ -2,6 +2,7 @@ use std::time::Instant; +use axum::body::Body; use axum::{extract::MatchedPath, http::Request, middleware::Next, response::IntoResponse}; use metrics::counter; use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle}; @@ -38,7 +39,7 @@ pub fn setup_metrics_recorder() -> PrometheusHandle { /// Middleware to record some common HTTP metrics /// Generic over B to allow for arbitrary body types (eg Vec, Streams, a deserialized thing, etc) /// Someday tower-http might provide a metrics middleware: https://github.com/tower-rs/tower-http/issues/57 -pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse { +pub async fn track_metrics(req: Request, next: Next) -> impl IntoResponse { let start = Instant::now(); let path = if let Some(matched_path) = req.extensions().get::() { diff --git a/capture/src/server.rs b/capture/src/server.rs index 22a1f3bc0bf04..2fc88c60687e6 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -1,8 +1,9 @@ use std::future::Future; -use std::net::{SocketAddr, TcpListener}; +use std::net::SocketAddr; use std::sync::Arc; use time::Duration; +use tokio::net::TcpListener; use crate::config::Config; use crate::health::{ComponentStatus, HealthRegistry}; @@ -15,7 +16,7 @@ use crate::sinks::print::PrintSink; pub async fn serve(config: Config, listener: TcpListener, shutdown: F) where - F: Future, + F: Future + Send + 'static, { let liveness = HealthRegistry::new("liveness"); @@ -80,10 +81,11 @@ where // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` tracing::info!("listening on {:?}", listener.local_addr().unwrap()); - axum::Server::from_tcp(listener) - .unwrap() - .serve(app.into_make_service_with_connect_info::()) - .with_graceful_shutdown(shutdown) - .await - .unwrap() + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(shutdown) + .await + .unwrap() } diff --git a/hook-api/Cargo.toml b/hook-api/Cargo.toml index 96c897cd3ab1d..a596e87076b18 100644 --- a/hook-api/Cargo.toml +++ b/hook-api/Cargo.toml @@ -12,7 +12,6 @@ eyre = { workspace = true } hook-common = { path = "../hook-common" } http-body-util = { workspace = true } metrics = { workspace = true } -metrics-exporter-prometheus = { workspace = true } serde = { workspace = true } serde_derive = { workspace = true } serde_json = { workspace = true } diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index ea7ce2fbb9cac..8ccf8dd5b2ebb 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -13,9 +13,7 @@ http = { workspace = true } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } reqwest = { workspace = true } -regex = { workspace = true } serde = { workspace = true } -serde_derive = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } time = { workspace = true } diff --git a/hook-janitor/Cargo.toml b/hook-janitor/Cargo.toml index 96a80ebd9c3d5..a29a80c1c5256 100644 --- a/hook-janitor/Cargo.toml +++ b/hook-janitor/Cargo.toml @@ -12,18 +12,12 @@ envconfig = { workspace = true } eyre = { workspace = true } futures = { workspace = true } hook-common = { path = "../hook-common" } -http-body-util = { workspace = true } metrics = { workspace = true } -metrics-exporter-prometheus = { workspace = true } rdkafka = { workspace = true } -serde = { workspace = true } -serde_derive = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } time = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } -tower = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -url = { workspace = true } diff --git a/hook-worker/Cargo.toml b/hook-worker/Cargo.toml index 6ed5796efd1f0..5d6874a8af4cd 100644 --- a/hook-worker/Cargo.toml +++ b/hook-worker/Cargo.toml @@ -9,11 +9,9 @@ chrono = { workspace = true } envconfig = { workspace = true } futures = "0.3" hook-common = { path = "../hook-common" } -http = { version = "0.2" } +http = { workspace = true } metrics = { workspace = true } reqwest = { workspace = true } -serde = { workspace = true } -serde_derive = { workspace = true } sqlx = { workspace = true } time = { workspace = true } thiserror = { workspace = true } From 591d765691c28861c6adfccc8606fa61afca1c7d Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 22 Apr 2024 15:38:45 +0200 Subject: [PATCH 221/249] capture: make otel service name configurable, will use deploy name (#22) --- capture-server/src/main.rs | 14 ++++++++++---- capture-server/tests/common.rs | 1 + capture/src/config.rs | 3 +++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/capture-server/src/main.rs b/capture-server/src/main.rs index 97967ed5f0ae7..12b91941c7f6c 100644 --- a/capture-server/src/main.rs +++ b/capture-server/src/main.rs @@ -1,7 +1,7 @@ use std::time::Duration; use envconfig::Envconfig; -use opentelemetry::KeyValue; +use opentelemetry::{KeyValue, Value}; use opentelemetry_otlp::WithExportConfig; use opentelemetry_sdk::trace::{BatchConfig, RandomIdGenerator, Sampler, Tracer}; use opentelemetry_sdk::{runtime, Resource}; @@ -31,7 +31,7 @@ async fn shutdown() { tracing::info!("Shutting down gracefully..."); } -fn init_tracer(sink_url: &str, sampling_rate: f64) -> Tracer { +fn init_tracer(sink_url: &str, sampling_rate: f64, service_name: &str) -> Tracer { opentelemetry_otlp::new_pipeline() .tracing() .with_trace_config( @@ -42,7 +42,7 @@ fn init_tracer(sink_url: &str, sampling_rate: f64) -> Tracer { .with_id_generator(RandomIdGenerator::default()) .with_resource(Resource::new(vec![KeyValue::new( "service.name", - "capture", + Value::from(service_name.to_string()), )])), ) .with_batch_config(BatchConfig::default()) @@ -67,7 +67,13 @@ async fn main() { let otel_layer = config .otel_url .clone() - .map(|url| OpenTelemetryLayer::new(init_tracer(&url, config.otel_sampling_rate))) + .map(|url| { + OpenTelemetryLayer::new(init_tracer( + &url, + config.otel_sampling_rate, + &config.otel_service_name, + )) + }) .with_filter(LevelFilter::from_level(Level::INFO)); tracing_subscriber::registry() .with(log_layer) diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index e33ef4c20f561..5ee2caa5ca7a9 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -43,6 +43,7 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { }, otel_url: None, otel_sampling_rate: 0.0, + otel_service_name: "capture-testing".to_string(), export_prometheus: false, }); diff --git a/capture/src/config.rs b/capture/src/config.rs index 0c6ab1ce9eb62..a4bd8f2cfd5cc 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -27,6 +27,9 @@ pub struct Config { #[envconfig(default = "1.0")] pub otel_sampling_rate: f64, + #[envconfig(default = "capture")] + pub otel_service_name: String, + #[envconfig(default = "true")] pub export_prometheus: bool, } From 4db3670622086546ffc9b37fcf1bca25490cb0c9 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 22 Apr 2024 17:32:53 +0200 Subject: [PATCH 222/249] capture: add support for the /batch request shape (#21) --- Cargo.toml | 2 +- capture/src/api.rs | 32 ++-- capture/src/lib.rs | 4 +- capture/src/router.rs | 40 +++- capture/src/sinks/kafka.rs | 6 +- capture/src/sinks/mod.rs | 3 +- capture/src/sinks/print.rs | 3 +- capture/src/{capture.rs => v0_endpoint.rs} | 160 ++++------------ capture/src/{event.rs => v0_request.rs} | 210 +++++++++++++++++---- capture/tests/django_compat.rs | 19 +- capture/tests/requests_dump.jsonl | 2 + 11 files changed, 276 insertions(+), 205 deletions(-) rename capture/src/{capture.rs => v0_endpoint.rs} (57%) rename capture/src/{event.rs => v0_request.rs} (60%) diff --git a/Cargo.toml b/Cargo.toml index f77c1fbd85ad2..ef70e645d4e13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ anyhow = "1.0" assert-json-diff = "2.0.2" async-trait = "0.1.74" -axum = { version = "0.7.5", features = ["http2", "macros"] } +axum = { version = "0.7.5", features = ["http2", "macros", "matched-path"] } axum-client-ip = "0.6.0" base64 = "0.22.0" bytes = "1" diff --git a/capture/src/api.rs b/capture/src/api.rs index b27b1a9630c45..0938ced399773 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -1,19 +1,11 @@ -use crate::token::InvalidTokenReason; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; use thiserror::Error; +use time::OffsetDateTime; +use uuid::Uuid; -#[derive(Debug, Deserialize, Serialize)] -pub struct CaptureRequest { - #[serde(alias = "$token", alias = "api_key")] - pub token: String, - - pub event: String, - pub properties: HashMap, -} +use crate::token::InvalidTokenReason; #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum CaptureResponseCode { @@ -84,3 +76,21 @@ impl IntoResponse for CaptureError { .into_response() } } + +#[derive(Clone, Default, Debug, Serialize, Eq, PartialEq)] +pub struct ProcessedEvent { + pub uuid: Uuid, + pub distinct_id: String, + pub ip: String, + pub data: String, + pub now: String, + #[serde(with = "time::serde::rfc3339::option")] + pub sent_at: Option, + pub token: String, +} + +impl ProcessedEvent { + pub fn key(&self) -> String { + format!("{}:{}", self.token, self.distinct_id) + } +} diff --git a/capture/src/lib.rs b/capture/src/lib.rs index 058e994186edb..176fc6f09963e 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -1,7 +1,5 @@ pub mod api; -pub mod capture; pub mod config; -pub mod event; pub mod health; pub mod limiters; pub mod prometheus; @@ -12,3 +10,5 @@ pub mod sinks; pub mod time; pub mod token; pub mod utils; +pub mod v0_endpoint; +pub mod v0_request; diff --git a/capture/src/router.rs b/capture/src/router.rs index d02e63faaad5d..85475ceb2a990 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -10,7 +10,9 @@ use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; use tower_http::trace::TraceLayer; use crate::health::HealthRegistry; -use crate::{capture, limiters::billing::BillingLimiter, redis::Client, sinks, time::TimeSource}; +use crate::{ + limiters::billing::BillingLimiter, redis::Client, sinks, time::TimeSource, v0_endpoint, +}; use crate::prometheus::{setup_metrics_recorder, track_metrics}; @@ -58,17 +60,41 @@ pub fn router< .route("/", get(index)) .route("/_readiness", get(index)) .route("/_liveness", get(move || ready(liveness.get_status()))) + .route( + "/e", + post(v0_endpoint::event) + .get(v0_endpoint::event) + .options(v0_endpoint::options), + ) + .route( + "/e/", + post(v0_endpoint::event) + .get(v0_endpoint::event) + .options(v0_endpoint::options), + ) + .route( + "/batch", + post(v0_endpoint::event) + .get(v0_endpoint::event) + .options(v0_endpoint::options), + ) + .route( + "/batch/", + post(v0_endpoint::event) + .get(v0_endpoint::event) + .options(v0_endpoint::options), + ) .route( "/i/v0/e", - post(capture::event) - .get(capture::event) - .options(capture::options), + post(v0_endpoint::event) + .get(v0_endpoint::event) + .options(v0_endpoint::options), ) .route( "/i/v0/e/", - post(capture::event) - .get(capture::event) - .options(capture::options), + post(v0_endpoint::event) + .get(v0_endpoint::event) + .options(v0_endpoint::options), ) .layer(TraceLayer::new_for_http()) .layer(cors) diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index 4a2bd94f7e30f..4a48b6e4d5335 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -10,9 +10,8 @@ use tokio::task::JoinSet; use tracing::log::{debug, error, info}; use tracing::{info_span, instrument, Instrument}; -use crate::api::CaptureError; +use crate::api::{CaptureError, ProcessedEvent}; use crate::config::KafkaConfig; -use crate::event::ProcessedEvent; use crate::health::HealthHandle; use crate::limiters::overflow::OverflowLimiter; use crate::prometheus::report_dropped_events; @@ -259,9 +258,8 @@ impl Event for KafkaSink { #[cfg(test)] mod tests { - use crate::api::CaptureError; + use crate::api::{CaptureError, ProcessedEvent}; use crate::config; - use crate::event::ProcessedEvent; use crate::health::HealthRegistry; use crate::limiters::overflow::OverflowLimiter; use crate::sinks::kafka::KafkaSink; diff --git a/capture/src/sinks/mod.rs b/capture/src/sinks/mod.rs index 0747f0e222a79..bedbcbc8df69d 100644 --- a/capture/src/sinks/mod.rs +++ b/capture/src/sinks/mod.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; -use crate::api::CaptureError; -use crate::event::ProcessedEvent; +use crate::api::{CaptureError, ProcessedEvent}; pub mod kafka; pub mod print; diff --git a/capture/src/sinks/print.rs b/capture/src/sinks/print.rs index 5e71899ce68c5..7845a3d039b56 100644 --- a/capture/src/sinks/print.rs +++ b/capture/src/sinks/print.rs @@ -2,8 +2,7 @@ use async_trait::async_trait; use metrics::{counter, histogram}; use tracing::log::info; -use crate::api::CaptureError; -use crate::event::ProcessedEvent; +use crate::api::{CaptureError, ProcessedEvent}; use crate::sinks::Event; pub struct PrintSink {} diff --git a/capture/src/capture.rs b/capture/src/v0_endpoint.rs similarity index 57% rename from capture/src/capture.rs rename to capture/src/v0_endpoint.rs index 622bc750f90c7..3862995f2f4f8 100644 --- a/capture/src/capture.rs +++ b/capture/src/v0_endpoint.rs @@ -1,41 +1,45 @@ -use std::collections::HashSet; use std::ops::Deref; use std::sync::Arc; -use bytes::Bytes; - use axum::{debug_handler, Json}; +use bytes::Bytes; // TODO: stream this instead -use axum::extract::{Query, State}; +use axum::extract::{MatchedPath, Query, State}; use axum::http::{HeaderMap, Method}; use axum_client_ip::InsecureClientIp; use base64::Engine; use metrics::counter; - -use time::OffsetDateTime; use tracing::instrument; -use crate::event::{Compression, ProcessingContext}; use crate::limiters::billing::QuotaResource; use crate::prometheus::report_dropped_events; -use crate::token::validate_token; +use crate::v0_request::{Compression, ProcessingContext, RawRequest}; use crate::{ - api::{CaptureError, CaptureResponse, CaptureResponseCode}, - event::{EventFormData, EventQuery, ProcessedEvent, RawEvent}, + api::{CaptureError, CaptureResponse, CaptureResponseCode, ProcessedEvent}, router, sinks, utils::uuid_v7, + v0_request::{EventFormData, EventQuery, RawEvent}, }; +/// Flexible endpoint that targets wide compatibility with the wide range of requests +/// currently processed by posthog-events (analytics events capture). Replay is out +/// of scope and should be processed on a separate endpoint. +/// +/// Because it must accommodate several shapes, it is inefficient in places. A v1 +/// endpoint should be created, that only accepts the BatchedRequest payload shape. + #[instrument( skip_all, fields( + path, token, batch_size, user_agent, content_encoding, content_type, version, - compression + compression, + is_historical ) )] #[debug_handler] @@ -45,11 +49,9 @@ pub async fn event( meta: Query, headers: HeaderMap, method: Method, + path: MatchedPath, body: Bytes, ) -> Result, CaptureError> { - // content-type - // user-agent - let user_agent = headers .get("user-agent") .map_or("unknown", |v| v.to_str().unwrap_or("unknown")); @@ -68,8 +70,9 @@ pub async fn event( tracing::Span::current().record("version", meta.lib_version.clone()); tracing::Span::current().record("compression", comp.as_str()); tracing::Span::current().record("method", method.as_str()); + tracing::Span::current().record("path", path.as_str().trim_end_matches('/')); - let events = match headers + let request = match headers .get("content-type") .map_or("", |v| v.to_str().unwrap_or("")) { @@ -86,41 +89,36 @@ pub async fn event( tracing::error!("failed to decode form data: {}", e); CaptureError::RequestDecodingError(String::from("missing data field")) })?; - RawEvent::from_bytes(payload.into()) + RawRequest::from_bytes(payload.into()) } ct => { tracing::Span::current().record("content_type", ct); - RawEvent::from_bytes(body) + RawRequest::from_bytes(body) } }?; + let sent_at = request.sent_at().or(meta.sent_at()); + let token = match request.extract_and_verify_token() { + Ok(token) => token, + Err(err) => { + report_dropped_events("token_shape_invalid", request.events().len() as u64); + return Err(err); + } + }; + let is_historical = request.is_historical(); // TODO: use to write to historical topic + let events = request.events(); // Takes ownership of request + + tracing::Span::current().record("token", &token); + tracing::Span::current().record("is_historical", is_historical); tracing::Span::current().record("batch_size", events.len()); if events.is_empty() { return Err(CaptureError::EmptyBatch); } - let token = extract_and_verify_token(&events).map_err(|err| { - report_dropped_events("token_shape_invalid", events.len() as u64); - err - })?; - - tracing::Span::current().record("token", &token); - counter!("capture_events_received_total").increment(events.len() as u64); - let sent_at = meta.sent_at.and_then(|value| { - let value_nanos: i128 = i128::from(value) * 1_000_000; // Assuming the value is in milliseconds, latest posthog-js releases - if let Ok(sent_at) = OffsetDateTime::from_unix_timestamp_nanos(value_nanos) { - if sent_at.year() > 2020 { - // Could be lower if the input is in seconds - return Some(sent_at); - } - } - None - }); - let context = ProcessingContext { lib_version: meta.lib_version.clone(), sent_at, @@ -192,28 +190,6 @@ pub fn process_single_event( }) } -#[instrument(skip_all, fields(events = events.len()))] -pub fn extract_and_verify_token(events: &[RawEvent]) -> Result { - let distinct_tokens: HashSet> = HashSet::from_iter( - events - .iter() - .map(RawEvent::extract_token) - .filter(Option::is_some), - ); - - return match distinct_tokens.len() { - 0 => Err(CaptureError::NoTokenError), - 1 => match distinct_tokens.iter().last() { - Some(Some(token)) => { - validate_token(token)?; - Ok(token.clone()) - } - _ => Err(CaptureError::NoTokenError), - }, - _ => Err(CaptureError::MultipleTokensError), - }; -} - #[instrument(skip_all, fields(events = events.len()))] pub async fn process_events<'a>( sink: Arc, @@ -233,73 +209,3 @@ pub async fn process_events<'a>( sink.send_batch(events).await } } - -#[cfg(test)] -mod tests { - use crate::capture::extract_and_verify_token; - use crate::event::RawEvent; - use serde_json::json; - use std::collections::HashMap; - - #[tokio::test] - async fn all_events_have_same_token() { - let events = vec![ - RawEvent { - token: Some(String::from("hello")), - distinct_id: Some(json!("testing")), - uuid: None, - event: String::new(), - properties: HashMap::new(), - timestamp: None, - offset: None, - set: Default::default(), - set_once: Default::default(), - }, - RawEvent { - token: None, - distinct_id: Some(json!("testing")), - uuid: None, - event: String::new(), - properties: HashMap::from([(String::from("token"), json!("hello"))]), - timestamp: None, - offset: None, - set: Default::default(), - set_once: Default::default(), - }, - ]; - - let processed = extract_and_verify_token(&events); - assert_eq!(processed.is_ok(), true, "{:?}", processed); - } - - #[tokio::test] - async fn all_events_have_different_token() { - let events = vec![ - RawEvent { - token: Some(String::from("hello")), - distinct_id: Some(json!("testing")), - uuid: None, - event: String::new(), - properties: HashMap::new(), - timestamp: None, - offset: None, - set: Default::default(), - set_once: Default::default(), - }, - RawEvent { - token: None, - distinct_id: Some(json!("testing")), - uuid: None, - event: String::new(), - properties: HashMap::from([(String::from("token"), json!("goodbye"))]), - timestamp: None, - offset: None, - set: Default::default(), - set_once: Default::default(), - }, - ]; - - let processed = extract_and_verify_token(&events); - assert_eq!(processed.is_err(), true); - } -} diff --git a/capture/src/event.rs b/capture/src/v0_request.rs similarity index 60% rename from capture/src/event.rs rename to capture/src/v0_request.rs index ea71a3f276704..3d0052e0c072c 100644 --- a/capture/src/event.rs +++ b/capture/src/v0_request.rs @@ -1,15 +1,17 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::io::prelude::*; use bytes::{Buf, Bytes}; use flate2::read::GzDecoder; use serde::{Deserialize, Serialize}; use serde_json::Value; +use time::format_description::well_known::Iso8601; use time::OffsetDateTime; use tracing::instrument; use uuid::Uuid; use crate::api::CaptureError; +use crate::token::validate_token; #[derive(Deserialize, Default)] pub enum Compression { @@ -28,7 +30,25 @@ pub struct EventQuery { pub lib_version: Option, #[serde(alias = "_")] - pub sent_at: Option, + sent_at: Option, +} + +impl EventQuery { + /// Returns the parsed value of the sent_at timestamp if present in the query params. + /// We only support the format sent by recent posthog-js versions, in milliseconds integer. + /// Values in seconds integer (older SDKs will be ignored). + pub fn sent_at(&self) -> Option { + if let Some(value) = self.sent_at { + let value_nanos: i128 = i128::from(value) * 1_000_000; // Assuming the value is in milliseconds, latest posthog-js releases + if let Ok(sent_at) = OffsetDateTime::from_unix_timestamp_nanos(value_nanos) { + if sent_at.year() > 2020 { + // Could be lower if the input is in seconds + return Some(sent_at); + } + } + } + None + } } #[derive(Debug, Deserialize)] @@ -64,30 +84,32 @@ static GZIP_MAGIC_NUMBERS: [u8; 3] = [0x1f, 0x8b, 8]; #[derive(Deserialize)] #[serde(untagged)] -enum RawRequest { - /// Batch of events - Batch(Vec), - /// Single event +pub enum RawRequest { + /// Array of events (posthog-js) + Array(Vec), + /// Batched events (/batch) + Batch(BatchedRequest), + /// Single event (/capture) One(Box), } -impl RawRequest { - pub fn events(self) -> Vec { - match self { - RawRequest::Batch(events) => events, - RawRequest::One(event) => vec![*event], - } - } +#[derive(Deserialize)] +pub struct BatchedRequest { + #[serde(alias = "api_key")] + pub token: String, + pub historical_migration: Option, + pub sent_at: Option, + pub batch: Vec, } -impl RawEvent { - /// Takes a request payload and tries to decompress and unmarshall it into events. +impl RawRequest { + /// Takes a request payload and tries to decompress and unmarshall it. /// While posthog-js sends a compression query param, a sizable portion of requests /// fail due to it being missing when the body is compressed. /// Instead of trusting the parameter, we peek at the payload's first three bytes to /// detect gzip, fallback to uncompressed utf8 otherwise. #[instrument(skip_all)] - pub fn from_bytes(bytes: Bytes) -> Result, CaptureError> { + pub fn from_bytes(bytes: Bytes) -> Result { tracing::debug!(len = bytes.len(), "decoding new event"); let payload = if bytes.starts_with(&GZIP_MAGIC_NUMBERS) { @@ -106,9 +128,66 @@ impl RawEvent { }; tracing::debug!(json = payload, "decoded event data"); - Ok(serde_json::from_str::(&payload)?.events()) + Ok(serde_json::from_str::(&payload)?) } + pub fn events(self) -> Vec { + match self { + RawRequest::Array(events) => events, + RawRequest::One(event) => vec![*event], + RawRequest::Batch(req) => req.batch, + } + } + + pub fn extract_and_verify_token(&self) -> Result { + let token = match self { + RawRequest::Batch(req) => req.token.to_string(), + RawRequest::One(event) => event.extract_token().ok_or(CaptureError::NoTokenError)?, + RawRequest::Array(events) => extract_token(events)?, + }; + validate_token(&token)?; + Ok(token) + } + + pub fn is_historical(&self) -> bool { + match self { + RawRequest::Batch(req) => req.historical_migration.unwrap_or_default(), + _ => false, + } + } + + pub fn sent_at(&self) -> Option { + if let RawRequest::Batch(req) = &self { + if let Some(value) = &req.sent_at { + if let Ok(parsed) = OffsetDateTime::parse(value, &Iso8601::DEFAULT) { + return Some(parsed); + } + } + } + None + } +} + +#[instrument(skip_all, fields(events = events.len()))] +pub fn extract_token(events: &[RawEvent]) -> Result { + let distinct_tokens: HashSet> = HashSet::from_iter( + events + .iter() + .map(RawEvent::extract_token) + .filter(Option::is_some), + ); + + return match distinct_tokens.len() { + 0 => Err(CaptureError::NoTokenError), + 1 => match distinct_tokens.iter().last() { + Some(Some(token)) => Ok(token.clone()), + _ => Err(CaptureError::NoTokenError), + }, + _ => Err(CaptureError::MultipleTokensError), + }; +} + +impl RawEvent { pub fn extract_token(&self) -> Option { match &self.token { Some(value) => Some(value.clone()), @@ -154,26 +233,9 @@ pub struct ProcessingContext { pub client_ip: String, } -#[derive(Clone, Default, Debug, Serialize, Eq, PartialEq)] -pub struct ProcessedEvent { - pub uuid: Uuid, - pub distinct_id: String, - pub ip: String, - pub data: String, - pub now: String, - #[serde(with = "time::serde::rfc3339::option")] - pub sent_at: Option, - pub token: String, -} - -impl ProcessedEvent { - pub fn key(&self) -> String { - format!("{}:{}", self.token, self.distinct_id) - } -} - #[cfg(test)] mod tests { + use crate::token::InvalidTokenReason; use base64::Engine as _; use bytes::Bytes; use rand::distributions::Alphanumeric; @@ -181,7 +243,7 @@ mod tests { use serde_json::json; use super::CaptureError; - use super::RawEvent; + use super::RawRequest; #[test] fn decode_uncompressed_raw_event() { @@ -192,7 +254,9 @@ mod tests { .expect("payload is not base64"), ); - let events = RawEvent::from_bytes(compressed_bytes).expect("failed to parse"); + let events = RawRequest::from_bytes(compressed_bytes) + .expect("failed to parse") + .events(); assert_eq!(1, events.len()); assert_eq!(Some("my_token1".to_string()), events[0].extract_token()); assert_eq!("my_event1".to_string(), events[0].event); @@ -212,7 +276,9 @@ mod tests { .expect("payload is not base64"), ); - let events = RawEvent::from_bytes(compressed_bytes).expect("failed to parse"); + let events = RawRequest::from_bytes(compressed_bytes) + .expect("failed to parse") + .events(); assert_eq!(1, events.len()); assert_eq!(Some("my_token2".to_string()), events[0].extract_token()); assert_eq!("my_event2".to_string(), events[0].event); @@ -227,7 +293,9 @@ mod tests { #[test] fn extract_distinct_id() { let parse_and_extract = |input: &'static str| -> Result { - let parsed = RawEvent::from_bytes(input.into()).expect("failed to parse"); + let parsed = RawRequest::from_bytes(input.into()) + .expect("failed to parse") + .events(); parsed[0].extract_distinct_id() }; // Return MissingDistinctId if not found @@ -288,10 +356,72 @@ mod tests { "distinct_id": distinct_id }]); - let parsed = RawEvent::from_bytes(input.to_string().into()).expect("failed to parse"); + let parsed = RawRequest::from_bytes(input.to_string().into()) + .expect("failed to parse") + .events(); assert_eq!( parsed[0].extract_distinct_id().expect("failed to extract"), expected_distinct_id ); } + + #[test] + fn extract_and_verify_token() { + let parse_and_extract = |input: &'static str| -> Result { + RawRequest::from_bytes(input.into()) + .expect("failed to parse") + .extract_and_verify_token() + }; + + let assert_extracted_token = |input: &'static str, expected: &str| { + let id = parse_and_extract(input).expect("failed to extract"); + assert_eq!(id, expected); + }; + + // Return NoTokenError if not found + assert!(matches!( + parse_and_extract(r#"{"event": "e"}"#), + Err(CaptureError::NoTokenError) + )); + + // Return TokenValidationError if token empty + assert!(matches!( + parse_and_extract(r#"{"api_key": "", "batch":[{"event": "e"}]}"#), + Err(CaptureError::TokenValidationError( + InvalidTokenReason::Empty + )) + )); + + // Return TokenValidationError if personal apikey + assert!(matches!( + parse_and_extract(r#"[{"event": "e", "token": "phx_hellothere"}]"#), + Err(CaptureError::TokenValidationError( + InvalidTokenReason::PersonalApiKey + )) + )); + + // Return MultipleTokensError if tokens don't match in array + assert!(matches!( + parse_and_extract( + r#"[{"event": "e", "token": "token1"},{"event": "e", "token": "token2"}]"# + ), + Err(CaptureError::MultipleTokensError) + )); + + // Return token from array if consistent + assert_extracted_token( + r#"[{"event":"e","token":"token1"},{"event":"e","token":"token1"}]"#, + "token1", + ); + + // Return token from batch if present + assert_extracted_token( + r#"{"batch":[{"event":"e","token":"token1"}],"api_key":"batched"}"#, + "batched", + ); + + // Return token from single event if present + assert_extracted_token(r#"{"event":"e","$token":"single_token"}"#, "single_token"); + assert_extracted_token(r#"{"event":"e","api_key":"single_token"}"#, "single_token"); + } } diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index 5d778997a89a9..c7ec0ad8d2770 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -4,8 +4,7 @@ use axum::http::StatusCode; use axum_test_helper::TestClient; use base64::engine::general_purpose; use base64::Engine; -use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode}; -use capture::event::ProcessedEvent; +use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode, ProcessedEvent}; use capture::health::HealthRegistry; use capture::limiters::billing::BillingLimiter; use capture::redis::MockRedisClient; @@ -88,11 +87,6 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { continue; } let case: RequestDump = serde_json::from_str(&line_contents)?; - if !case.path.starts_with("/e/") { - println!("Skipping {} test case", &case.path); - continue; - } - let raw_body = general_purpose::STANDARD.decode(&case.body)?; assert_eq!( case.method, "POST", @@ -117,7 +111,7 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { ); let client = TestClient::new(app); - let mut req = client.post(&format!("/i/v0{}", case.path)).body(raw_body); + let mut req = client.post(&case.path).body(raw_body); if !case.content_encoding.is_empty() { req = req.header("Content-encoding", case.content_encoding); } @@ -164,8 +158,15 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { if let Some(expected_data) = expected.get_mut("data") { // Data is a serialized JSON map. Unmarshall both and compare them, // instead of expecting the serialized bytes to be equal - let expected_props: Value = + let mut expected_props: Value = serde_json::from_str(expected_data.as_str().expect("not str"))?; + if let Some(object) = expected_props.as_object_mut() { + // toplevel fields added by posthog-node that plugin-server will ignore anyway + object.remove("type"); + object.remove("library"); + object.remove("library_version"); + } + let found_props: Value = serde_json::from_str(&message.data)?; let match_config = assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict); diff --git a/capture/tests/requests_dump.jsonl b/capture/tests/requests_dump.jsonl index ec0f4df482afb..36cf8ade36439 100644 --- a/capture/tests/requests_dump.jsonl +++ b/capture/tests/requests_dump.jsonl @@ -13,3 +13,5 @@ ### Compression query param mismatch, to confirm gzip autodetection {"path":"/e/?compression=gzip-js&ip=1&_=1694769302319&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.321230+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l5WlMwM1ltUXpMV0ppTXpBdE5qWXhOalkxTjJNek9HWmhJaXdpWlhabGJuUWlPaUlrYjNCMFgybHVJaXdpY0hKdmNHVnlkR2xsY3lJNmV5SWtiM01pT2lKTllXTWdUMU1nV0NJc0lpUnZjMTkyWlhKemFXOXVJam9pTVRBdU1UVXVNQ0lzSWlSaWNtOTNjMlZ5SWpvaVJtbHlaV1p2ZUNJc0lpUmtaWFpwWTJWZmRIbHdaU0k2SWtSbGMydDBiM0FpTENJa1kzVnljbVZ1ZEY5MWNtd2lPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPamd3TURBdklpd2lKR2h2YzNRaU9pSnNiMk5oYkdodmMzUTZPREF3TUNJc0lpUndZWFJvYm1GdFpTSTZJaThpTENJa1luSnZkM05sY2w5MlpYSnphVzl1SWpveE1UY3NJaVJpY205M2MyVnlYMnhoYm1kMVlXZGxJam9pWlc0dFZWTWlMQ0lrYzJOeVpXVnVYMmhsYVdkb2RDSTZNVEExTWl3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRZeU1Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPVEV4TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFMU5EZ3NJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpjNExqVWlMQ0lrYVc1elpYSjBYMmxrSWpvaU1XVTRaV1p5WkdSbE1HSTBNV2RtYkNJc0lpUjBhVzFsSWpveE5qazBOelk1TXpBeUxqTXhPQ3dpWkdsemRHbHVZM1JmYVdRaU9pSXdNVGhoT1RneFppMDBZakprTFRjNFlXVXRPVFEzWWkxaVpXUmlZV0V3TW1Fd1pqUWlMQ0lrWkdWMmFXTmxYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaQzAzT0dGbExUazBOMkl0WW1Wa1ltRmhNREpoTUdZMElpd2lKSEpsWm1WeWNtVnlJam9pSkdScGNtVmpkQ0lzSWlSeVpXWmxjbkpwYm1kZlpHOXRZV2x1SWpvaUpHUnBjbVZqZENJc0luUnZhMlZ1SWpvaWNHaGpYM0ZuVlVad05YZDZNa0pxUXpSU2MwWnFUVWR3VVROUVIwUnljMmsyVVRCRFJ6QkRVRFJPUVdGak1Fa2lMQ0lrYzJWemMybHZibDlwWkNJNklqQXhPR0U1T0RGbUxUUmlNbVV0TjJKa015MWlZak13TFRZMk1UZGlaalE0T0RnMk9TSXNJaVIzYVc1a2IzZGZhV1FpT2lJd01UaGhPVGd4WmkwMFlqSmxMVGRpWkRNdFltSXpNQzAyTmpFNE1UQm1aV1EyTldZaWZTd2lkR2x0WlhOMFlXMXdJam9pTWpBeU15MHdPUzB4TlZRd09Ub3hOVG93TWk0ek1UaGFJbjAlM0Q=","output":[{"uuid":"018a981f-4b2e-7bd3-bb30-6616657c38fa","distinct_id":"018a981f-4b2d-78ae-947b-bedbaa02a0f4","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b2e-7bd3-bb30-6616657c38fa\", \"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"1e8efrdde0b41gfl\", \"$time\": 1694769302.318, \"distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"timestamp\": \"2023-09-15T09:15:02.318Z\"}","now":"2023-09-15T09:15:02.321230+00:00","sent_at":"2023-09-15T09:15:02.319000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?ip=1&_=1694769329412&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:29.500667+00:00","body":"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","output":[{"uuid":"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0ovdk9xlnv9fhfak\", \"$time\": 1694769326.837, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"persons-related-flags-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Feature flags\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 74.26666259765625px; --lemon-tabs-slider-offset: 176.29998779296875px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2572}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-aafa-79e8-abf4-4e68eb64c30f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aafa-79e8-abf4-4e68eb64c30f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"y3x76ea6mqqi2q0a\", \"$time\": 1694769326.842, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 2567}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af3d-79d4-be4a-d729c776e0da","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af3d-79d4-be4a-d729c776e0da\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ltw7rl4fhi4bgq63\", \"$time\": 1694769327.934, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 86.23332214355469px; --lemon-tabs-slider-offset: 367.0999755859375px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1475}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af46-7832-9a69-c566751c6625","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af46-7832-9a69-c566751c6625\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"6yl35wdtv5v11o6c\", \"$time\": 1694769327.942, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 1467}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-b008-737a-bb40-6a6a81ba2244","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-b008-737a-bb40-6a6a81ba2244\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"8guwvm82yhwyhfo6\", \"$time\": 1694769328.136, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"EventsQuery\", \"select\": [\"*\", \"event\", \"person\", \"coalesce(properties.$current_url, properties.$screen_name) -- Url / Screen\", \"properties.$lib\", \"timestamp\"], \"personId\": \"018a981f-4c9a-0000-68ff-417481f7c5b5\", \"after\": \"-24h\"}, \"duration\": 171, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1273}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} +### nodejs, default params +{"path":"/batch/","method":"POST","content_encoding":"","content_type":"application/json","ip":"127.0.0.1","now":"2024-04-17T14:40:56.918578+00:00","body":"eyJhcGlfa2V5IjoicGhjX05WWk95WWI4aFgyR0RzODBtUWQwNzJPVDBXVXJBYmRRb200WGVDVjVOZmgiLCJiYXRjaCI6W3siZGlzdGluY3RfaWQiOiJpZDEiLCJldmVudCI6InRoaXMgZXZlbnQiLCJwcm9wZXJ0aWVzIjp7IiRsaWIiOiJwb3N0aG9nLW5vZGUiLCIkbGliX3ZlcnNpb24iOiI0LjAuMCIsIiRnZW9pcF9kaXNhYmxlIjp0cnVlfSwidHlwZSI6ImNhcHR1cmUiLCJsaWJyYXJ5IjoicG9zdGhvZy1ub2RlIiwibGlicmFyeV92ZXJzaW9uIjoiNC4wLjAiLCJ0aW1lc3RhbXAiOiIyMDI0LTA0LTE3VDE0OjQwOjU2LjkwMFoiLCJ1dWlkIjoiMDE4ZWVjODAtZjA0NC03YzJkLWI5NjYtMzVlZmM4ZWQ2MWQ1In0seyJkaXN0aW5jdF9pZCI6ImRpc3RpbmN0X2lkX29mX3RoZV91c2VyIiwiZXZlbnQiOiJ1c2VyIHNpZ25lZCB1cCIsInByb3BlcnRpZXMiOnsibG9naW5fdHlwZSI6ImVtYWlsIiwiaXNfZnJlZV90cmlhbCI6dHJ1ZSwiJGxpYiI6InBvc3Rob2ctbm9kZSIsIiRsaWJfdmVyc2lvbiI6IjQuMC4wIiwiJGdlb2lwX2Rpc2FibGUiOnRydWV9LCJ0eXBlIjoiY2FwdHVyZSIsImxpYnJhcnkiOiJwb3N0aG9nLW5vZGUiLCJsaWJyYXJ5X3ZlcnNpb24iOiI0LjAuMCIsInRpbWVzdGFtcCI6IjIwMjQtMDQtMTdUMTQ6NDA6NTYuOTAwWiIsInV1aWQiOiIwMThlZWM4MC1mMDQ0LTdjMmQtYjk2Ni0zNWYwNTA1OWYzNzUifV0sInNlbnRfYXQiOiIyMDI0LTA0LTE3VDE0OjQwOjU2LjkwMFoifQ==","output":[{"uuid":"018eec80-f044-7c2d-b966-35efc8ed61d5","distinct_id":"id1","ip":"127.0.0.1","site_url":"http://127.0.0.1:8000","data":"{\"distinct_id\": \"id1\", \"event\": \"this event\", \"properties\": {\"$lib\": \"posthog-node\", \"$lib_version\": \"4.0.0\", \"$geoip_disable\": true}, \"type\": \"capture\", \"library\": \"posthog-node\", \"library_version\": \"4.0.0\", \"timestamp\": \"2024-04-17T14:40:56.900Z\", \"uuid\": \"018eec80-f044-7c2d-b966-35efc8ed61d5\"}","now":"2024-04-17T14:40:56.918578+00:00","sent_at":"2024-04-17T14:40:56.900000+00:00","token":"phc_NVZOyYb8hX2GDs80mQd072OT0WUrAbdQom4XeCV5Nfh"},{"uuid":"018eec80-f044-7c2d-b966-35f05059f375","distinct_id":"distinct_id_of_the_user","ip":"127.0.0.1","site_url":"http://127.0.0.1:8000","data":"{\"distinct_id\": \"distinct_id_of_the_user\", \"event\": \"user signed up\", \"properties\": {\"login_type\": \"email\", \"is_free_trial\": true, \"$lib\": \"posthog-node\", \"$lib_version\": \"4.0.0\", \"$geoip_disable\": true}, \"type\": \"capture\", \"library\": \"posthog-node\", \"library_version\": \"4.0.0\", \"timestamp\": \"2024-04-17T14:40:56.900Z\", \"uuid\": \"018eec80-f044-7c2d-b966-35f05059f375\"}","now":"2024-04-17T14:40:56.918578+00:00","sent_at":"2024-04-17T14:40:56.900000+00:00","token":"phc_NVZOyYb8hX2GDs80mQd072OT0WUrAbdQom4XeCV5Nfh"}]} \ No newline at end of file From 0d48419bdd584d424ba5f9241b9242628520e3e4 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 24 Apr 2024 17:30:11 +0200 Subject: [PATCH 223/249] common: refactor health into its own lib crate (#23) --- Cargo.lock | 17 +- Cargo.toml | 7 +- capture/Cargo.toml | 33 +- capture/src/lib.rs | 1 - capture/src/router.rs | 2 +- capture/src/server.rs | 3 +- capture/src/sinks/kafka.rs | 4 +- capture/tests/django_compat.rs | 2 +- common/README.md | 8 + common/health/Cargo.toml | 12 + .../src/health.rs => common/health/src/lib.rs | 8 +- hook-common/Cargo.toml | 2 +- hook-common/src/health.rs | 346 ------------------ hook-common/src/lib.rs | 1 - hook-janitor/Cargo.toml | 3 +- hook-janitor/src/handlers/app.rs | 2 +- hook-janitor/src/kafka_producer.rs | 2 +- hook-janitor/src/main.rs | 2 +- hook-janitor/src/webhooks.rs | 2 +- hook-worker/Cargo.toml | 5 +- hook-worker/src/main.rs | 2 +- hook-worker/src/worker.rs | 4 +- 22 files changed, 79 insertions(+), 389 deletions(-) create mode 100644 common/README.md create mode 100644 common/health/Cargo.toml rename capture/src/health.rs => common/health/src/lib.rs (98%) delete mode 100644 hook-common/src/health.rs diff --git a/Cargo.lock b/Cargo.lock index 71cbc8d51a5d1..82c787aded2cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,7 @@ dependencies = [ "envconfig", "flate2", "governor", + "health", "metrics", "metrics-exporter-prometheus", "rand", @@ -450,9 +451,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "futures-core", @@ -1001,6 +1002,16 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "health" +version = "0.1.0" +dependencies = [ + "axum 0.7.5", + "time", + "tokio", + "tracing", +] + [[package]] name = "heck" version = "0.4.1" @@ -1100,6 +1111,7 @@ dependencies = [ "envconfig", "eyre", "futures", + "health", "hook-common", "metrics", "rdkafka", @@ -1120,6 +1132,7 @@ dependencies = [ "chrono", "envconfig", "futures", + "health", "hook-common", "http 1.1.0", "metrics", diff --git a/Cargo.toml b/Cargo.toml index ef70e645d4e13..cb4aa8e58e88d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,11 @@ resolver = "2" members = [ "capture", "capture-server", - "hook-common", + "common/health", "hook-api", - "hook-worker", + "hook-common", "hook-janitor", + "hook-worker", ] [workspace.dependencies] @@ -44,13 +45,13 @@ sqlx = { version = "0.7", features = [ "tls-native-tls", "uuid", ] } -thiserror = { version = "1.0" } time = { version = "0.3.20", features = [ "formatting", "macros", "parsing", "serde", ] } +thiserror = { version = "1.0" } tokio = { version = "1.34.0", features = ["full"] } tower = "0.4.13" tower-http = { version = "0.5.2", features = ["cors", "trace"] } diff --git a/capture/Cargo.toml b/capture/Cargo.toml index 9ec0f97fb3ddb..02ada312932c1 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -6,33 +6,34 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } axum = { workspace = true } axum-client-ip = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -governor = { workspace = true } -time = { workspace = true } -tower-http = { workspace = true } +base64 = { workspace = true } bytes = { workspace = true } -anyhow = { workspace = true } +envconfig = { workspace = true } flate2 = { workspace = true } -base64 = { workspace = true } -uuid = { workspace = true } -async-trait = { workspace = true } -serde_urlencoded = { workspace = true } -rand = { workspace = true } -rdkafka = { workspace = true } +governor = { workspace = true } +health = { path = "../common/health" } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } -thiserror = { workspace = true } +rand = { workspace = true } +rdkafka = { workspace = true } redis = { version = "0.23.3", features = [ "tokio-comp", "cluster", "cluster-async", ] } -envconfig = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_urlencoded = { workspace = true } +thiserror = { workspace = true } +time = { workspace = true } +tokio = { workspace = true } +tower-http = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true } [dev-dependencies] assert-json-diff = { workspace = true } diff --git a/capture/src/lib.rs b/capture/src/lib.rs index 176fc6f09963e..d5d47dd9ea421 100644 --- a/capture/src/lib.rs +++ b/capture/src/lib.rs @@ -1,6 +1,5 @@ pub mod api; pub mod config; -pub mod health; pub mod limiters; pub mod prometheus; pub mod redis; diff --git a/capture/src/router.rs b/capture/src/router.rs index 85475ceb2a990..0710302549def 100644 --- a/capture/src/router.rs +++ b/capture/src/router.rs @@ -6,10 +6,10 @@ use axum::{ routing::{get, post}, Router, }; +use health::HealthRegistry; use tower_http::cors::{AllowHeaders, AllowOrigin, CorsLayer}; use tower_http::trace::TraceLayer; -use crate::health::HealthRegistry; use crate::{ limiters::billing::BillingLimiter, redis::Client, sinks, time::TimeSource, v0_endpoint, }; diff --git a/capture/src/server.rs b/capture/src/server.rs index 2fc88c60687e6..07049874edef9 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -2,11 +2,12 @@ use std::future::Future; use std::net::SocketAddr; use std::sync::Arc; +use health::{ComponentStatus, HealthRegistry}; use time::Duration; use tokio::net::TcpListener; use crate::config::Config; -use crate::health::{ComponentStatus, HealthRegistry}; + use crate::limiters::billing::BillingLimiter; use crate::limiters::overflow::OverflowLimiter; use crate::redis::RedisClient; diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index 4a48b6e4d5335..8b286e84d8838 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -1,6 +1,7 @@ use std::time::Duration; use async_trait::async_trait; +use health::HealthHandle; use metrics::{counter, gauge, histogram}; use rdkafka::error::{KafkaError, RDKafkaErrorCode}; use rdkafka::producer::{DeliveryFuture, FutureProducer, FutureRecord, Producer}; @@ -12,7 +13,6 @@ use tracing::{info_span, instrument, Instrument}; use crate::api::{CaptureError, ProcessedEvent}; use crate::config::KafkaConfig; -use crate::health::HealthHandle; use crate::limiters::overflow::OverflowLimiter; use crate::prometheus::report_dropped_events; use crate::sinks::Event; @@ -260,11 +260,11 @@ impl Event for KafkaSink { mod tests { use crate::api::{CaptureError, ProcessedEvent}; use crate::config; - use crate::health::HealthRegistry; use crate::limiters::overflow::OverflowLimiter; use crate::sinks::kafka::KafkaSink; use crate::sinks::Event; use crate::utils::uuid_v7; + use health::HealthRegistry; use rand::distributions::Alphanumeric; use rand::Rng; use rdkafka::mocking::MockCluster; diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index c7ec0ad8d2770..9ef4e391b5934 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -5,12 +5,12 @@ use axum_test_helper::TestClient; use base64::engine::general_purpose; use base64::Engine; use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode, ProcessedEvent}; -use capture::health::HealthRegistry; use capture::limiters::billing::BillingLimiter; use capture::redis::MockRedisClient; use capture::router::router; use capture::sinks::Event; use capture::time::TimeSource; +use health::HealthRegistry; use serde::Deserialize; use serde_json::{json, Value}; use std::fs::File; diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000000000..0e490c70a6245 --- /dev/null +++ b/common/README.md @@ -0,0 +1,8 @@ +# Common crates for the hog-rs services + +This folder holds internal crates for code reuse between services in the monorepo. To keep maintenance costs low, +these crates should ideally: + +- Cover a small feature scope and use as little dependencies as possible +- Only use `{ workspace = true }` dependencies, instead of pinning versions that could diverge from the workspace +- Have adequate test coverage and documentation diff --git a/common/health/Cargo.toml b/common/health/Cargo.toml new file mode 100644 index 0000000000000..9bbadc151f9db --- /dev/null +++ b/common/health/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "health" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = { workspace = true } +time = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/capture/src/health.rs b/common/health/src/lib.rs similarity index 98% rename from capture/src/health.rs rename to common/health/src/lib.rs index dcddbe477e7cc..5d42bafa8ff05 100644 --- a/capture/src/health.rs +++ b/common/health/src/lib.rs @@ -1,9 +1,9 @@ -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; use std::collections::HashMap; use std::ops::Add; use std::sync::{Arc, RwLock}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; use time::Duration; use tokio::sync::mpsc; use tracing::{info, warn}; @@ -98,7 +98,7 @@ impl HealthHandle { )) } - /// Asynchronously report component status, returns when the message is queued. + /// Synchronously report component status, returns when the message is queued. pub fn report_status_blocking(&self, status: ComponentStatus) { let message = HealthMessage { component: self.component.clone(), @@ -198,7 +198,7 @@ impl HealthRegistry { #[cfg(test)] mod tests { - use crate::health::{ComponentStatus, HealthRegistry, HealthStatus}; + use crate::{ComponentStatus, HealthRegistry, HealthStatus}; use axum::http::StatusCode; use axum::response::IntoResponse; use std::ops::{Add, Sub}; diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index 8ccf8dd5b2ebb..af31e052f1351 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -16,9 +16,9 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } +thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true } -thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } diff --git a/hook-common/src/health.rs b/hook-common/src/health.rs deleted file mode 100644 index c5c79c904c950..0000000000000 --- a/hook-common/src/health.rs +++ /dev/null @@ -1,346 +0,0 @@ -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; -use std::collections::HashMap; -use std::ops::Add; -use std::sync::{Arc, RwLock}; - -use time::Duration; -use tokio::sync::mpsc; -use tracing::{info, warn}; - -/// Health reporting for components of the service. -/// -/// FIXME: copied over from capture, make sure to keep in sync until we share the crate -/// -/// The capture server contains several asynchronous loops, and -/// the process can only be trusted with user data if all the -/// loops are properly running and reporting. -/// -/// HealthRegistry allows an arbitrary number of components to -/// be registered and report their health. The process' health -/// status is the combination of these individual health status: -/// - if any component is unhealthy, the process is unhealthy -/// - if all components recently reported healthy, the process is healthy -/// - if a component failed to report healthy for its defined deadline, -/// it is considered unhealthy, and the check fails. -/// -/// Trying to merge the k8s concepts of liveness and readiness in -/// a single state is full of foot-guns, so HealthRegistry does not -/// try to do it. Each probe should have its separate instance of -/// the registry to avoid confusions. - -#[derive(Default, Debug)] -pub struct HealthStatus { - /// The overall status: true of all components are healthy - pub healthy: bool, - /// Current status of each registered component, for display - pub components: HashMap, -} -impl IntoResponse for HealthStatus { - /// Computes the axum status code based on the overall health status, - /// and prints each component status in the body for debugging. - fn into_response(self) -> Response { - let body = format!("{:?}", self); - match self.healthy { - true => (StatusCode::OK, body), - false => (StatusCode::INTERNAL_SERVER_ERROR, body), - } - .into_response() - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum ComponentStatus { - /// Automatically set when a component is newly registered - Starting, - /// Recently reported healthy, will need to report again before the date - HealthyUntil(time::OffsetDateTime), - /// Reported unhealthy - Unhealthy, - /// Automatically set when the HealthyUntil deadline is reached - Stalled, -} -struct HealthMessage { - component: String, - status: ComponentStatus, -} - -pub struct HealthHandle { - component: String, - deadline: Duration, - sender: mpsc::Sender, -} - -impl HealthHandle { - /// Asynchronously report healthy, returns when the message is queued. - /// Must be called more frequently than the configured deadline. - pub async fn report_healthy(&self) { - self.report_status(ComponentStatus::HealthyUntil( - time::OffsetDateTime::now_utc().add(self.deadline), - )) - .await - } - - /// Asynchronously report component status, returns when the message is queued. - pub async fn report_status(&self, status: ComponentStatus) { - let message = HealthMessage { - component: self.component.clone(), - status, - }; - if let Err(err) = self.sender.send(message).await { - warn!("failed to report heath status: {}", err) - } - } - - /// Synchronously report as healthy, returns when the message is queued. - /// Must be called more frequently than the configured deadline. - pub fn report_healthy_blocking(&self) { - self.report_status_blocking(ComponentStatus::HealthyUntil( - time::OffsetDateTime::now_utc().add(self.deadline), - )) - } - - /// Asynchronously report component status, returns when the message is queued. - pub fn report_status_blocking(&self, status: ComponentStatus) { - let message = HealthMessage { - component: self.component.clone(), - status, - }; - if let Err(err) = self.sender.blocking_send(message) { - warn!("failed to report heath status: {}", err) - } - } -} - -#[derive(Clone)] -pub struct HealthRegistry { - name: String, - components: Arc>>, - sender: mpsc::Sender, -} - -impl HealthRegistry { - pub fn new(name: &str) -> Self { - let (tx, mut rx) = mpsc::channel::(16); - let registry = Self { - name: name.to_owned(), - components: Default::default(), - sender: tx, - }; - - let components = registry.components.clone(); - tokio::spawn(async move { - while let Some(message) = rx.recv().await { - if let Ok(mut map) = components.write() { - _ = map.insert(message.component, message.status); - } else { - // Poisoned mutex: Just warn, the probes will fail and the process restart - warn!("poisoned HeathRegistry mutex") - } - } - }); - - registry - } - - /// Registers a new component in the registry. The returned handle should be passed - /// to the component, to allow it to frequently report its health status. - pub async fn register(&self, component: String, deadline: time::Duration) -> HealthHandle { - let handle = HealthHandle { - component, - deadline, - sender: self.sender.clone(), - }; - handle.report_status(ComponentStatus::Starting).await; - handle - } - - /// Returns the overall process status, computed from the status of all the components - /// currently registered. Can be used as an axum handler. - pub fn get_status(&self) -> HealthStatus { - let components = self - .components - .read() - .expect("poisoned HeathRegistry mutex"); - - let result = HealthStatus { - healthy: !components.is_empty(), // unhealthy if no component has registered yet - components: Default::default(), - }; - let now = time::OffsetDateTime::now_utc(); - - let result = components - .iter() - .fold(result, |mut result, (name, status)| { - match status { - ComponentStatus::HealthyUntil(until) => { - if until.gt(&now) { - _ = result.components.insert(name.clone(), status.clone()) - } else { - result.healthy = false; - _ = result - .components - .insert(name.clone(), ComponentStatus::Stalled) - } - } - _ => { - result.healthy = false; - _ = result.components.insert(name.clone(), status.clone()) - } - } - result - }); - match result.healthy { - true => info!("{} health check ok", self.name), - false => warn!("{} health check failed: {:?}", self.name, result.components), - } - result - } -} - -#[cfg(test)] -mod tests { - use crate::health::{ComponentStatus, HealthRegistry, HealthStatus}; - use axum::http::StatusCode; - use axum::response::IntoResponse; - use std::ops::{Add, Sub}; - use time::{Duration, OffsetDateTime}; - - async fn assert_or_retry(check: F) - where - F: Fn() -> bool, - { - assert_or_retry_for_duration(check, Duration::seconds(5)).await - } - - async fn assert_or_retry_for_duration(check: F, timeout: Duration) - where - F: Fn() -> bool, - { - let deadline = OffsetDateTime::now_utc().add(timeout); - while !check() && OffsetDateTime::now_utc().lt(&deadline) { - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - } - assert!(check()) - } - #[tokio::test] - async fn defaults_to_unhealthy() { - let registry = HealthRegistry::new("liveness"); - assert!(!registry.get_status().healthy); - } - - #[tokio::test] - async fn one_component() { - let registry = HealthRegistry::new("liveness"); - - // New components are registered in Starting - let handle = registry - .register("one".to_string(), Duration::seconds(30)) - .await; - assert_or_retry(|| registry.get_status().components.len() == 1).await; - let mut status = registry.get_status(); - assert!(!status.healthy); - assert_eq!( - status.components.get("one"), - Some(&ComponentStatus::Starting) - ); - - // Status goes healthy once the component reports - handle.report_healthy().await; - assert_or_retry(|| registry.get_status().healthy).await; - status = registry.get_status(); - assert_eq!(status.components.len(), 1); - - // Status goes unhealthy if the components says so - handle.report_status(ComponentStatus::Unhealthy).await; - assert_or_retry(|| !registry.get_status().healthy).await; - status = registry.get_status(); - assert_eq!(status.components.len(), 1); - assert_eq!( - status.components.get("one"), - Some(&ComponentStatus::Unhealthy) - ); - } - - #[tokio::test] - async fn staleness_check() { - let registry = HealthRegistry::new("liveness"); - let handle = registry - .register("one".to_string(), Duration::seconds(30)) - .await; - - // Status goes healthy once the component reports - handle.report_healthy().await; - assert_or_retry(|| registry.get_status().healthy).await; - let mut status = registry.get_status(); - assert_eq!(status.components.len(), 1); - - // If the component's ping is too old, it is considered stalled and the healthcheck fails - // FIXME: we should mock the time instead - handle - .report_status(ComponentStatus::HealthyUntil( - OffsetDateTime::now_utc().sub(Duration::seconds(1)), - )) - .await; - assert_or_retry(|| !registry.get_status().healthy).await; - status = registry.get_status(); - assert_eq!(status.components.len(), 1); - assert_eq!( - status.components.get("one"), - Some(&ComponentStatus::Stalled) - ); - } - - #[tokio::test] - async fn several_components() { - let registry = HealthRegistry::new("liveness"); - let handle1 = registry - .register("one".to_string(), Duration::seconds(30)) - .await; - let handle2 = registry - .register("two".to_string(), Duration::seconds(30)) - .await; - assert_or_retry(|| registry.get_status().components.len() == 2).await; - - // First component going healthy is not enough - handle1.report_healthy().await; - assert_or_retry(|| { - registry.get_status().components.get("one").unwrap() != &ComponentStatus::Starting - }) - .await; - assert!(!registry.get_status().healthy); - - // Second component going healthy brings the health to green - handle2.report_healthy().await; - assert_or_retry(|| { - registry.get_status().components.get("two").unwrap() != &ComponentStatus::Starting - }) - .await; - assert!(registry.get_status().healthy); - - // First component going unhealthy takes down the health to red - handle1.report_status(ComponentStatus::Unhealthy).await; - assert_or_retry(|| !registry.get_status().healthy).await; - - // First component recovering returns the health to green - handle1.report_healthy().await; - assert_or_retry(|| registry.get_status().healthy).await; - - // Second component going unhealthy takes down the health to red - handle2.report_status(ComponentStatus::Unhealthy).await; - assert_or_retry(|| !registry.get_status().healthy).await; - } - - #[tokio::test] - async fn into_response() { - let nok = HealthStatus::default().into_response(); - assert_eq!(nok.status(), StatusCode::INTERNAL_SERVER_ERROR); - - let ok = HealthStatus { - healthy: true, - components: Default::default(), - } - .into_response(); - assert_eq!(ok.status(), StatusCode::OK); - } -} diff --git a/hook-common/src/lib.rs b/hook-common/src/lib.rs index 7f49049add362..8e63ded5a7bf2 100644 --- a/hook-common/src/lib.rs +++ b/hook-common/src/lib.rs @@ -1,4 +1,3 @@ -pub mod health; pub mod kafka_messages; pub mod metrics; pub mod pgqueue; diff --git a/hook-janitor/Cargo.toml b/hook-janitor/Cargo.toml index a29a80c1c5256..654798d95f148 100644 --- a/hook-janitor/Cargo.toml +++ b/hook-janitor/Cargo.toml @@ -11,13 +11,14 @@ axum = { workspace = true } envconfig = { workspace = true } eyre = { workspace = true } futures = { workspace = true } +health = { path = "../common/health" } hook-common = { path = "../hook-common" } metrics = { workspace = true } rdkafka = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } -time = { workspace = true } thiserror = { workspace = true } +time = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/hook-janitor/src/handlers/app.rs b/hook-janitor/src/handlers/app.rs index 507a1cba48d46..65692b14592a2 100644 --- a/hook-janitor/src/handlers/app.rs +++ b/hook-janitor/src/handlers/app.rs @@ -1,5 +1,5 @@ use axum::{routing::get, Router}; -use hook_common::health::HealthRegistry; +use health::HealthRegistry; use std::future::ready; pub fn app(liveness: HealthRegistry) -> Router { diff --git a/hook-janitor/src/kafka_producer.rs b/hook-janitor/src/kafka_producer.rs index ba368663072c3..92608bcb999c8 100644 --- a/hook-janitor/src/kafka_producer.rs +++ b/hook-janitor/src/kafka_producer.rs @@ -1,6 +1,6 @@ use crate::config::KafkaConfig; -use hook_common::health::HealthHandle; +use health::HealthHandle; use rdkafka::error::KafkaError; use rdkafka::producer::FutureProducer; use rdkafka::ClientConfig; diff --git a/hook-janitor/src/main.rs b/hook-janitor/src/main.rs index 46ee37560bb90..325aa098ed6fe 100644 --- a/hook-janitor/src/main.rs +++ b/hook-janitor/src/main.rs @@ -4,7 +4,7 @@ use config::Config; use envconfig::Envconfig; use eyre::Result; use futures::future::{select, Either}; -use hook_common::health::{HealthHandle, HealthRegistry}; +use health::{HealthHandle, HealthRegistry}; use kafka_producer::create_kafka_producer; use std::{str::FromStr, time::Duration}; use tokio::sync::Semaphore; diff --git a/hook-janitor/src/webhooks.rs b/hook-janitor/src/webhooks.rs index 0e3900c045d84..c1dfbba51aa35 100644 --- a/hook-janitor/src/webhooks.rs +++ b/hook-janitor/src/webhooks.rs @@ -495,7 +495,7 @@ mod tests { use super::*; use crate::config; use crate::kafka_producer::{create_kafka_producer, KafkaContext}; - use hook_common::health::HealthRegistry; + use health::HealthRegistry; use hook_common::kafka_messages::app_metrics::{ Error as WebhookError, ErrorDetails, ErrorType, }; diff --git a/hook-worker/Cargo.toml b/hook-worker/Cargo.toml index 5d6874a8af4cd..83ea923b1b9b3 100644 --- a/hook-worker/Cargo.toml +++ b/hook-worker/Cargo.toml @@ -8,14 +8,15 @@ axum = { workspace = true } chrono = { workspace = true } envconfig = { workspace = true } futures = "0.3" +health = { path = "../common/health" } hook-common = { path = "../hook-common" } http = { workspace = true } metrics = { workspace = true } reqwest = { workspace = true } sqlx = { workspace = true } -time = { workspace = true } thiserror = { workspace = true } +time = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } -tokio = { workspace = true } url = { version = "2.2" } diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index 2997dfc65ff50..8a6eeb37435ab 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -4,7 +4,7 @@ use axum::Router; use envconfig::Envconfig; use std::future::ready; -use hook_common::health::HealthRegistry; +use health::HealthRegistry; use hook_common::{ metrics::serve, metrics::setup_metrics_routes, pgqueue::PgQueue, retry::RetryPolicy, }; diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 441e1ee609de8..5965d26a0801d 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -4,7 +4,7 @@ use std::time; use chrono::Utc; use futures::future::join_all; -use hook_common::health::HealthHandle; +use health::HealthHandle; use hook_common::pgqueue::PgTransactionBatch; use hook_common::{ pgqueue::{ @@ -452,7 +452,7 @@ mod tests { // This is due to a long-standing cargo bug that reports imports and helper functions as unused. // See: https://github.com/rust-lang/rust/issues/46379. #[allow(unused_imports)] - use hook_common::health::HealthRegistry; + use health::HealthRegistry; #[allow(unused_imports)] use hook_common::pgqueue::{JobStatus, NewJob}; #[allow(unused_imports)] From 55292bd6287a53a053f7bf4f80d775c5cd1580ec Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 24 Apr 2024 17:30:40 +0200 Subject: [PATCH 224/249] capture: don't allow events submitted with an empty distinct_id (#25) --- capture/src/api.rs | 3 +++ capture/src/v0_request.rs | 16 +++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/capture/src/api.rs b/capture/src/api.rs index 0938ced399773..91ed578f24864 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -28,6 +28,8 @@ pub enum CaptureError { EmptyBatch, #[error("event submitted with an empty event name")] MissingEventName, + #[error("event submitted with an empty distinct_id")] + EmptyDistinctId, #[error("event submitted without a distinct_id")] MissingDistinctId, @@ -59,6 +61,7 @@ impl IntoResponse for CaptureError { | CaptureError::RequestParsingError(_) | CaptureError::EmptyBatch | CaptureError::MissingEventName + | CaptureError::EmptyDistinctId | CaptureError::MissingDistinctId | CaptureError::EventTooBig | CaptureError::NonRetryableSinkError => (StatusCode::BAD_REQUEST, self.to_string()), diff --git a/capture/src/v0_request.rs b/capture/src/v0_request.rs index 3d0052e0c072c..a4e1042f49c78 100644 --- a/capture/src/v0_request.rs +++ b/capture/src/v0_request.rs @@ -217,10 +217,11 @@ impl RawEvent { .as_str() .map(|s| s.to_owned()) .unwrap_or_else(|| value.to_string()); - Ok(match distinct_id.len() { - 0..=200 => distinct_id, - _ => distinct_id.chars().take(200).collect(), - }) + match distinct_id.len() { + 0 => Err(CaptureError::EmptyDistinctId), + 1..=200 => Ok(distinct_id), + _ => Ok(distinct_id.chars().take(200).collect()), + } } } @@ -303,11 +304,16 @@ mod tests { parse_and_extract(r#"{"event": "e"}"#), Err(CaptureError::MissingDistinctId) )); - // Return MissingDistinctId if null, breaking compat with capture-py + // Return MissingDistinctId if null assert!(matches!( parse_and_extract(r#"{"event": "e", "distinct_id": null}"#), Err(CaptureError::MissingDistinctId) )); + // Return EmptyDistinctId if empty string + assert!(matches!( + parse_and_extract(r#"{"event": "e", "distinct_id": ""}"#), + Err(CaptureError::EmptyDistinctId) + )); let assert_extracted_id = |input: &'static str, expected: &str| { let id = parse_and_extract(input).expect("failed to extract"); From 60debad5f115bf5fd9a549b919b3f505ffee0d14 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 24 Apr 2024 17:39:18 +0200 Subject: [PATCH 225/249] chore: improve linting (#26) --- .github/workflows/rust.yml | 24 ++++++++++++------------ Cargo.toml | 15 +++++++++++++++ capture-server/Cargo.toml | 3 +++ capture/Cargo.toml | 3 ++- capture/src/sinks/kafka.rs | 4 ++-- common/health/Cargo.toml | 3 ++- hook-api/Cargo.toml | 3 ++- hook-common/Cargo.toml | 3 ++- hook-common/src/pgqueue.rs | 14 ++++++++------ hook-janitor/Cargo.toml | 3 ++- hook-worker/Cargo.toml | 3 +++ 11 files changed, 53 insertions(+), 25 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 30f17341d5f08..ed5acb2ed6a90 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -5,7 +5,6 @@ on: push: branches: [main] pull_request: - branches: [main] env: CARGO_TERM_COLOR: always @@ -71,7 +70,7 @@ jobs: - name: Run cargo check run: cargo check --all-features - clippy: + linting: runs-on: depot-ubuntu-22.04-4 steps: @@ -81,7 +80,7 @@ jobs: uses: dtolnay/rust-toolchain@master with: toolchain: stable - components: clippy + components: clippy,rustfmt - uses: actions/cache@v3 with: @@ -94,17 +93,18 @@ jobs: - name: Run clippy run: cargo clippy -- -D warnings - format: - runs-on: depot-ubuntu-22.04-4 + - name: Check format + run: cargo fmt -- --check + shear: + runs-on: depot-ubuntu-22.04-4 steps: - uses: actions/checkout@v3 - - name: Install latest rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - components: rustfmt + - name: Install cargo-binstall + uses: cargo-bins/cargo-binstall@main - - name: Format - run: cargo fmt -- --check + - name: Install cargo-shear + run: cargo binstall --no-confirm cargo-shear + + - run: cargo shear diff --git a/Cargo.toml b/Cargo.toml index cb4aa8e58e88d..d34cd0ae39231 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,21 @@ members = [ "hook-worker", ] +[workspace.lints.rust] +# See https://doc.rust-lang.org/stable/rustc/lints/listing/allowed-by-default.html +unsafe_code = "forbid" # forbid cannot be ignored with an annotation +unstable_features = "forbid" +macro_use_extern_crate = "forbid" +let_underscore_drop = "deny" +non_ascii_idents = "deny" +trivial_casts = "deny" +trivial_numeric_casts = "deny" +unit_bindings = "deny" + +[workspace.lints.clippy] +# See https://rust-lang.github.io/rust-clippy/, we might want to add more +enum_glob_use = "deny" + [workspace.dependencies] anyhow = "1.0" assert-json-diff = "2.0.2" diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml index ae06664ec96f9..39ee742d2b048 100644 --- a/capture-server/Cargo.toml +++ b/capture-server/Cargo.toml @@ -3,6 +3,9 @@ name = "capture-server" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] capture = { path = "../capture" } envconfig = { workspace = true } diff --git a/capture/Cargo.toml b/capture/Cargo.toml index 02ada312932c1..4e35d10be7b85 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -3,7 +3,8 @@ name = "capture" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true [dependencies] anyhow = { workspace = true } diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index 8b286e84d8838..b7af44993bc13 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -118,10 +118,10 @@ impl KafkaSink { client_config.create_with_context(KafkaContext { liveness })?; // Ping the cluster to make sure we can reach brokers, fail after 10 seconds - _ = producer.client().fetch_metadata( + drop(producer.client().fetch_metadata( Some("__consumer_offsets"), Timeout::After(Duration::new(10, 0)), - )?; + )?); info!("connected to Kafka brokers"); Ok(KafkaSink { diff --git a/common/health/Cargo.toml b/common/health/Cargo.toml index 9bbadc151f9db..c38e704bd7ce3 100644 --- a/common/health/Cargo.toml +++ b/common/health/Cargo.toml @@ -3,7 +3,8 @@ name = "health" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true [dependencies] axum = { workspace = true } diff --git a/hook-api/Cargo.toml b/hook-api/Cargo.toml index a596e87076b18..c3528d23da5d2 100644 --- a/hook-api/Cargo.toml +++ b/hook-api/Cargo.toml @@ -3,7 +3,8 @@ name = "hook-api" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true [dependencies] axum = { workspace = true } diff --git a/hook-common/Cargo.toml b/hook-common/Cargo.toml index af31e052f1351..58232a80fe17d 100644 --- a/hook-common/Cargo.toml +++ b/hook-common/Cargo.toml @@ -3,7 +3,8 @@ name = "hook-common" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true [dependencies] async-trait = { workspace = true } diff --git a/hook-common/src/pgqueue.rs b/hook-common/src/pgqueue.rs index f155506123b35..5d8a14485697e 100644 --- a/hook-common/src/pgqueue.rs +++ b/hook-common/src/pgqueue.rs @@ -827,14 +827,15 @@ mod tests { let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); - let _ = job - .retry( + drop( + job.retry( "a very reasonable failure reason", retry_interval, &retry_queue, ) .await - .expect("failed to retry job"); + .expect("failed to retry job"), + ); batch.commit().await.expect("failed to commit transaction"); let retried_job: PgTransactionJob = queue @@ -883,14 +884,15 @@ mod tests { let retry_interval = retry_policy.retry_interval(job.job.attempt as u32, None); let retry_queue = retry_policy.retry_queue(&job.job.queue).to_owned(); - let _ = job - .retry( + drop( + job.retry( "a very reasonable failure reason", retry_interval, &retry_queue, ) .await - .expect("failed to retry job"); + .expect("failed to retry job"), + ); batch.commit().await.expect("failed to commit transaction"); let retried_job_not_found: Option> = queue diff --git a/hook-janitor/Cargo.toml b/hook-janitor/Cargo.toml index 654798d95f148..741918e79385a 100644 --- a/hook-janitor/Cargo.toml +++ b/hook-janitor/Cargo.toml @@ -3,7 +3,8 @@ name = "hook-janitor" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true [dependencies] async-trait = { workspace = true } diff --git a/hook-worker/Cargo.toml b/hook-worker/Cargo.toml index 83ea923b1b9b3..79416f9004a10 100644 --- a/hook-worker/Cargo.toml +++ b/hook-worker/Cargo.toml @@ -3,6 +3,9 @@ name = "hook-worker" version = "0.1.0" edition = "2021" +[lints] +workspace = true + [dependencies] axum = { workspace = true } chrono = { workspace = true } From 26a67e9c586606a5a2b5f9ec6a9221e4fbd4bd7d Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 25 Apr 2024 16:42:17 +0200 Subject: [PATCH 226/249] capture: set kafka partitioner to murmur2_random (#27) --- capture/src/sinks/kafka.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index b7af44993bc13..8ffb2d6d7f898 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -96,6 +96,7 @@ impl KafkaSink { client_config .set("bootstrap.servers", &config.kafka_hosts) .set("statistics.interval.ms", "10000") + .set("partitioner", "murmur2_random") // Compatibility with python-kafka .set("linger.ms", config.kafka_producer_linger_ms.to_string()) .set( "message.timeout.ms", From 9f5b72db5f8960c90a1ffbc2baa344a058d05c8c Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 30 Apr 2024 12:24:13 +0200 Subject: [PATCH 227/249] capture: don't serialize sent_at if empty (#28) --- capture/src/api.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/capture/src/api.rs b/capture/src/api.rs index 91ed578f24864..8e595fcfeb0a0 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -87,7 +87,10 @@ pub struct ProcessedEvent { pub ip: String, pub data: String, pub now: String, - #[serde(with = "time::serde::rfc3339::option")] + #[serde( + with = "time::serde::rfc3339::option", + skip_serializing_if = "Option::is_none" + )] pub sent_at: Option, pub token: String, } From bca9f8457eb1a8a3689cb9ad72cf102701528238 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Tue, 30 Apr 2024 12:24:25 +0200 Subject: [PATCH 228/249] feat(capture): send historical_migration batches to separate topic (#30) --- capture-server/tests/common.rs | 6 +- capture-server/tests/events.rs | 112 ++++++++++++++++++++++++++---- capture/src/api.rs | 9 ++- capture/src/config.rs | 3 + capture/src/sinks/kafka.rs | 48 +++++++------ capture/src/v0_endpoint.rs | 15 ++-- capture/src/v0_request.rs | 3 +- capture/tests/django_compat.rs | 23 ++++-- capture/tests/requests_dump.jsonl | 8 ++- 9 files changed, 179 insertions(+), 48 deletions(-) diff --git a/capture-server/tests/common.rs b/capture-server/tests/common.rs index 5ee2caa5ca7a9..788e6e28240c7 100644 --- a/capture-server/tests/common.rs +++ b/capture-server/tests/common.rs @@ -39,6 +39,7 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { kafka_compression_codec: "none".to_string(), kafka_hosts: "kafka:9092".to_string(), kafka_topic: "events_plugin_ingestion".to_string(), + kafka_historical_topic: "events_plugin_ingestion_historical".to_string(), kafka_tls: false, }, otel_url: None, @@ -61,9 +62,10 @@ pub struct ServerHandle { } impl ServerHandle { - pub async fn for_topic(topic: &EphemeralTopic) -> Self { + pub async fn for_topics(main: &EphemeralTopic, historical: &EphemeralTopic) -> Self { let mut config = DEFAULT_CONFIG.clone(); - config.kafka.kafka_topic = topic.topic_name().to_string(); + config.kafka.kafka_topic = main.topic_name().to_string(); + config.kafka.kafka_historical_topic = historical.topic_name().to_string(); Self::for_config(config).await } pub async fn for_config(config: Config) -> Self { diff --git a/capture-server/tests/events.rs b/capture-server/tests/events.rs index 8a4220b8e3920..111b02c7f2cb1 100644 --- a/capture-server/tests/events.rs +++ b/capture-server/tests/events.rs @@ -13,8 +13,10 @@ async fn it_captures_one_event() -> Result<()> { setup_tracing(); let token = random_string("token", 16); let distinct_id = random_string("id", 16); - let topic = EphemeralTopic::new().await; - let server = ServerHandle::for_topic(&topic).await; + + let main_topic = EphemeralTopic::new().await; + let histo_topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topics(&main_topic, &histo_topic).await; let event = json!({ "token": token, @@ -24,7 +26,7 @@ async fn it_captures_one_event() -> Result<()> { let res = server.capture_events(event.to_string()).await; assert_eq!(StatusCode::OK, res.status()); - let event = topic.next_event()?; + let event = main_topic.next_event()?; assert_json_include!( actual: event, expected: json!({ @@ -37,14 +39,15 @@ async fn it_captures_one_event() -> Result<()> { } #[tokio::test] -async fn it_captures_a_batch() -> Result<()> { +async fn it_captures_a_posthogjs_array() -> Result<()> { setup_tracing(); let token = random_string("token", 16); let distinct_id1 = random_string("id", 16); let distinct_id2 = random_string("id", 16); - let topic = EphemeralTopic::new().await; - let server = ServerHandle::for_topic(&topic).await; + let main_topic = EphemeralTopic::new().await; + let histo_topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topics(&main_topic, &histo_topic).await; let event = json!([{ "token": token, @@ -59,14 +62,98 @@ async fn it_captures_a_batch() -> Result<()> { assert_eq!(StatusCode::OK, res.status()); assert_json_include!( - actual: topic.next_event()?, + actual: main_topic.next_event()?, expected: json!({ "token": token, "distinct_id": distinct_id1 }) ); assert_json_include!( - actual: topic.next_event()?, + actual: main_topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": distinct_id2 + }) + ); + + Ok(()) +} + +#[tokio::test] +async fn it_captures_a_batch() -> Result<()> { + setup_tracing(); + let token = random_string("token", 16); + let distinct_id1 = random_string("id", 16); + let distinct_id2 = random_string("id", 16); + + let main_topic = EphemeralTopic::new().await; + let histo_topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topics(&main_topic, &histo_topic).await; + + let event = json!({ + "token": token, + "batch": [{ + "event": "event1", + "distinct_id": distinct_id1 + },{ + "event": "event2", + "distinct_id": distinct_id2 + }] + }); + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + assert_json_include!( + actual: main_topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": distinct_id1 + }) + ); + assert_json_include!( + actual: main_topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": distinct_id2 + }) + ); + + Ok(()) +} +#[tokio::test] +async fn it_captures_a_historical_batch() -> Result<()> { + setup_tracing(); + let token = random_string("token", 16); + let distinct_id1 = random_string("id", 16); + let distinct_id2 = random_string("id", 16); + + let main_topic = EphemeralTopic::new().await; + let histo_topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topics(&main_topic, &histo_topic).await; + + let event = json!({ + "token": token, + "historical_migration": true, + "batch": [{ + "event": "event1", + "distinct_id": distinct_id1 + },{ + "event": "event2", + "distinct_id": distinct_id2 + }] + }); + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + assert_json_include!( + actual: histo_topic.next_event()?, + expected: json!({ + "token": token, + "distinct_id": distinct_id1 + }) + ); + assert_json_include!( + actual: histo_topic.next_event()?, expected: json!({ "token": token, "distinct_id": distinct_id2 @@ -175,8 +262,9 @@ async fn it_trims_distinct_id() -> Result<()> { let distinct_id2 = random_string("id", 222); let (trimmed_distinct_id2, _) = distinct_id2.split_at(200); // works because ascii chars - let topic = EphemeralTopic::new().await; - let server = ServerHandle::for_topic(&topic).await; + let main_topic = EphemeralTopic::new().await; + let histo_topic = EphemeralTopic::new().await; + let server = ServerHandle::for_topics(&main_topic, &histo_topic).await; let event = json!([{ "token": token, @@ -191,14 +279,14 @@ async fn it_trims_distinct_id() -> Result<()> { assert_eq!(StatusCode::OK, res.status()); assert_json_include!( - actual: topic.next_event()?, + actual: main_topic.next_event()?, expected: json!({ "token": token, "distinct_id": distinct_id1 }) ); assert_json_include!( - actual: topic.next_event()?, + actual: main_topic.next_event()?, expected: json!({ "token": token, "distinct_id": trimmed_distinct_id2 diff --git a/capture/src/api.rs b/capture/src/api.rs index 8e595fcfeb0a0..97d84857075e4 100644 --- a/capture/src/api.rs +++ b/capture/src/api.rs @@ -80,8 +80,15 @@ impl IntoResponse for CaptureError { } } -#[derive(Clone, Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum DataType { + AnalyticsMain, + AnalyticsHistorical, +} +#[derive(Clone, Debug, Serialize, Eq, PartialEq)] pub struct ProcessedEvent { + #[serde(skip_serializing)] + pub data_type: DataType, pub uuid: Uuid, pub distinct_id: String, pub ip: String, diff --git a/capture/src/config.rs b/capture/src/config.rs index a4bd8f2cfd5cc..07b7f89496d44 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -45,7 +45,10 @@ pub struct KafkaConfig { #[envconfig(default = "none")] pub kafka_compression_codec: String, // none, gzip, snappy, lz4, zstd pub kafka_hosts: String, + #[envconfig(default = "events_plugin_ingestion")] pub kafka_topic: String, + #[envconfig(default = "events_plugin_ingestion_historical")] + pub kafka_historical_topic: String, #[envconfig(default = "false")] pub kafka_tls: bool, } diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index 8ffb2d6d7f898..945e581183d37 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -11,7 +11,7 @@ use tokio::task::JoinSet; use tracing::log::{debug, error, info}; use tracing::{info_span, instrument, Instrument}; -use crate::api::{CaptureError, ProcessedEvent}; +use crate::api::{CaptureError, DataType, ProcessedEvent}; use crate::config::KafkaConfig; use crate::limiters::overflow::OverflowLimiter; use crate::prometheus::report_dropped_events; @@ -80,8 +80,9 @@ impl rdkafka::ClientContext for KafkaContext { #[derive(Clone)] pub struct KafkaSink { producer: FutureProducer, - topic: String, partition: OverflowLimiter, + main_topic: String, + historical_topic: String, } impl KafkaSink { @@ -128,7 +129,8 @@ impl KafkaSink { Ok(KafkaSink { producer, partition, - topic: config.kafka_topic, + main_topic: config.kafka_topic, + historical_topic: config.kafka_historical_topic, }) } @@ -137,22 +139,27 @@ impl KafkaSink { self.producer.flush(Duration::new(30, 0)) } - async fn kafka_send( - producer: FutureProducer, - topic: String, - event: ProcessedEvent, - limited: bool, - ) -> Result { + async fn kafka_send(&self, event: ProcessedEvent) -> Result { let payload = serde_json::to_string(&event).map_err(|e| { error!("failed to serialize event: {}", e); CaptureError::NonRetryableSinkError })?; - let key = event.key(); - let partition_key = if limited { None } else { Some(key.as_str()) }; + let event_key = event.key(); + let (topic, partition_key): (&str, Option<&str>) = match &event.data_type { + DataType::AnalyticsHistorical => (&self.historical_topic, Some(event_key.as_str())), // We never trigger overflow on historical events + DataType::AnalyticsMain => { + // TODO: deprecate capture-led overflow or move logic in handler + if self.partition.is_limited(&event_key) { + (&self.main_topic, None) // Analytics overflow goes to the main topic without locality + } else { + (&self.main_topic, Some(event_key.as_str())) + } + } + }; - match producer.send_result(FutureRecord { - topic: topic.as_str(), + match self.producer.send_result(FutureRecord { + topic, payload: Some(&payload), partition: None, key: partition_key, @@ -206,9 +213,7 @@ impl KafkaSink { impl Event for KafkaSink { #[instrument(skip_all)] async fn send(&self, event: ProcessedEvent) -> Result<(), CaptureError> { - let limited = self.partition.is_limited(&event.key()); - let ack = - Self::kafka_send(self.producer.clone(), self.topic.clone(), event, limited).await?; + let ack = self.kafka_send(event).await?; histogram!("capture_event_batch_size").record(1.0); Self::process_ack(ack) .instrument(info_span!("ack_wait_one")) @@ -220,12 +225,8 @@ impl Event for KafkaSink { let mut set = JoinSet::new(); let batch_size = events.len(); for event in events { - let producer = self.producer.clone(); - let topic = self.topic.clone(); - let limited = self.partition.is_limited(&event.key()); - // We await kafka_send to get events in the producer queue sequentially - let ack = Self::kafka_send(producer, topic, event, limited).await?; + let ack = self.kafka_send(event).await?; // Then stash the returned DeliveryFuture, waiting concurrently for the write ACKs from brokers. set.spawn(Self::process_ack(ack)); @@ -259,7 +260,7 @@ impl Event for KafkaSink { #[cfg(test)] mod tests { - use crate::api::{CaptureError, ProcessedEvent}; + use crate::api::{CaptureError, DataType, ProcessedEvent}; use crate::config; use crate::limiters::overflow::OverflowLimiter; use crate::sinks::kafka::KafkaSink; @@ -292,6 +293,7 @@ mod tests { kafka_compression_codec: "none".to_string(), kafka_hosts: cluster.bootstrap_servers(), kafka_topic: "events_plugin_ingestion".to_string(), + kafka_historical_topic: "events_plugin_ingestion_historical".to_string(), kafka_tls: false, }; let sink = KafkaSink::new(config, handle, limiter).expect("failed to create sink"); @@ -305,6 +307,7 @@ mod tests { let (cluster, sink) = start_on_mocked_sink().await; let event: ProcessedEvent = ProcessedEvent { + data_type: DataType::AnalyticsMain, uuid: uuid_v7(), distinct_id: "id1".to_string(), ip: "".to_string(), @@ -336,6 +339,7 @@ mod tests { .map(char::from) .collect(); let big_event: ProcessedEvent = ProcessedEvent { + data_type: DataType::AnalyticsMain, uuid: uuid_v7(), distinct_id: "id1".to_string(), ip: "".to_string(), diff --git a/capture/src/v0_endpoint.rs b/capture/src/v0_endpoint.rs index 3862995f2f4f8..3849e29328efa 100644 --- a/capture/src/v0_endpoint.rs +++ b/capture/src/v0_endpoint.rs @@ -15,7 +15,7 @@ use crate::limiters::billing::QuotaResource; use crate::prometheus::report_dropped_events; use crate::v0_request::{Compression, ProcessingContext, RawRequest}; use crate::{ - api::{CaptureError, CaptureResponse, CaptureResponseCode, ProcessedEvent}, + api::{CaptureError, CaptureResponse, CaptureResponseCode, DataType, ProcessedEvent}, router, sinks, utils::uuid_v7, v0_request::{EventFormData, EventQuery, RawEvent}, @@ -39,7 +39,7 @@ use crate::{ content_type, version, compression, - is_historical + historical_migration ) )] #[debug_handler] @@ -106,11 +106,11 @@ pub async fn event( return Err(err); } }; - let is_historical = request.is_historical(); // TODO: use to write to historical topic + let historical_migration = request.historical_migration(); let events = request.events(); // Takes ownership of request tracing::Span::current().record("token", &token); - tracing::Span::current().record("is_historical", is_historical); + tracing::Span::current().record("historical_migration", historical_migration); tracing::Span::current().record("batch_size", events.len()); if events.is_empty() { @@ -125,6 +125,7 @@ pub async fn event( token, now: state.timesource.current_time(), client_ip: ip.to_string(), + historical_migration, }; let billing_limited = state @@ -174,12 +175,18 @@ pub fn process_single_event( return Err(CaptureError::MissingEventName); } + let data_type = match context.historical_migration { + true => DataType::AnalyticsHistorical, + false => DataType::AnalyticsMain, + }; + let data = serde_json::to_string(&event).map_err(|e| { tracing::error!("failed to encode data field: {}", e); CaptureError::NonRetryableSinkError })?; Ok(ProcessedEvent { + data_type, uuid: event.uuid.unwrap_or_else(uuid_v7), distinct_id: event.extract_distinct_id()?, ip: context.client_ip.clone(), diff --git a/capture/src/v0_request.rs b/capture/src/v0_request.rs index a4e1042f49c78..c0d5f36d3577f 100644 --- a/capture/src/v0_request.rs +++ b/capture/src/v0_request.rs @@ -149,7 +149,7 @@ impl RawRequest { Ok(token) } - pub fn is_historical(&self) -> bool { + pub fn historical_migration(&self) -> bool { match self { RawRequest::Batch(req) => req.historical_migration.unwrap_or_default(), _ => false, @@ -232,6 +232,7 @@ pub struct ProcessingContext { pub token: String, pub now: String, pub client_ip: String, + pub historical_migration: bool, } #[cfg(test)] diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index 9ef4e391b5934..d1d313cd5e112 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -4,7 +4,7 @@ use axum::http::StatusCode; use axum_test_helper::TestClient; use base64::engine::general_purpose; use base64::Engine; -use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode, ProcessedEvent}; +use capture::api::{CaptureError, CaptureResponse, CaptureResponseCode, DataType, ProcessedEvent}; use capture::limiters::billing::BillingLimiter; use capture::redis::MockRedisClient; use capture::router::router; @@ -29,6 +29,8 @@ struct RequestDump { now: String, body: String, output: Vec, + #[serde(default)] // default = false + historical_migration: bool, } static REQUESTS_DUMP_FILE_NAME: &str = "tests/requests_dump.jsonl"; @@ -146,14 +148,27 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { for (event_number, (message, expected)) in sink.events().iter().zip(case.output.iter()).enumerate() { + // Ensure the data type matches + if case.historical_migration { + assert_eq!(DataType::AnalyticsHistorical, message.data_type); + } else { + assert_eq!(DataType::AnalyticsMain, message.data_type); + } + // Normalizing the expected event to align with known django->rust inconsistencies let mut expected = expected.clone(); if let Some(value) = expected.get_mut("sent_at") { // Default ISO format is different between python and rust, both are valid // Parse and re-print the value before comparison - let sent_at = - OffsetDateTime::parse(value.as_str().expect("empty"), &Iso8601::DEFAULT)?; - *value = Value::String(sent_at.format(&Rfc3339)?) + let raw_value = value.as_str().expect("sent_at field is not a string"); + if raw_value.is_empty() { + *value = Value::Null + } else { + let sent_at = + OffsetDateTime::parse(value.as_str().expect("empty"), &Iso8601::DEFAULT) + .expect("failed to parse expected sent_at"); + *value = Value::String(sent_at.format(&Rfc3339)?) + } } if let Some(expected_data) = expected.get_mut("data") { // Data is a serialized JSON map. Unmarshall both and compare them, diff --git a/capture/tests/requests_dump.jsonl b/capture/tests/requests_dump.jsonl index 36cf8ade36439..4b59c3bc971b3 100644 --- a/capture/tests/requests_dump.jsonl +++ b/capture/tests/requests_dump.jsonl @@ -1,3 +1,4 @@ +### posthog-js {"path":"/e/?ip=1&_=1694769302325&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.328551+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l6TlMwM1l6WTJMVGd5TldVdE9HSXdaV1ZoWlRZMU56RTBJaXdpWlhabGJuUWlPaUlrYVdSbGJuUnBabmtpTENKd2NtOXdaWEowYVdWeklqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR1JsZG1salpWOTBlWEJsSWpvaVJHVnphM1J2Y0NJc0lpUmpkWEp5Wlc1MFgzVnliQ0k2SW1oMGRIQTZMeTlzYjJOaGJHaHZjM1E2T0RBd01DOGlMQ0lrYUc5emRDSTZJbXh2WTJGc2FHOXpkRG80TURBd0lpd2lKSEJoZEdodVlXMWxJam9pTHlJc0lpUmljbTkzYzJWeVgzWmxjbk5wYjI0aU9qRXhOeXdpSkdKeWIzZHpaWEpmYkdGdVozVmhaMlVpT2lKbGJpMVZVeUlzSWlSelkzSmxaVzVmYUdWcFoyaDBJam94TURVeUxDSWtjMk55WldWdVgzZHBaSFJvSWpveE5qSXdMQ0lrZG1sbGQzQnZjblJmYUdWcFoyaDBJam81TVRFc0lpUjJhV1YzY0c5eWRGOTNhV1IwYUNJNk1UVTBPQ3dpSkd4cFlpSTZJbmRsWWlJc0lpUnNhV0pmZG1WeWMybHZiaUk2SWpFdU56Z3VOU0lzSWlScGJuTmxjblJmYVdRaU9pSTBNSEIwTVhWamNHczNORFpwYkdWd0lpd2lKSFJwYldVaU9qRTJPVFEzTmprek1ESXVNekkxTENKa2FYTjBhVzVqZEY5cFpDSTZJbkJYUVd0SlZIbFJNME5PTnpNek1sVnhVWGh1U0Rad00wWldPRlpLWkRkd1dUWTBOMFZrVG10NFYyTWlMQ0lrWkdWMmFXTmxYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaQzAzT0dGbExUazBOMkl0WW1Wa1ltRmhNREpoTUdZMElpd2lKSFZ6WlhKZmFXUWlPaUp3VjBGclNWUjVVVE5EVGpjek16SlZjVkY0YmtnMmNETkdWamhXU21RM2NGazJORGRGWkU1cmVGZGpJaXdpSkhKbFptVnljbVZ5SWpvaUpHUnBjbVZqZENJc0lpUnlaV1psY25KcGJtZGZaRzl0WVdsdUlqb2lKR1JwY21WamRDSXNJaVJoYm05dVgyUnBjM1JwYm1OMFgybGtJam9pTURFNFlUazRNV1l0TkdJeVpDMDNPR0ZsTFRrME4ySXRZbVZrWW1GaE1ESmhNR1kwSWl3aWRHOXJaVzRpT2lKd2FHTmZjV2RWUm5BMWQzb3lRbXBETkZKelJtcE5SM0JSTTFCSFJISnphVFpSTUVOSE1FTlFORTVCWVdNd1NTSXNJaVJ6WlhOemFXOXVYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaUzAzWW1RekxXSmlNekF0TmpZeE4ySm1ORGc0T0RZNUlpd2lKSGRwYm1SdmQxOXBaQ0k2SWpBeE9HRTVPREZtTFRSaU1tVXROMkprTXkxaVlqTXdMVFkyTVRneE1HWmxaRFkxWmlKOUxDSWtjMlYwSWpwN2ZTd2lKSE5sZEY5dmJtTmxJanA3ZlN3aWRHbHRaWE4wWVcxd0lqb2lNakF5TXkwd09TMHhOVlF3T1RveE5Ub3dNaTR6TWpWYUluMCUzRA==","output":[{"uuid":"018a981f-4b35-7c66-825e-8b0eeae65714","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b35-7c66-825e-8b0eeae65714\", \"event\": \"$identify\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"40pt1ucpk746ilep\", \"$time\": 1694769302.325, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$anon_distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"$set\": {}, \"$set_once\": {}, \"timestamp\": \"2023-09-15T09:15:02.325Z\"}","now":"2023-09-15T09:15:02.328551+00:00","sent_at":"2023-09-15T09:15:02.325000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?ip=1&_=1694769302319&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.322717+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l5WmkwM09URmhMVGxsWWpFdE5qZGhaVFZpT1dWalpqZzBJaXdpWlhabGJuUWlPaUlrY0dGblpYWnBaWGNpTENKd2NtOXdaWEowYVdWeklqcDdJaVJ2Y3lJNklrMWhZeUJQVXlCWUlpd2lKRzl6WDNabGNuTnBiMjRpT2lJeE1DNHhOUzR3SWl3aUpHSnliM2R6WlhJaU9pSkdhWEpsWm05NElpd2lKR1JsZG1salpWOTBlWEJsSWpvaVJHVnphM1J2Y0NJc0lpUmpkWEp5Wlc1MFgzVnliQ0k2SW1oMGRIQTZMeTlzYjJOaGJHaHZjM1E2T0RBd01DOGlMQ0lrYUc5emRDSTZJbXh2WTJGc2FHOXpkRG80TURBd0lpd2lKSEJoZEdodVlXMWxJam9pTHlJc0lpUmljbTkzYzJWeVgzWmxjbk5wYjI0aU9qRXhOeXdpSkdKeWIzZHpaWEpmYkdGdVozVmhaMlVpT2lKbGJpMVZVeUlzSWlSelkzSmxaVzVmYUdWcFoyaDBJam94TURVeUxDSWtjMk55WldWdVgzZHBaSFJvSWpveE5qSXdMQ0lrZG1sbGQzQnZjblJmYUdWcFoyaDBJam81TVRFc0lpUjJhV1YzY0c5eWRGOTNhV1IwYUNJNk1UVTBPQ3dpSkd4cFlpSTZJbmRsWWlJc0lpUnNhV0pmZG1WeWMybHZiaUk2SWpFdU56Z3VOU0lzSWlScGJuTmxjblJmYVdRaU9pSnpjSEozT0RVM2JHVnVOM0ZxTXpSMklpd2lKSFJwYldVaU9qRTJPVFEzTmprek1ESXVNekU1TENKa2FYTjBhVzVqZEY5cFpDSTZJakF4T0dFNU9ERm1MVFJpTW1RdE56aGhaUzA1TkRkaUxXSmxaR0poWVRBeVlUQm1OQ0lzSWlSa1pYWnBZMlZmYVdRaU9pSXdNVGhoT1RneFppMDBZakprTFRjNFlXVXRPVFEzWWkxaVpXUmlZV0V3TW1Fd1pqUWlMQ0lrY21WbVpYSnlaWElpT2lJa1pHbHlaV04wSWl3aUpISmxabVZ5Y21sdVoxOWtiMjFoYVc0aU9pSWtaR2x5WldOMElpd2lkR2wwYkdVaU9pSlFiM04wU0c5bklpd2lkRzlyWlc0aU9pSndhR05mY1dkVlJuQTFkM295UW1wRE5GSnpSbXBOUjNCUk0xQkhSSEp6YVRaUk1FTkhNRU5RTkU1QllXTXdTU0lzSWlSelpYTnphVzl1WDJsa0lqb2lNREU0WVRrNE1XWXROR0l5WlMwM1ltUXpMV0ppTXpBdE5qWXhOMkptTkRnNE9EWTVJaXdpSkhkcGJtUnZkMTlwWkNJNklqQXhPR0U1T0RGbUxUUmlNbVV0TjJKa015MWlZak13TFRZMk1UZ3hNR1psWkRZMVppSjlMQ0owYVcxbGMzUmhiWEFpT2lJeU1ESXpMVEE1TFRFMVZEQTVPakUxT2pBeUxqTXhPVm9pZlElM0QlM0Q=","output":[{"uuid":"018a981f-4b2f-791a-9eb1-67ae5b9ecf84","distinct_id":"018a981f-4b2d-78ae-947b-bedbaa02a0f4","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b2f-791a-9eb1-67ae5b9ecf84\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"sprw857len7qj34v\", \"$time\": 1694769302.319, \"distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"title\": \"PostHog\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"timestamp\": \"2023-09-15T09:15:02.319Z\"}","now":"2023-09-15T09:15:02.322717+00:00","sent_at":"2023-09-15T09:15:02.319000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?ip=1&_=1694769302319&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.321230+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l5WlMwM1ltUXpMV0ppTXpBdE5qWXhOalkxTjJNek9HWmhJaXdpWlhabGJuUWlPaUlrYjNCMFgybHVJaXdpY0hKdmNHVnlkR2xsY3lJNmV5SWtiM01pT2lKTllXTWdUMU1nV0NJc0lpUnZjMTkyWlhKemFXOXVJam9pTVRBdU1UVXVNQ0lzSWlSaWNtOTNjMlZ5SWpvaVJtbHlaV1p2ZUNJc0lpUmtaWFpwWTJWZmRIbHdaU0k2SWtSbGMydDBiM0FpTENJa1kzVnljbVZ1ZEY5MWNtd2lPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPamd3TURBdklpd2lKR2h2YzNRaU9pSnNiMk5oYkdodmMzUTZPREF3TUNJc0lpUndZWFJvYm1GdFpTSTZJaThpTENJa1luSnZkM05sY2w5MlpYSnphVzl1SWpveE1UY3NJaVJpY205M2MyVnlYMnhoYm1kMVlXZGxJam9pWlc0dFZWTWlMQ0lrYzJOeVpXVnVYMmhsYVdkb2RDSTZNVEExTWl3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRZeU1Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPVEV4TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFMU5EZ3NJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpjNExqVWlMQ0lrYVc1elpYSjBYMmxrSWpvaU1XVTRaV1p5WkdSbE1HSTBNV2RtYkNJc0lpUjBhVzFsSWpveE5qazBOelk1TXpBeUxqTXhPQ3dpWkdsemRHbHVZM1JmYVdRaU9pSXdNVGhoT1RneFppMDBZakprTFRjNFlXVXRPVFEzWWkxaVpXUmlZV0V3TW1Fd1pqUWlMQ0lrWkdWMmFXTmxYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaQzAzT0dGbExUazBOMkl0WW1Wa1ltRmhNREpoTUdZMElpd2lKSEpsWm1WeWNtVnlJam9pSkdScGNtVmpkQ0lzSWlSeVpXWmxjbkpwYm1kZlpHOXRZV2x1SWpvaUpHUnBjbVZqZENJc0luUnZhMlZ1SWpvaWNHaGpYM0ZuVlVad05YZDZNa0pxUXpSU2MwWnFUVWR3VVROUVIwUnljMmsyVVRCRFJ6QkRVRFJPUVdGak1Fa2lMQ0lrYzJWemMybHZibDlwWkNJNklqQXhPR0U1T0RGbUxUUmlNbVV0TjJKa015MWlZak13TFRZMk1UZGlaalE0T0RnMk9TSXNJaVIzYVc1a2IzZGZhV1FpT2lJd01UaGhPVGd4WmkwMFlqSmxMVGRpWkRNdFltSXpNQzAyTmpFNE1UQm1aV1EyTldZaWZTd2lkR2x0WlhOMFlXMXdJam9pTWpBeU15MHdPUzB4TlZRd09Ub3hOVG93TWk0ek1UaGFJbjAlM0Q=","output":[{"uuid":"018a981f-4b2e-7bd3-bb30-6616657c38fa","distinct_id":"018a981f-4b2d-78ae-947b-bedbaa02a0f4","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b2e-7bd3-bb30-6616657c38fa\", \"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"1e8efrdde0b41gfl\", \"$time\": 1694769302.318, \"distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"timestamp\": \"2023-09-15T09:15:02.318Z\"}","now":"2023-09-15T09:15:02.321230+00:00","sent_at":"2023-09-15T09:15:02.319000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} @@ -10,8 +11,11 @@ {"path":"/e/?compression=gzip-js&ip=1&_=1694769323385&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:23.391445+00:00","body":"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","output":[{"uuid":"018a981f-95fe-76af-9f1d-da5e526b4081","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-95fe-76af-9f1d-da5e526b4081\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0rsqs282xgp3wd4o\", \"$time\": 1694769321.47, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"classes\": [\"text-default\", \"grow\"], \"attr__class\": \"text-default grow\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Cohorts\", \"attr__href\": \"/cohorts\"}, {\"tag_name\": \"span\", \"classes\": [\"LemonButton__content\"], \"attr__class\": \"LemonButton__content\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"a\", \"$el_text\": \"Cohorts\", \"classes\": [\"Link\", \"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__class\": \"Link LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__href\": \"/cohorts\", \"attr__data-attr\": \"menu-item-cohorts\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 13, \"nth_of_type\": 10}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider__content\"], \"attr__class\": \"SideBar__slider__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1913}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9664-7a21-9852-42ce19c880c6","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9664-7a21-9852-42ce19c880c6\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ymf6pk54unynhu8h\", \"$time\": 1694769321.573, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"show-product-intro-existing-products\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1810}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-966b-7dcc-abee-f41b896a74cc","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-966b-7dcc-abee-f41b896a74cc\", \"event\": \"recording viewed with no playtime summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"uz55qy2obbr2z36g\", \"$time\": 1694769321.58, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 3288, \"play_time_ms\": 0, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"engagement_score\": 0, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1803}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-966e-7272-8b9d-bffdc5c840d2","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-966e-7272-8b9d-bffdc5c840d2\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"5w3t82ytjay0nqiw\", \"$time\": 1694769321.582, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Cohorts \\u2022 PostHog\"}, \"offset\": 1801}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9d2f-72eb-8999-bec9f2a9f542","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9d2f-72eb-8999-bec9f2a9f542\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/cohorts\", \"$host\": \"localhost:8000\", \"$pathname\": \"/cohorts\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"tk1tnyoiz4gbnk2t\", \"$time\": 1694769323.311, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"a\", \"$el_text\": \"Persons & Groups\", \"classes\": [\"Link\", \"LemonButton\", \"LemonButton--tertiary\", \"LemonButton--status-stealth\", \"LemonButton--full-width\", \"LemonButton--has-icon\"], \"attr__class\": \"Link LemonButton LemonButton--tertiary LemonButton--status-stealth LemonButton--full-width LemonButton--has-icon\", \"attr__href\": \"/persons\", \"attr__data-attr\": \"menu-item-persons\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"nth_child\": 12, \"nth_of_type\": 9}, {\"tag_name\": \"ul\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider__content\"], \"attr__class\": \"SideBar__slider__content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__slider\"], \"attr__class\": \"SideBar__slider\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 71}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-9d37-712e-b09d-61c3f8cf3624","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9d37-712e-b09d-61c3f8cf3624\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"648njm5gtwx2efjj\", \"$time\": 1694769323.319, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Persons & Groups \\u2022 PostHog\"}, \"offset\": 64}","now":"2023-09-15T09:15:23.391445+00:00","sent_at":"2023-09-15T09:15:23.385000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?compression=gzip-js&ip=1&_=1694769326396&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:26.539412+00:00","body":"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","output":[{"uuid":"018a981f-9db5-7188-8161-91e9fd602fd7","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-9db5-7188-8161-91e9fd602fd7\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/persons\", \"$host\": \"localhost:8000\", \"$pathname\": \"/persons\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"c5yz9qfwa86vhxab\", \"$time\": 1694769323.445, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"PersonsNode\"}, \"duration\": 102, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2945}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a25d-743f-a813-6d909390f5c9","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a25d-743f-a813-6d909390f5c9\", \"event\": \"$feature_flag_called\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"i100qaub5hceuld4\", \"$time\": 1694769324.637, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$feature_flag\": \"cs-dashboards\", \"$feature_flag_response\": false, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1753}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a264-7a2a-9439-198973cc7878","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a264-7a2a-9439-198973cc7878\", \"event\": \"recording viewed with no playtime summary\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"wzrv024h7b0m7a8c\", \"$time\": 1694769324.645, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"viewed_time_ms\": 1295, \"play_time_ms\": 0, \"recording_duration_ms\": 0, \"rrweb_warning_count\": 0, \"error_count_during_recording_playback\": 0, \"engagement_score\": 0, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1745}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a266-73d2-a66f-1fbcc32d9f02","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a266-73d2-a66f-1fbcc32d9f02\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ksstzx9julgopw7a\", \"$time\": 1694769324.647, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"Persons \\u2022 PostHog\"}, \"offset\": 1743}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a4b3-7b40-b430-9495d1bbed93","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a4b3-7b40-b430-9495d1bbed93\", \"event\": \"person viewed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"s2fzjz6c7t0ekgtm\", \"$time\": 1694769325.236, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"properties_count\": 18, \"has_email\": true, \"has_name\": false, \"custom_properties_count\": 4, \"posthog_properties_count\": 14, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1154}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a676-7722-bece-2f9b3d6b84ce","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a676-7722-bece-2f9b3d6b84ce\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"npyk617r6ht5qzbh\", \"$time\": 1694769325.686, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"person-session-recordings-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Recordings\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 3, \"nth_of_type\": 3}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 68.19999694824219px; --lemon-tabs-slider-offset: 0px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 704}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a67b-7a6f-9637-bcaacec2496c","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a67b-7a6f-9637-bcaacec2496c\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"fhsu6w5edy7tvvuy\", \"$time\": 1694769325.691, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 699}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-a783-7924-b938-3a789f71e25a","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-a783-7924-b938-3a789f71e25a\", \"event\": \"recording list fetched\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"fcebvj6tugbw47wk\", \"$time\": 1694769325.955, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"load_time\": 145, \"listing_version\": \"3\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 435}","now":"2023-09-15T09:15:26.539412+00:00","sent_at":"2023-09-15T09:15:26.396000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?compression=gzip-js&ip=1&_=1694769329412&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:29.500667+00:00","body":"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","output":[{"uuid":"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0ovdk9xlnv9fhfak\", \"$time\": 1694769326.837, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"persons-related-flags-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Feature flags\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 74.26666259765625px; --lemon-tabs-slider-offset: 176.29998779296875px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2572}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-aafa-79e8-abf4-4e68eb64c30f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aafa-79e8-abf4-4e68eb64c30f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"y3x76ea6mqqi2q0a\", \"$time\": 1694769326.842, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 2567}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af3d-79d4-be4a-d729c776e0da","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af3d-79d4-be4a-d729c776e0da\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ltw7rl4fhi4bgq63\", \"$time\": 1694769327.934, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 86.23332214355469px; --lemon-tabs-slider-offset: 367.0999755859375px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1475}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af46-7832-9a69-c566751c6625","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af46-7832-9a69-c566751c6625\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"6yl35wdtv5v11o6c\", \"$time\": 1694769327.942, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 1467}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-b008-737a-bb40-6a6a81ba2244","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-b008-737a-bb40-6a6a81ba2244\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"8guwvm82yhwyhfo6\", \"$time\": 1694769328.136, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"EventsQuery\", \"select\": [\"*\", \"event\", \"person\", \"coalesce(properties.$current_url, properties.$screen_name) -- Url / Screen\", \"properties.$lib\", \"timestamp\"], \"personId\": \"018a981f-4c9a-0000-68ff-417481f7c5b5\", \"after\": \"-24h\"}, \"duration\": 171, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1273}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} -### Compression query param mismatch, to confirm gzip autodetection +### posthog-js with compression query param mismatch, to confirm gzip autodetection {"path":"/e/?compression=gzip-js&ip=1&_=1694769302319&ver=1.78.5","method":"POST","content_encoding":"","content_type":"application/x-www-form-urlencoded","ip":"127.0.0.1","now":"2023-09-15T09:15:02.321230+00:00","body":"ZGF0YT1leUoxZFdsa0lqb2lNREU0WVRrNE1XWXROR0l5WlMwM1ltUXpMV0ppTXpBdE5qWXhOalkxTjJNek9HWmhJaXdpWlhabGJuUWlPaUlrYjNCMFgybHVJaXdpY0hKdmNHVnlkR2xsY3lJNmV5SWtiM01pT2lKTllXTWdUMU1nV0NJc0lpUnZjMTkyWlhKemFXOXVJam9pTVRBdU1UVXVNQ0lzSWlSaWNtOTNjMlZ5SWpvaVJtbHlaV1p2ZUNJc0lpUmtaWFpwWTJWZmRIbHdaU0k2SWtSbGMydDBiM0FpTENJa1kzVnljbVZ1ZEY5MWNtd2lPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPamd3TURBdklpd2lKR2h2YzNRaU9pSnNiMk5oYkdodmMzUTZPREF3TUNJc0lpUndZWFJvYm1GdFpTSTZJaThpTENJa1luSnZkM05sY2w5MlpYSnphVzl1SWpveE1UY3NJaVJpY205M2MyVnlYMnhoYm1kMVlXZGxJam9pWlc0dFZWTWlMQ0lrYzJOeVpXVnVYMmhsYVdkb2RDSTZNVEExTWl3aUpITmpjbVZsYmw5M2FXUjBhQ0k2TVRZeU1Dd2lKSFpwWlhkd2IzSjBYMmhsYVdkb2RDSTZPVEV4TENJa2RtbGxkM0J2Y25SZmQybGtkR2dpT2pFMU5EZ3NJaVJzYVdJaU9pSjNaV0lpTENJa2JHbGlYM1psY25OcGIyNGlPaUl4TGpjNExqVWlMQ0lrYVc1elpYSjBYMmxrSWpvaU1XVTRaV1p5WkdSbE1HSTBNV2RtYkNJc0lpUjBhVzFsSWpveE5qazBOelk1TXpBeUxqTXhPQ3dpWkdsemRHbHVZM1JmYVdRaU9pSXdNVGhoT1RneFppMDBZakprTFRjNFlXVXRPVFEzWWkxaVpXUmlZV0V3TW1Fd1pqUWlMQ0lrWkdWMmFXTmxYMmxrSWpvaU1ERTRZVGs0TVdZdE5HSXlaQzAzT0dGbExUazBOMkl0WW1Wa1ltRmhNREpoTUdZMElpd2lKSEpsWm1WeWNtVnlJam9pSkdScGNtVmpkQ0lzSWlSeVpXWmxjbkpwYm1kZlpHOXRZV2x1SWpvaUpHUnBjbVZqZENJc0luUnZhMlZ1SWpvaWNHaGpYM0ZuVlVad05YZDZNa0pxUXpSU2MwWnFUVWR3VVROUVIwUnljMmsyVVRCRFJ6QkRVRFJPUVdGak1Fa2lMQ0lrYzJWemMybHZibDlwWkNJNklqQXhPR0U1T0RGbUxUUmlNbVV0TjJKa015MWlZak13TFRZMk1UZGlaalE0T0RnMk9TSXNJaVIzYVc1a2IzZGZhV1FpT2lJd01UaGhPVGd4WmkwMFlqSmxMVGRpWkRNdFltSXpNQzAyTmpFNE1UQm1aV1EyTldZaWZTd2lkR2x0WlhOMFlXMXdJam9pTWpBeU15MHdPUzB4TlZRd09Ub3hOVG93TWk0ek1UaGFJbjAlM0Q=","output":[{"uuid":"018a981f-4b2e-7bd3-bb30-6616657c38fa","distinct_id":"018a981f-4b2d-78ae-947b-bedbaa02a0f4","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-4b2e-7bd3-bb30-6616657c38fa\", \"event\": \"$opt_in\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/\", \"$host\": \"localhost:8000\", \"$pathname\": \"/\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"1e8efrdde0b41gfl\", \"$time\": 1694769302.318, \"distinct_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"timestamp\": \"2023-09-15T09:15:02.318Z\"}","now":"2023-09-15T09:15:02.321230+00:00","sent_at":"2023-09-15T09:15:02.319000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} {"path":"/e/?ip=1&_=1694769329412&ver=1.78.5","method":"POST","content_encoding":"","content_type":"text/plain","ip":"127.0.0.1","now":"2023-09-15T09:15:29.500667+00:00","body":"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","output":[{"uuid":"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aaf5-7ff0-9ffd-8f5e1f85717f\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=sessionRecordings\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"0ovdk9xlnv9fhfak\", \"$time\": 1694769326.837, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"span\", \"attr__data-attr\": \"persons-related-flags-tab\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"Feature flags\"}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 5, \"nth_of_type\": 5}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 74.26666259765625px; --lemon-tabs-slider-offset: 176.29998779296875px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 2572}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-aafa-79e8-abf4-4e68eb64c30f","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-aafa-79e8-abf4-4e68eb64c30f\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"y3x76ea6mqqi2q0a\", \"$time\": 1694769326.842, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 2567}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af3d-79d4-be4a-d729c776e0da","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af3d-79d4-be4a-d729c776e0da\", \"event\": \"$autocapture\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=featureFlags\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"ltw7rl4fhi4bgq63\", \"$time\": 1694769327.934, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"$event_type\": \"click\", \"$ce_version\": 1, \"$elements\": [{\"tag_name\": \"div\", \"classes\": [\"LemonTabs__tab-content\"], \"attr__class\": \"LemonTabs__tab-content\", \"nth_child\": 1, \"nth_of_type\": 1, \"$el_text\": \"\"}, {\"tag_name\": \"li\", \"classes\": [\"LemonTabs__tab\"], \"attr__class\": \"LemonTabs__tab\", \"attr__role\": \"tab\", \"attr__aria-selected\": \"false\", \"attr__tabindex\": \"0\", \"nth_child\": 2, \"nth_of_type\": 2}, {\"tag_name\": \"ul\", \"classes\": [\"LemonTabs__bar\"], \"attr__class\": \"LemonTabs__bar\", \"attr__role\": \"tablist\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"LemonTabs\"], \"attr__class\": \"LemonTabs\", \"attr__style\": \"--lemon-tabs-slider-width: 86.23332214355469px; --lemon-tabs-slider-offset: 367.0999755859375px;\", \"attr__data-attr\": \"persons-tabs\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"main-app-content\"], \"attr__class\": \"main-app-content\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"classes\": [\"SideBar__content\"], \"attr__class\": \"SideBar__content\", \"nth_child\": 4, \"nth_of_type\": 4}, {\"tag_name\": \"div\", \"classes\": [\"SideBar\"], \"attr__class\": \"SideBar\", \"nth_child\": 4, \"nth_of_type\": 3}, {\"tag_name\": \"div\", \"classes\": [\"h-screen\", \"flex\", \"flex-col\"], \"attr__class\": \"h-screen flex flex-col\", \"nth_child\": 1, \"nth_of_type\": 1}, {\"tag_name\": \"div\", \"attr__id\": \"root\", \"nth_child\": 3, \"nth_of_type\": 1}, {\"tag_name\": \"body\", \"attr__theme\": \"light\", \"attr__class\": \"\", \"nth_child\": 2, \"nth_of_type\": 1}], \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1475}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-af46-7832-9a69-c566751c6625","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-af46-7832-9a69-c566751c6625\", \"event\": \"$pageview\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"6yl35wdtv5v11o6c\", \"$time\": 1694769327.942, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\", \"title\": \"xavier@posthog.com \\u2022 Persons \\u2022 PostHog\"}, \"offset\": 1467}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"},{"uuid":"018a981f-b008-737a-bb40-6a6a81ba2244","distinct_id":"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"018a981f-b008-737a-bb40-6a6a81ba2244\", \"event\": \"query completed\", \"properties\": {\"$os\": \"Mac OS X\", \"$os_version\": \"10.15.0\", \"$browser\": \"Firefox\", \"$device_type\": \"Desktop\", \"$current_url\": \"http://localhost:8000/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4#activeTab=events\", \"$host\": \"localhost:8000\", \"$pathname\": \"/person/018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$browser_version\": 117, \"$browser_language\": \"en-US\", \"$screen_height\": 1052, \"$screen_width\": 1620, \"$viewport_height\": 911, \"$viewport_width\": 1548, \"$lib\": \"web\", \"$lib_version\": \"1.78.5\", \"$insert_id\": \"8guwvm82yhwyhfo6\", \"$time\": 1694769328.136, \"distinct_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"$device_id\": \"018a981f-4b2d-78ae-947b-bedbaa02a0f4\", \"$user_id\": \"pWAkITyQ3CN7332UqQxnH6p3FV8VJd7pY647EdNkxWc\", \"is_demo_project\": false, \"$groups\": {\"project\": \"018a981e-f8b2-0000-d5ed-9380ee5aad4b\", \"organization\": \"018a981e-f55c-0000-a85d-25c15e555d5d\", \"instance\": \"http://localhost:8000\"}, \"$autocapture_disabled_server_side\": false, \"$active_feature_flags\": [], \"$feature_flag_payloads\": {}, \"realm\": \"hosted-clickhouse\", \"email_service_available\": false, \"slack_service_available\": false, \"has_billing_plan\": false, \"percentage_usage.product_analytics\": 0, \"current_usage.product_analytics\": 0, \"percentage_usage.session_replay\": 0, \"current_usage.session_replay\": 0, \"percentage_usage.feature_flags\": 0, \"current_usage.feature_flags\": 0, \"$referrer\": \"$direct\", \"$referring_domain\": \"$direct\", \"query\": {\"kind\": \"EventsQuery\", \"select\": [\"*\", \"event\", \"person\", \"coalesce(properties.$current_url, properties.$screen_name) -- Url / Screen\", \"properties.$lib\", \"timestamp\"], \"personId\": \"018a981f-4c9a-0000-68ff-417481f7c5b5\", \"after\": \"-24h\"}, \"duration\": 171, \"token\": \"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I\", \"$session_id\": \"018a981f-4b2e-7bd3-bb30-6617bf488869\", \"$window_id\": \"018a981f-4b2e-7bd3-bb30-661810fed65f\"}, \"offset\": 1273}","now":"2023-09-15T09:15:29.500667+00:00","sent_at":"2023-09-15T09:15:29.412000+00:00","token":"phc_qgUFp5wz2BjC4RsFjMGpQ3PGDrsi6Q0CG0CP4NAac0I"}]} ### nodejs, default params -{"path":"/batch/","method":"POST","content_encoding":"","content_type":"application/json","ip":"127.0.0.1","now":"2024-04-17T14:40:56.918578+00:00","body":"eyJhcGlfa2V5IjoicGhjX05WWk95WWI4aFgyR0RzODBtUWQwNzJPVDBXVXJBYmRRb200WGVDVjVOZmgiLCJiYXRjaCI6W3siZGlzdGluY3RfaWQiOiJpZDEiLCJldmVudCI6InRoaXMgZXZlbnQiLCJwcm9wZXJ0aWVzIjp7IiRsaWIiOiJwb3N0aG9nLW5vZGUiLCIkbGliX3ZlcnNpb24iOiI0LjAuMCIsIiRnZW9pcF9kaXNhYmxlIjp0cnVlfSwidHlwZSI6ImNhcHR1cmUiLCJsaWJyYXJ5IjoicG9zdGhvZy1ub2RlIiwibGlicmFyeV92ZXJzaW9uIjoiNC4wLjAiLCJ0aW1lc3RhbXAiOiIyMDI0LTA0LTE3VDE0OjQwOjU2LjkwMFoiLCJ1dWlkIjoiMDE4ZWVjODAtZjA0NC03YzJkLWI5NjYtMzVlZmM4ZWQ2MWQ1In0seyJkaXN0aW5jdF9pZCI6ImRpc3RpbmN0X2lkX29mX3RoZV91c2VyIiwiZXZlbnQiOiJ1c2VyIHNpZ25lZCB1cCIsInByb3BlcnRpZXMiOnsibG9naW5fdHlwZSI6ImVtYWlsIiwiaXNfZnJlZV90cmlhbCI6dHJ1ZSwiJGxpYiI6InBvc3Rob2ctbm9kZSIsIiRsaWJfdmVyc2lvbiI6IjQuMC4wIiwiJGdlb2lwX2Rpc2FibGUiOnRydWV9LCJ0eXBlIjoiY2FwdHVyZSIsImxpYnJhcnkiOiJwb3N0aG9nLW5vZGUiLCJsaWJyYXJ5X3ZlcnNpb24iOiI0LjAuMCIsInRpbWVzdGFtcCI6IjIwMjQtMDQtMTdUMTQ6NDA6NTYuOTAwWiIsInV1aWQiOiIwMThlZWM4MC1mMDQ0LTdjMmQtYjk2Ni0zNWYwNTA1OWYzNzUifV0sInNlbnRfYXQiOiIyMDI0LTA0LTE3VDE0OjQwOjU2LjkwMFoifQ==","output":[{"uuid":"018eec80-f044-7c2d-b966-35efc8ed61d5","distinct_id":"id1","ip":"127.0.0.1","site_url":"http://127.0.0.1:8000","data":"{\"distinct_id\": \"id1\", \"event\": \"this event\", \"properties\": {\"$lib\": \"posthog-node\", \"$lib_version\": \"4.0.0\", \"$geoip_disable\": true}, \"type\": \"capture\", \"library\": \"posthog-node\", \"library_version\": \"4.0.0\", \"timestamp\": \"2024-04-17T14:40:56.900Z\", \"uuid\": \"018eec80-f044-7c2d-b966-35efc8ed61d5\"}","now":"2024-04-17T14:40:56.918578+00:00","sent_at":"2024-04-17T14:40:56.900000+00:00","token":"phc_NVZOyYb8hX2GDs80mQd072OT0WUrAbdQom4XeCV5Nfh"},{"uuid":"018eec80-f044-7c2d-b966-35f05059f375","distinct_id":"distinct_id_of_the_user","ip":"127.0.0.1","site_url":"http://127.0.0.1:8000","data":"{\"distinct_id\": \"distinct_id_of_the_user\", \"event\": \"user signed up\", \"properties\": {\"login_type\": \"email\", \"is_free_trial\": true, \"$lib\": \"posthog-node\", \"$lib_version\": \"4.0.0\", \"$geoip_disable\": true}, \"type\": \"capture\", \"library\": \"posthog-node\", \"library_version\": \"4.0.0\", \"timestamp\": \"2024-04-17T14:40:56.900Z\", \"uuid\": \"018eec80-f044-7c2d-b966-35f05059f375\"}","now":"2024-04-17T14:40:56.918578+00:00","sent_at":"2024-04-17T14:40:56.900000+00:00","token":"phc_NVZOyYb8hX2GDs80mQd072OT0WUrAbdQom4XeCV5Nfh"}]} \ No newline at end of file +{"path":"/batch/","method":"POST","content_encoding":"","content_type":"application/json","ip":"127.0.0.1","now":"2024-04-17T14:40:56.918578+00:00","body":"eyJhcGlfa2V5IjoicGhjX05WWk95WWI4aFgyR0RzODBtUWQwNzJPVDBXVXJBYmRRb200WGVDVjVOZmgiLCJiYXRjaCI6W3siZGlzdGluY3RfaWQiOiJpZDEiLCJldmVudCI6InRoaXMgZXZlbnQiLCJwcm9wZXJ0aWVzIjp7IiRsaWIiOiJwb3N0aG9nLW5vZGUiLCIkbGliX3ZlcnNpb24iOiI0LjAuMCIsIiRnZW9pcF9kaXNhYmxlIjp0cnVlfSwidHlwZSI6ImNhcHR1cmUiLCJsaWJyYXJ5IjoicG9zdGhvZy1ub2RlIiwibGlicmFyeV92ZXJzaW9uIjoiNC4wLjAiLCJ0aW1lc3RhbXAiOiIyMDI0LTA0LTE3VDE0OjQwOjU2LjkwMFoiLCJ1dWlkIjoiMDE4ZWVjODAtZjA0NC03YzJkLWI5NjYtMzVlZmM4ZWQ2MWQ1In0seyJkaXN0aW5jdF9pZCI6ImRpc3RpbmN0X2lkX29mX3RoZV91c2VyIiwiZXZlbnQiOiJ1c2VyIHNpZ25lZCB1cCIsInByb3BlcnRpZXMiOnsibG9naW5fdHlwZSI6ImVtYWlsIiwiaXNfZnJlZV90cmlhbCI6dHJ1ZSwiJGxpYiI6InBvc3Rob2ctbm9kZSIsIiRsaWJfdmVyc2lvbiI6IjQuMC4wIiwiJGdlb2lwX2Rpc2FibGUiOnRydWV9LCJ0eXBlIjoiY2FwdHVyZSIsImxpYnJhcnkiOiJwb3N0aG9nLW5vZGUiLCJsaWJyYXJ5X3ZlcnNpb24iOiI0LjAuMCIsInRpbWVzdGFtcCI6IjIwMjQtMDQtMTdUMTQ6NDA6NTYuOTAwWiIsInV1aWQiOiIwMThlZWM4MC1mMDQ0LTdjMmQtYjk2Ni0zNWYwNTA1OWYzNzUifV0sInNlbnRfYXQiOiIyMDI0LTA0LTE3VDE0OjQwOjU2LjkwMFoifQ==","output":[{"uuid":"018eec80-f044-7c2d-b966-35efc8ed61d5","distinct_id":"id1","ip":"127.0.0.1","site_url":"http://127.0.0.1:8000","data":"{\"distinct_id\": \"id1\", \"event\": \"this event\", \"properties\": {\"$lib\": \"posthog-node\", \"$lib_version\": \"4.0.0\", \"$geoip_disable\": true}, \"type\": \"capture\", \"library\": \"posthog-node\", \"library_version\": \"4.0.0\", \"timestamp\": \"2024-04-17T14:40:56.900Z\", \"uuid\": \"018eec80-f044-7c2d-b966-35efc8ed61d5\"}","now":"2024-04-17T14:40:56.918578+00:00","sent_at":"2024-04-17T14:40:56.900000+00:00","token":"phc_NVZOyYb8hX2GDs80mQd072OT0WUrAbdQom4XeCV5Nfh"},{"uuid":"018eec80-f044-7c2d-b966-35f05059f375","distinct_id":"distinct_id_of_the_user","ip":"127.0.0.1","site_url":"http://127.0.0.1:8000","data":"{\"distinct_id\": \"distinct_id_of_the_user\", \"event\": \"user signed up\", \"properties\": {\"login_type\": \"email\", \"is_free_trial\": true, \"$lib\": \"posthog-node\", \"$lib_version\": \"4.0.0\", \"$geoip_disable\": true}, \"type\": \"capture\", \"library\": \"posthog-node\", \"library_version\": \"4.0.0\", \"timestamp\": \"2024-04-17T14:40:56.900Z\", \"uuid\": \"018eec80-f044-7c2d-b966-35f05059f375\"}","now":"2024-04-17T14:40:56.918578+00:00","sent_at":"2024-04-17T14:40:56.900000+00:00","token":"phc_NVZOyYb8hX2GDs80mQd072OT0WUrAbdQom4XeCV5Nfh"}]} +### batch from temporal batch export +{"historical_migration":true,"path":"/batch","method":"POST","content_encoding":"","content_type":"application/json","ip":"127.0.0.1","now":"2024-04-29T14:49:29.553700+00:00","body":"eyJhcGlfa2V5IjogImFiY2RlZjEyMzQ1NiIsImhpc3RvcmljYWxfbWlncmF0aW9uIjp0cnVlLCJiYXRjaCI6IFt7InV1aWQiOiJmNGI3NmJhNy1lMGZmLTRkYTctOTBkNS02ZTkwZjY4ZWRjMmUiLCJkaXN0aW5jdF9pZCI6ImJhYTkyOGNiLTU4YjUtNGY0Zi04ZWQ0LWJkYzMzMzQ4YmRjYyIsInRpbWVzdGFtcCI6IjIwMjMtMDQtMjRUMDY6MzQ6MDArMDA6MDAiLCJldmVudCI6InRlc3Qtbm8tcHJvcC0wIiwicHJvcGVydGllcyI6eyIkZ2VvaXBfZGlzYWJsZSI6dHJ1ZX19LHsidXVpZCI6ImFhNDZlODNmLWJmNzctNDJjNy04OGExLTczMGE5MmFlMjZhNyIsImRpc3RpbmN0X2lkIjoiMzhlMWM3MTAtYTAzNC00MjAzLTg2NzUtZTVkNDgxY2FjOTIzIiwidGltZXN0YW1wIjoiMjAyMy0wNC0yMVQyMzo1MzowMCswMDowMCIsImV2ZW50IjoidGVzdC0xIiwicHJvcGVydGllcyI6eyIkYnJvd3NlciI6IkNocm9tZSIsIiRvcyI6Ik1hYyBPUyBYIiwiJGdlb2lwX2Rpc2FibGUiOnRydWV9fSx7InV1aWQiOiJiNWY1ZGFhOS00YmRlLTQ2ZDAtYjk2YS1kM2VkZmI4N2ViODEiLCJkaXN0aW5jdF9pZCI6ImZhMWI1NjFhLTAxNGMtNDMzNS1hM2VmLTk4YzA2ZTRmZDdlOCIsInRpbWVzdGFtcCI6IjIwMjMtMDQtMjJUMTU6MDY6MDArMDA6MDAiLCJldmVudCI6InRlc3QtMCIsInByb3BlcnRpZXMiOnsiJGJyb3dzZXIiOiJDaHJvbWUiLCIkb3MiOiJNYWMgT1MgWCIsIiRnZW9pcF9kaXNhYmxlIjp0cnVlfX0seyJ1dWlkIjoiYjQyOTM1ZTUtZDIwNi00ODRiLTk1OTEtODcxZjI2MmYzMGRkIiwiZGlzdGluY3RfaWQiOiIxMDk5OGJmZS03M2Y4LTQ3MDktYTBkNi0yMDI3MTk1MjA2MDYiLCJ0aW1lc3RhbXAiOiIyMDIzLTA0LTI0VDEzOjQ2OjAwKzAwOjAwIiwiZXZlbnQiOiJ0ZXN0LTIiLCJwcm9wZXJ0aWVzIjp7IiRicm93c2VyIjoiQ2hyb21lIiwiJG9zIjoiTWFjIE9TIFgiLCIkZ2VvaXBfZGlzYWJsZSI6dHJ1ZX19XX0=","output":[{"uuid":"f4b76ba7-e0ff-4da7-90d5-6e90f68edc2e","distinct_id":"baa928cb-58b5-4f4f-8ed4-bdc33348bdcc","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"f4b76ba7-e0ff-4da7-90d5-6e90f68edc2e\", \"distinct_id\": \"baa928cb-58b5-4f4f-8ed4-bdc33348bdcc\", \"timestamp\": \"2023-04-24T06:34:00+00:00\", \"event\": \"test-no-prop-0\", \"properties\": {\"$geoip_disable\": true}}","now":"2024-04-29T14:49:29.553700+00:00","sent_at":"","token":"abcdef123456"},{"uuid":"aa46e83f-bf77-42c7-88a1-730a92ae26a7","distinct_id":"38e1c710-a034-4203-8675-e5d481cac923","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"aa46e83f-bf77-42c7-88a1-730a92ae26a7\", \"distinct_id\": \"38e1c710-a034-4203-8675-e5d481cac923\", \"timestamp\": \"2023-04-21T23:53:00+00:00\", \"event\": \"test-1\", \"properties\": {\"$browser\": \"Chrome\", \"$os\": \"Mac OS X\", \"$geoip_disable\": true}}","now":"2024-04-29T14:49:29.553700+00:00","sent_at":"","token":"abcdef123456"},{"uuid":"b5f5daa9-4bde-46d0-b96a-d3edfb87eb81","distinct_id":"fa1b561a-014c-4335-a3ef-98c06e4fd7e8","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"b5f5daa9-4bde-46d0-b96a-d3edfb87eb81\", \"distinct_id\": \"fa1b561a-014c-4335-a3ef-98c06e4fd7e8\", \"timestamp\": \"2023-04-22T15:06:00+00:00\", \"event\": \"test-0\", \"properties\": {\"$browser\": \"Chrome\", \"$os\": \"Mac OS X\", \"$geoip_disable\": true}}","now":"2024-04-29T14:49:29.553700+00:00","sent_at":"","token":"abcdef123456"},{"uuid":"b42935e5-d206-484b-9591-871f262f30dd","distinct_id":"10998bfe-73f8-4709-a0d6-202719520606","ip":"127.0.0.1","site_url":"http://localhost:8000","data":"{\"uuid\": \"b42935e5-d206-484b-9591-871f262f30dd\", \"distinct_id\": \"10998bfe-73f8-4709-a0d6-202719520606\", \"timestamp\": \"2023-04-24T13:46:00+00:00\", \"event\": \"test-2\", \"properties\": {\"$browser\": \"Chrome\", \"$os\": \"Mac OS X\", \"$geoip_disable\": true}}","now":"2024-04-29T14:49:29.553700+00:00","sent_at":"","token":"abcdef123456"}]} +### end \ No newline at end of file From db332e59f2c9cb75b2fb91d1d842df3203cab6a4 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 2 May 2024 12:04:53 +0200 Subject: [PATCH 229/249] fix capture CI (#32) --- Cargo.lock | 2 +- capture/Cargo.toml | 2 +- capture/tests/django_compat.rs | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82c787aded2cc..9d7fc97883afd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -267,7 +267,7 @@ dependencies = [ [[package]] name = "axum-test-helper" version = "0.4.0" -source = "git+https://github.com/orphan-rs/axum-test-helper.git#8ca0aedaad5a6bdf351c34d5b80593ae1b7d2f3f" +source = "git+https://github.com/posthog/axum-test-helper.git#002d45d8bbbac04e6a474e9a850b7f023a87d32f" dependencies = [ "axum 0.7.5", "bytes", diff --git a/capture/Cargo.toml b/capture/Cargo.toml index 4e35d10be7b85..ae5ad9a3bd127 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -38,4 +38,4 @@ uuid = { workspace = true } [dev-dependencies] assert-json-diff = { workspace = true } -axum-test-helper = { git = "https://github.com/orphan-rs/axum-test-helper.git" } # TODO: remove, directly use reqwest like capture-server tests +axum-test-helper = { git = "https://github.com/posthog/axum-test-helper.git" } # TODO: remove, directly use reqwest like capture-server tests diff --git a/capture/tests/django_compat.rs b/capture/tests/django_compat.rs index d1d313cd5e112..87b0a1b269256 100644 --- a/capture/tests/django_compat.rs +++ b/capture/tests/django_compat.rs @@ -201,6 +201,11 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { if let Some(object) = expected.as_object_mut() { // site_url is unused in the pipeline now, let's drop it object.remove("site_url"); + + // Remove sent_at field if empty: Rust will skip marshalling it + if let Some(None) = object.get("sent_at").map(|v| v.as_str()) { + object.remove("sent_at"); + } } let match_config = assert_json_diff::Config::new(assert_json_diff::CompareMode::Strict); @@ -209,7 +214,9 @@ async fn it_matches_django_capture_behaviour() -> anyhow::Result<()> { { println!( "record mismatch at line {}, event {}: {}", - line_number, event_number, e + line_number + 1, + event_number, + e ); mismatches += 1; } From c6d5c67d901248d0b8019973201ada1db704c587 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 2 May 2024 12:11:41 +0200 Subject: [PATCH 230/249] ci: move cargo check to linting job (#33) --- .github/workflows/rust.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ed5acb2ed6a90..a525f3bf942c1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -67,9 +67,6 @@ jobs: - name: Run cargo test run: cargo test --all-features - - name: Run cargo check - run: cargo check --all-features - linting: runs-on: depot-ubuntu-22.04-4 @@ -89,12 +86,15 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} - + + - name: Check format + run: cargo fmt -- --check + - name: Run clippy run: cargo clippy -- -D warnings - - name: Check format - run: cargo fmt -- --check + - name: Run cargo check + run: cargo check --all-features shear: runs-on: depot-ubuntu-22.04-4 From f7e02cc92c68de49227e6b5217a1d120a70dfc5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Far=C3=ADas=20Santana?= Date: Fri, 3 May 2024 10:34:51 +0200 Subject: [PATCH 231/249] feat: Track response in error details (#29) Co-authored-by: Brett Hoerner --- Cargo.lock | 2 + Cargo.toml | 2 +- hook-worker/src/error.rs | 111 ++++++++++++++++++++++-- hook-worker/src/lib.rs | 1 + hook-worker/src/util.rs | 35 ++++++++ hook-worker/src/worker.rs | 175 +++++++++++++++++++++++++++----------- 6 files changed, 270 insertions(+), 56 deletions(-) create mode 100644 hook-worker/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 9d7fc97883afd..7cc582ee6696c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2343,10 +2343,12 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg 0.52.0", ] diff --git a/Cargo.toml b/Cargo.toml index d34cd0ae39231..d1f75274aa629 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ metrics = "0.22.0" metrics-exporter-prometheus = "0.14.0" rand = "0.8.5" rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl", "tracing"] } -reqwest = { version = "0.12.3" } +reqwest = { version = "0.12.3", features = ["stream"] } serde = { version = "1.0", features = ["derive"] } serde_derive = { version = "1.0" } serde_json = { version = "1.0" } diff --git a/hook-worker/src/error.rs b/hook-worker/src/error.rs index 914ffb1b2e2ee..48468bc65f544 100644 --- a/hook-worker/src/error.rs +++ b/hook-worker/src/error.rs @@ -1,24 +1,125 @@ +use std::fmt; use std::time; -use hook_common::pgqueue; +use hook_common::{pgqueue, webhook::WebhookJobError}; use thiserror::Error; -/// Enumeration of errors related to webhook job processing in the WebhookWorker. +/// Enumeration of error classes handled by `WebhookWorker`. #[derive(Error, Debug)] pub enum WebhookError { + #[error(transparent)] + Parse(#[from] WebhookParseError), + #[error(transparent)] + Request(#[from] WebhookRequestError), +} + +/// Enumeration of parsing errors that can occur as `WebhookWorker` sets up a webhook. +#[derive(Error, Debug)] +pub enum WebhookParseError { #[error("{0} is not a valid HttpMethod")] ParseHttpMethodError(String), #[error("error parsing webhook headers")] ParseHeadersError(http::Error), #[error("error parsing webhook url")] ParseUrlError(url::ParseError), - #[error("a webhook could not be delivered but it could be retried later: {error}")] +} + +/// Enumeration of request errors that can occur as `WebhookWorker` sends a request. +#[derive(Error, Debug)] +pub enum WebhookRequestError { RetryableRequestError { error: reqwest::Error, + response: Option, retry_after: Option, }, - #[error("a webhook could not be delivered and it cannot be retried further: {0}")] - NonRetryableRetryableRequestError(reqwest::Error), + NonRetryableRetryableRequestError { + error: reqwest::Error, + response: Option, + }, +} + +/// Enumeration of errors that can occur while handling a `reqwest::Response`. +/// Currently, not consumed anywhere. Grouped here to support a common error type for +/// `utils::first_n_bytes_of_response`. +#[derive(Error, Debug)] +pub enum WebhookResponseError { + #[error("failed to parse a response as UTF8")] + ParseUTF8StringError(#[from] std::str::Utf8Error), + #[error("error while iterating over response body chunks")] + StreamIterationError(#[from] reqwest::Error), + #[error("attempted to slice a chunk of length {0} with an out of bounds index of {1}")] + ChunkOutOfBoundsError(usize, usize), +} + +/// Implement display of `WebhookRequestError` by appending to the underlying `reqwest::Error` +/// any response message if available. +impl fmt::Display for WebhookRequestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WebhookRequestError::RetryableRequestError { + error, response, .. + } + | WebhookRequestError::NonRetryableRetryableRequestError { error, response } => { + let response_message = match response { + Some(m) => m.to_string(), + None => "No response from the server".to_string(), + }; + writeln!(f, "{}", error)?; + write!(f, "{}", response_message)?; + + Ok(()) + } + } + } +} + +/// Implementation of `WebhookRequestError` designed to further describe the error. +/// In particular, we pass some calls to underyling `reqwest::Error` to provide more details. +impl WebhookRequestError { + pub fn is_timeout(&self) -> bool { + match self { + WebhookRequestError::RetryableRequestError { error, .. } + | WebhookRequestError::NonRetryableRetryableRequestError { error, .. } => { + error.is_timeout() + } + } + } + + pub fn is_status(&self) -> bool { + match self { + WebhookRequestError::RetryableRequestError { error, .. } + | WebhookRequestError::NonRetryableRetryableRequestError { error, .. } => { + error.is_status() + } + } + } + + pub fn status(&self) -> Option { + match self { + WebhookRequestError::RetryableRequestError { error, .. } + | WebhookRequestError::NonRetryableRetryableRequestError { error, .. } => { + error.status() + } + } + } +} + +impl From<&WebhookRequestError> for WebhookJobError { + fn from(error: &WebhookRequestError) -> Self { + if error.is_timeout() { + WebhookJobError::new_timeout(&error.to_string()) + } else if error.is_status() { + WebhookJobError::new_http_status( + error.status().expect("status code is defined").into(), + &error.to_string(), + ) + } else { + // Catch all other errors as `app_metrics::ErrorType::Connection` errors. + // Not all of `reqwest::Error` may strictly be connection errors, so our supported error types may need an extension + // depending on how strict error reporting has to be. + WebhookJobError::new_connection(&error.to_string()) + } + } } /// Enumeration of errors related to initialization and consumption of webhook jobs. diff --git a/hook-worker/src/lib.rs b/hook-worker/src/lib.rs index 22823c9a7e5cd..8488d15b20a36 100644 --- a/hook-worker/src/lib.rs +++ b/hook-worker/src/lib.rs @@ -1,3 +1,4 @@ pub mod config; pub mod error; +pub mod util; pub mod worker; diff --git a/hook-worker/src/util.rs b/hook-worker/src/util.rs new file mode 100644 index 0000000000000..00c5432168645 --- /dev/null +++ b/hook-worker/src/util.rs @@ -0,0 +1,35 @@ +use crate::error::WebhookResponseError; +use futures::StreamExt; +use reqwest::Response; + +pub async fn first_n_bytes_of_response( + response: Response, + n: usize, +) -> Result { + let mut body = response.bytes_stream(); + let mut buffer = String::with_capacity(n); + + while let Some(chunk) = body.next().await { + if buffer.len() >= n { + break; + } + + let chunk = chunk?; + let chunk_str = std::str::from_utf8(&chunk)?; + let upper_bound = std::cmp::min(n - buffer.len(), chunk_str.len()); + + if let Some(partial_chunk_str) = chunk_str.get(0..upper_bound) { + buffer.push_str(partial_chunk_str); + } else { + // For whatever reason we are out of bounds. We should never land here + // given the `std::cmp::min` usage, but I am being extra careful by not + // using a slice index that would panic instead. + return Err(WebhookResponseError::ChunkOutOfBoundsError( + chunk_str.len(), + upper_bound, + )); + } + } + + Ok(buffer) +} diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 5965d26a0801d..824f1e2f23a87 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -18,7 +18,8 @@ use reqwest::header; use tokio::sync; use tracing::error; -use crate::error::{WebhookError, WorkerError}; +use crate::error::{WebhookError, WebhookParseError, WebhookRequestError, WorkerError}; +use crate::util::first_n_bytes_of_response; /// A WebhookJob is any `PgQueueJob` with `WebhookJobParameters` and `WebhookJobMetadata`. trait WebhookJob: PgQueueJob + std::marker::Send { @@ -259,7 +260,7 @@ async fn process_webhook_job( Ok(()) } - Err(WebhookError::ParseHeadersError(e)) => { + Err(WebhookError::Parse(WebhookParseError::ParseHeadersError(e))) => { webhook_job .fail(WebhookJobError::new_parse(&e.to_string())) .await @@ -272,7 +273,7 @@ async fn process_webhook_job( Ok(()) } - Err(WebhookError::ParseHttpMethodError(e)) => { + Err(WebhookError::Parse(WebhookParseError::ParseHttpMethodError(e))) => { webhook_job .fail(WebhookJobError::new_parse(&e)) .await @@ -285,7 +286,7 @@ async fn process_webhook_job( Ok(()) } - Err(WebhookError::ParseUrlError(e)) => { + Err(WebhookError::Parse(WebhookParseError::ParseUrlError(e))) => { webhook_job .fail(WebhookJobError::new_parse(&e.to_string())) .await @@ -298,26 +299,53 @@ async fn process_webhook_job( Ok(()) } - Err(WebhookError::RetryableRequestError { error, retry_after }) => { - let retry_interval = - retry_policy.retry_interval(webhook_job.attempt() as u32, retry_after); - let current_queue = webhook_job.queue(); - let retry_queue = retry_policy.retry_queue(¤t_queue); - - match webhook_job - .retry(WebhookJobError::from(&error), retry_interval, retry_queue) - .await - { - Ok(_) => { - metrics::counter!("webhook_jobs_retried", &labels).increment(1); - - Ok(()) + Err(WebhookError::Request(request_error)) => { + let webhook_job_error = WebhookJobError::from(&request_error); + + match request_error { + WebhookRequestError::RetryableRequestError { + error, retry_after, .. + } => { + let retry_interval = + retry_policy.retry_interval(webhook_job.attempt() as u32, retry_after); + let current_queue = webhook_job.queue(); + let retry_queue = retry_policy.retry_queue(¤t_queue); + + match webhook_job + .retry(webhook_job_error, retry_interval, retry_queue) + .await + { + Ok(_) => { + metrics::counter!("webhook_jobs_retried", &labels).increment(1); + + Ok(()) + } + Err(RetryError::RetryInvalidError(RetryInvalidError { + job: webhook_job, + .. + })) => { + webhook_job + .fail(WebhookJobError::from(&error)) + .await + .map_err(|job_error| { + metrics::counter!("webhook_jobs_database_error", &labels) + .increment(1); + job_error + })?; + + metrics::counter!("webhook_jobs_failed", &labels).increment(1); + + Ok(()) + } + Err(RetryError::DatabaseError(job_error)) => { + metrics::counter!("webhook_jobs_database_error", &labels).increment(1); + Err(WorkerError::from(job_error)) + } + } } - Err(RetryError::RetryInvalidError(RetryInvalidError { - job: webhook_job, .. - })) => { + WebhookRequestError::NonRetryableRetryableRequestError { .. } => { webhook_job - .fail(WebhookJobError::from(&error)) + .fail(webhook_job_error) .await .map_err(|job_error| { metrics::counter!("webhook_jobs_database_error", &labels).increment(1); @@ -328,25 +356,8 @@ async fn process_webhook_job( Ok(()) } - Err(RetryError::DatabaseError(job_error)) => { - metrics::counter!("webhook_jobs_database_error", &labels).increment(1); - Err(WorkerError::from(job_error)) - } } } - Err(WebhookError::NonRetryableRetryableRequestError(error)) => { - webhook_job - .fail(WebhookJobError::from(&error)) - .await - .map_err(|job_error| { - metrics::counter!("webhook_jobs_database_error", &labels).increment(1); - job_error - })?; - - metrics::counter!("webhook_jobs_failed", &labels).increment(1); - - Ok(()) - } } } @@ -367,10 +378,10 @@ async fn send_webhook( body: String, ) -> Result { let method: http::Method = method.into(); - let url: reqwest::Url = (url).parse().map_err(WebhookError::ParseUrlError)?; + let url: reqwest::Url = (url).parse().map_err(WebhookParseError::ParseUrlError)?; let headers: reqwest::header::HeaderMap = (headers) .try_into() - .map_err(WebhookError::ParseHeadersError)?; + .map_err(WebhookParseError::ParseHeadersError)?; let body = reqwest::Body::from(body); let response = client @@ -379,26 +390,36 @@ async fn send_webhook( .body(body) .send() .await - .map_err(|e| WebhookError::RetryableRequestError { + .map_err(|e| WebhookRequestError::RetryableRequestError { error: e, + response: None, retry_after: None, })?; let retry_after = parse_retry_after_header(response.headers()); - match response.error_for_status() { - Ok(response) => Ok(response), + match response.error_for_status_ref() { + Ok(_) => Ok(response), Err(err) => { if is_retryable_status( err.status() .expect("status code is set as error is generated from a response"), ) { - Err(WebhookError::RetryableRequestError { - error: err, - retry_after, - }) + Err(WebhookError::Request( + WebhookRequestError::RetryableRequestError { + error: err, + // TODO: Make amount of bytes configurable. + response: first_n_bytes_of_response(response, 10 * 1024).await.ok(), + retry_after, + }, + )) } else { - Err(WebhookError::NonRetryableRetryableRequestError(err)) + Err(WebhookError::Request( + WebhookRequestError::NonRetryableRetryableRequestError { + error: err, + response: first_n_bytes_of_response(response, 10 * 1024).await.ok(), + }, + )) } } } @@ -574,7 +595,7 @@ mod tests { } #[sqlx::test(migrations = "../migrations")] - async fn test_send_webhook(_: PgPool) { + async fn test_send_webhook(_pg: PgPool) { let method = HttpMethod::POST; let url = "http://localhost:18081/echo"; let headers = collections::HashMap::new(); @@ -591,4 +612,58 @@ mod tests { body.to_owned(), ); } + + #[sqlx::test(migrations = "../migrations")] + async fn test_error_message_contains_response_body(_pg: PgPool) { + let method = HttpMethod::POST; + let url = "http://localhost:18081/fail"; + let headers = collections::HashMap::new(); + let body = "this is an error message"; + let client = reqwest::Client::new(); + + let err = send_webhook(client, &method, url, &headers, body.to_owned()) + .await + .err() + .expect("request didn't fail when it should have failed"); + + assert!(matches!(err, WebhookError::Request(..))); + if let WebhookError::Request(request_error) = err { + assert_eq!(request_error.status(), Some(StatusCode::BAD_REQUEST)); + assert!(request_error.to_string().contains(body)); + // This is the display implementation of reqwest. Just checking it is still there. + // See: https://github.com/seanmonstar/reqwest/blob/master/src/error.rs + assert!(request_error.to_string().contains( + "HTTP status client error (400 Bad Request) for url (http://localhost:18081/fail)" + )); + } + } + + #[sqlx::test(migrations = "../migrations")] + async fn test_error_message_contains_up_to_n_bytes_of_response_body(_pg: PgPool) { + let method = HttpMethod::POST; + let url = "http://localhost:18081/fail"; + let headers = collections::HashMap::new(); + // This is double the current hardcoded amount of bytes. + // TODO: Make this configurable and change it here too. + let body = (0..20 * 1024).map(|_| "a").collect::>().concat(); + let client = reqwest::Client::new(); + + let err = send_webhook(client, &method, url, &headers, body.to_owned()) + .await + .err() + .expect("request didn't fail when it should have failed"); + + assert!(matches!(err, WebhookError::Request(..))); + if let WebhookError::Request(request_error) = err { + assert_eq!(request_error.status(), Some(StatusCode::BAD_REQUEST)); + assert!(request_error.to_string().contains(&body[0..10 * 1024])); + // The 81 bytes account for the reqwest erorr message as described below. + assert_eq!(request_error.to_string().len(), 10 * 1024 + 81); + // This is the display implementation of reqwest. Just checking it is still there. + // See: https://github.com/seanmonstar/reqwest/blob/master/src/error.rs + assert!(request_error.to_string().contains( + "HTTP status client error (400 Bad Request) for url (http://localhost:18081/fail)" + )); + } + } } From e2ce4661867c75ba64682d6fc1d32f61c4beee34 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Fri, 3 May 2024 11:03:44 +0200 Subject: [PATCH 232/249] CI: upgrade to Rust 1.77.2 and Debian bookworm (#34) --- .github/workflows/rust.yml | 13 ++++--------- Dockerfile | 4 ++-- hook-worker/src/dns.rs | 0 3 files changed, 6 insertions(+), 11 deletions(-) create mode 100644 hook-worker/src/dns.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a525f3bf942c1..4c3a3d5241f77 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -17,9 +17,7 @@ jobs: - uses: actions/checkout@v3 - name: Install rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable + uses: dtolnay/rust-toolchain@1.77 - uses: actions/cache@v3 with: @@ -52,9 +50,7 @@ jobs: echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts - name: Install rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable + uses: dtolnay/rust-toolchain@1.77 - uses: actions/cache@v3 with: @@ -73,10 +69,9 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Install latest rust - uses: dtolnay/rust-toolchain@master + - name: Install rust + uses: dtolnay/rust-toolchain@1.77 with: - toolchain: stable components: clippy,rustfmt - uses: actions/cache@v3 diff --git a/Dockerfile b/Dockerfile index 67aea7f210f44..a6c59b11a0e33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.74.0-buster AS chef +FROM docker.io/lukemathwalker/cargo-chef:latest-rust-1.77-bookworm AS chef ARG BIN WORKDIR /app @@ -20,7 +20,7 @@ RUN cargo chef cook --release --recipe-path recipe.json COPY . . RUN cargo build --release --bin $BIN -FROM debian:bullseye-20230320-slim AS runtime +FROM debian:bookworm-slim AS runtime RUN apt-get update && \ apt-get install -y --no-install-recommends \ diff --git a/hook-worker/src/dns.rs b/hook-worker/src/dns.rs new file mode 100644 index 0000000000000..e69de29bb2d1d From cf9b82d97d6a453506560f96a625f4758bd6223a Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 3 May 2024 11:12:28 -0600 Subject: [PATCH 233/249] Bump webhook max size to 5MB (#38) --- hook-api/src/handlers/app.rs | 9 +++++++-- hook-api/src/handlers/webhook.rs | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/hook-api/src/handlers/app.rs b/hook-api/src/handlers/app.rs index f8d4b24695b99..fa2bcbc30947a 100644 --- a/hook-api/src/handlers/app.rs +++ b/hook-api/src/handlers/app.rs @@ -1,4 +1,4 @@ -use axum::{routing, Router}; +use axum::{extract::DefaultBodyLimit, routing, Router}; use hook_common::pgqueue::PgQueue; @@ -9,7 +9,12 @@ pub fn add_routes(router: Router, pg_pool: PgQueue) -> Router { .route("/", routing::get(index)) .route("/_readiness", routing::get(index)) .route("/_liveness", routing::get(index)) // No async loop for now, just check axum health - .route("/webhook", routing::post(webhook::post).with_state(pg_pool)) + .route( + "/webhook", + routing::post(webhook::post) + .with_state(pg_pool) + .layer(DefaultBodyLimit::disable()), + ) } pub async fn index() -> &'static str { diff --git a/hook-api/src/handlers/webhook.rs b/hook-api/src/handlers/webhook.rs index e50b8b0241a51..47f21a6638b36 100644 --- a/hook-api/src/handlers/webhook.rs +++ b/hook-api/src/handlers/webhook.rs @@ -9,7 +9,7 @@ use hook_common::pgqueue::{NewJob, PgQueue}; use serde::Serialize; use tracing::{debug, error}; -const MAX_BODY_SIZE: usize = 1_000_000; +pub const MAX_BODY_SIZE: usize = 5_000_000; #[derive(Serialize, Deserialize)] pub struct WebhookPostResponse { @@ -252,7 +252,7 @@ mod tests { let app = add_routes(Router::new(), pg_queue); - let bytes: Vec = vec![b'a'; 1_000_000 * 2]; + let bytes: Vec = vec![b'a'; 5_000_000 * 2]; let long_string = String::from_utf8_lossy(&bytes); let response = app From ee769d9b0a7cfc073c0338be149ba01e67080b15 Mon Sep 17 00:00:00 2001 From: Brett Hoerner Date: Fri, 3 May 2024 13:53:46 -0600 Subject: [PATCH 234/249] Make max_body_size configurable via env, drop custom check, use 413 (#39) --- Cargo.lock | 1 + Cargo.toml | 4 ++-- hook-api/Cargo.toml | 1 + hook-api/src/config.rs | 3 +++ hook-api/src/handlers/app.rs | 9 +++++---- hook-api/src/handlers/webhook.rs | 27 +++++++++------------------ hook-api/src/main.rs | 2 +- 7 files changed, 22 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cc582ee6696c..c56738f84c3f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1076,6 +1076,7 @@ dependencies = [ "sqlx", "tokio", "tower", + "tower-http", "tracing", "tracing-subscriber", "url", diff --git a/Cargo.toml b/Cargo.toml index d1f75274aa629..e097eab2f7339 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ [workspace.lints.rust] # See https://doc.rust-lang.org/stable/rustc/lints/listing/allowed-by-default.html -unsafe_code = "forbid" # forbid cannot be ignored with an annotation +unsafe_code = "forbid" # forbid cannot be ignored with an annotation unstable_features = "forbid" macro_use_extern_crate = "forbid" let_underscore_drop = "deny" @@ -69,7 +69,7 @@ time = { version = "0.3.20", features = [ thiserror = { version = "1.0" } tokio = { version = "1.34.0", features = ["full"] } tower = "0.4.13" -tower-http = { version = "0.5.2", features = ["cors", "trace"] } +tower-http = { version = "0.5.2", features = ["cors", "limit", "trace"] } tracing = "0.1.40" tracing-subscriber = "0.3.18" url = { version = "2.5.0 " } diff --git a/hook-api/Cargo.toml b/hook-api/Cargo.toml index c3528d23da5d2..eb82438c47d65 100644 --- a/hook-api/Cargo.toml +++ b/hook-api/Cargo.toml @@ -19,6 +19,7 @@ serde_json = { workspace = true } sqlx = { workspace = true } tokio = { workspace = true } tower = { workspace = true } +tower-http = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } url = { workspace = true } diff --git a/hook-api/src/config.rs b/hook-api/src/config.rs index 55fa404e5149d..e15f0d3fac77a 100644 --- a/hook-api/src/config.rs +++ b/hook-api/src/config.rs @@ -16,6 +16,9 @@ pub struct Config { #[envconfig(default = "100")] pub max_pg_connections: u32, + + #[envconfig(default = "5000000")] + pub max_body_size: usize, } impl Config { diff --git a/hook-api/src/handlers/app.rs b/hook-api/src/handlers/app.rs index fa2bcbc30947a..7cbbc449e424d 100644 --- a/hook-api/src/handlers/app.rs +++ b/hook-api/src/handlers/app.rs @@ -1,10 +1,11 @@ -use axum::{extract::DefaultBodyLimit, routing, Router}; +use axum::{routing, Router}; +use tower_http::limit::RequestBodyLimitLayer; use hook_common::pgqueue::PgQueue; use super::webhook; -pub fn add_routes(router: Router, pg_pool: PgQueue) -> Router { +pub fn add_routes(router: Router, pg_pool: PgQueue, max_body_size: usize) -> Router { router .route("/", routing::get(index)) .route("/_readiness", routing::get(index)) @@ -13,7 +14,7 @@ pub fn add_routes(router: Router, pg_pool: PgQueue) -> Router { "/webhook", routing::post(webhook::post) .with_state(pg_pool) - .layer(DefaultBodyLimit::disable()), + .layer(RequestBodyLimitLayer::new(max_body_size)), ) } @@ -37,7 +38,7 @@ mod tests { async fn index(db: PgPool) { let pg_queue = PgQueue::new_from_pool("test_index", db).await; - let app = add_routes(Router::new(), pg_queue); + let app = add_routes(Router::new(), pg_queue, 1_000_000); let response = app .oneshot(Request::builder().uri("/").body(Body::empty()).unwrap()) diff --git a/hook-api/src/handlers/webhook.rs b/hook-api/src/handlers/webhook.rs index 47f21a6638b36..808c94878291b 100644 --- a/hook-api/src/handlers/webhook.rs +++ b/hook-api/src/handlers/webhook.rs @@ -9,8 +9,6 @@ use hook_common::pgqueue::{NewJob, PgQueue}; use serde::Serialize; use tracing::{debug, error}; -pub const MAX_BODY_SIZE: usize = 5_000_000; - #[derive(Serialize, Deserialize)] pub struct WebhookPostResponse { #[serde(skip_serializing_if = "Option::is_none")] @@ -37,15 +35,6 @@ pub async fn post( ) -> Result, (StatusCode, Json)> { debug!("received payload: {:?}", payload); - if payload.parameters.body.len() > MAX_BODY_SIZE { - return Err(( - StatusCode::BAD_REQUEST, - Json(WebhookPostResponse { - error: Some("body too large".to_owned()), - }), - )); - } - let url_hostname = get_hostname(&payload.parameters.url)?; // We could cast to i32, but this ensures we are not wrapping. let max_attempts = i32::try_from(payload.max_attempts).map_err(|_| { @@ -125,11 +114,13 @@ mod tests { use crate::handlers::app::add_routes; + const MAX_BODY_SIZE: usize = 1_000_000; + #[sqlx::test(migrations = "../migrations")] async fn webhook_success(db: PgPool) { let pg_queue = PgQueue::new_from_pool("test_index", db).await; - let app = add_routes(Router::new(), pg_queue); + let app = add_routes(Router::new(), pg_queue, MAX_BODY_SIZE); let mut headers = collections::HashMap::new(); headers.insert("Content-Type".to_owned(), "application/json".to_owned()); @@ -171,7 +162,7 @@ mod tests { async fn webhook_bad_url(db: PgPool) { let pg_queue = PgQueue::new_from_pool("test_index", db).await; - let app = add_routes(Router::new(), pg_queue); + let app = add_routes(Router::new(), pg_queue, MAX_BODY_SIZE); let response = app .oneshot( @@ -208,7 +199,7 @@ mod tests { async fn webhook_payload_missing_fields(db: PgPool) { let pg_queue = PgQueue::new_from_pool("test_index", db).await; - let app = add_routes(Router::new(), pg_queue); + let app = add_routes(Router::new(), pg_queue, MAX_BODY_SIZE); let response = app .oneshot( @@ -229,7 +220,7 @@ mod tests { async fn webhook_payload_not_json(db: PgPool) { let pg_queue = PgQueue::new_from_pool("test_index", db).await; - let app = add_routes(Router::new(), pg_queue); + let app = add_routes(Router::new(), pg_queue, MAX_BODY_SIZE); let response = app .oneshot( @@ -250,9 +241,9 @@ mod tests { async fn webhook_payload_body_too_large(db: PgPool) { let pg_queue = PgQueue::new_from_pool("test_index", db).await; - let app = add_routes(Router::new(), pg_queue); + let app = add_routes(Router::new(), pg_queue, MAX_BODY_SIZE); - let bytes: Vec = vec![b'a'; 5_000_000 * 2]; + let bytes: Vec = vec![b'a'; MAX_BODY_SIZE + 1]; let long_string = String::from_utf8_lossy(&bytes); let response = app @@ -283,6 +274,6 @@ mod tests { .await .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE); } } diff --git a/hook-api/src/main.rs b/hook-api/src/main.rs index 9a9a9fd41c0c2..ad05edef1ff98 100644 --- a/hook-api/src/main.rs +++ b/hook-api/src/main.rs @@ -34,7 +34,7 @@ async fn main() { .await .expect("failed to initialize queue"); - let app = handlers::add_routes(Router::new(), pg_queue); + let app = handlers::add_routes(Router::new(), pg_queue, config.max_body_size); let app = setup_metrics_routes(app); match listen(app, config.bind()).await { From 7715f46ed15cb992dc6eb4d1032d87b0c29ac90b Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 6 May 2024 09:25:18 +0200 Subject: [PATCH 235/249] chore: merge capture & capture-server crates (#36) --- .github/workflows/docker-capture.yml | 2 +- Cargo.lock | 29 +++++---------------- Cargo.toml | 8 ++++-- capture-server/Cargo.toml | 28 -------------------- capture/Cargo.toml | 12 +++++++++ {capture-server => capture}/src/main.rs | 0 {capture-server => capture}/tests/common.rs | 0 {capture-server => capture}/tests/events.rs | 0 8 files changed, 26 insertions(+), 53 deletions(-) delete mode 100644 capture-server/Cargo.toml rename {capture-server => capture}/src/main.rs (100%) rename {capture-server => capture}/tests/common.rs (100%) rename {capture-server => capture}/tests/events.rs (100%) diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-capture.yml index d0efac36b0852..d31e05dcef5ac 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-capture.yml @@ -69,7 +69,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: BIN=capture-server + build-args: BIN=capture - name: Capture image digest run: echo ${{ steps.docker_build_capture.outputs.digest }} diff --git a/Cargo.lock b/Cargo.lock index c56738f84c3f2..5caeea4c0131f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,13 +370,19 @@ dependencies = [ "bytes", "envconfig", "flate2", + "futures", "governor", "health", "metrics", "metrics-exporter-prometheus", + "once_cell", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rand", "rdkafka", "redis", + "reqwest 0.12.3", "serde", "serde_json", "serde_urlencoded", @@ -385,30 +391,9 @@ dependencies = [ "tokio", "tower-http", "tracing", - "uuid", -] - -[[package]] -name = "capture-server" -version = "0.1.0" -dependencies = [ - "anyhow", - "assert-json-diff", - "capture", - "envconfig", - "futures", - "once_cell", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "rand", - "rdkafka", - "reqwest 0.12.3", - "serde_json", - "tokio", - "tracing", "tracing-opentelemetry", "tracing-subscriber", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e097eab2f7339..180355b5ba49a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ resolver = "2" members = [ "capture", - "capture-server", "common/health", "hook-api", "hook-common", @@ -44,6 +43,10 @@ http = { version = "1.1.0" } http-body-util = "0.1.0" metrics = "0.22.0" metrics-exporter-prometheus = "0.14.0" +once_cell = "1.18.0" +opentelemetry = { version = "0.22.0", features = ["trace"]} +opentelemetry-otlp = "0.15.0" +opentelemetry_sdk = { version = "0.22.1", features = ["trace", "rt-tokio"] } rand = "0.8.5" rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl", "tracing"] } reqwest = { version = "0.12.3", features = ["stream"] } @@ -71,6 +74,7 @@ tokio = { version = "1.34.0", features = ["full"] } tower = "0.4.13" tower-http = { version = "0.5.2", features = ["cors", "limit", "trace"] } tracing = "0.1.40" -tracing-subscriber = "0.3.18" +tracing-opentelemetry = "0.23.0" +tracing-subscriber = { version="0.3.18", features = ["env-filter"] } url = { version = "2.5.0 " } uuid = { version = "1.6.1", features = ["v7", "serde"] } diff --git a/capture-server/Cargo.toml b/capture-server/Cargo.toml deleted file mode 100644 index 39ee742d2b048..0000000000000 --- a/capture-server/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "capture-server" -version = "0.1.0" -edition = "2021" - -[lints] -workspace = true - -[dependencies] -capture = { path = "../capture" } -envconfig = { workspace = true } -opentelemetry = { version = "0.22.0", features = ["trace"]} -opentelemetry-otlp = "0.15.0" -opentelemetry_sdk = { version = "0.22.1", features = ["trace", "rt-tokio"] } -tokio = { workspace = true } -tracing = { workspace = true } -tracing-opentelemetry = "0.23.0" -tracing-subscriber = { workspace = true, features = ["env-filter"] } - -[dev-dependencies] -anyhow = { workspace = true, features = [] } -assert-json-diff = { workspace = true } -futures = "0.3.29" -once_cell = "1.18.0" -rand = { workspace = true } -rdkafka = { workspace = true } -reqwest = { workspace = true } -serde_json = { workspace = true } diff --git a/capture/Cargo.toml b/capture/Cargo.toml index ae5ad9a3bd127..97d310f03d662 100644 --- a/capture/Cargo.toml +++ b/capture/Cargo.toml @@ -19,6 +19,9 @@ governor = { workspace = true } health = { path = "../common/health" } metrics = { workspace = true } metrics-exporter-prometheus = { workspace = true } +opentelemetry = { workspace = true } +opentelemetry-otlp = { workspace = true } +opentelemetry_sdk = { workspace = true } rand = { workspace = true } rdkafka = { workspace = true } redis = { version = "0.23.3", features = [ @@ -34,8 +37,17 @@ time = { workspace = true } tokio = { workspace = true } tower-http = { workspace = true } tracing = { workspace = true } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true } uuid = { workspace = true } [dev-dependencies] assert-json-diff = { workspace = true } axum-test-helper = { git = "https://github.com/posthog/axum-test-helper.git" } # TODO: remove, directly use reqwest like capture-server tests +anyhow = { workspace = true } +futures = { workspace = true } +once_cell = { workspace = true } +rand = { workspace = true } +rdkafka = { workspace = true } +reqwest = { workspace = true } +serde_json = { workspace = true } diff --git a/capture-server/src/main.rs b/capture/src/main.rs similarity index 100% rename from capture-server/src/main.rs rename to capture/src/main.rs diff --git a/capture-server/tests/common.rs b/capture/tests/common.rs similarity index 100% rename from capture-server/tests/common.rs rename to capture/tests/common.rs diff --git a/capture-server/tests/events.rs b/capture/tests/events.rs similarity index 100% rename from capture-server/tests/events.rs rename to capture/tests/events.rs From e8343d9ad1642f395a87925f6e9ad415d003e791 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 6 May 2024 10:35:37 +0200 Subject: [PATCH 236/249] ci: refactor docker build workflows for all rust crates (#37) --- .../{docker-capture.yml => docker-build.yml} | 26 ++++--- .github/workflows/docker-hook-api.yml | 75 ------------------- .github/workflows/docker-hook-janitor.yml | 75 ------------------- .github/workflows/docker-hook-worker.yml | 75 ------------------- 4 files changed, 15 insertions(+), 236 deletions(-) rename .github/workflows/{docker-capture.yml => docker-build.yml} (77%) delete mode 100644 .github/workflows/docker-hook-api.yml delete mode 100644 .github/workflows/docker-hook-janitor.yml delete mode 100644 .github/workflows/docker-hook-worker.yml diff --git a/.github/workflows/docker-capture.yml b/.github/workflows/docker-build.yml similarity index 77% rename from .github/workflows/docker-capture.yml rename to .github/workflows/docker-build.yml index d31e05dcef5ac..3132a61b0be03 100644 --- a/.github/workflows/docker-capture.yml +++ b/.github/workflows/docker-build.yml @@ -1,4 +1,4 @@ -name: Build capture docker image +name: Build container images on: workflow_dispatch: @@ -6,12 +6,16 @@ on: branches: - "main" -permissions: - packages: write - jobs: build: - name: build and publish capture image + name: Build and publish container image + strategy: + matrix: + image: + - capture + - hook-api + - hook-janitor + - hook-worker runs-on: depot-ubuntu-22.04-4 permissions: id-token: write # allow issuing OIDC tokens for this workflow run @@ -45,7 +49,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hog-rs/capture + images: ghcr.io/posthog/hog-rs/${{ matrix.image }} tags: | type=ref,event=pr type=ref,event=branch @@ -57,8 +61,8 @@ jobs: id: buildx uses: docker/setup-buildx-action@v2 - - name: Build and push capture - id: docker_build_capture + - name: Build and push image + id: docker_build uses: depot/build-push-action@v1 with: context: ./ @@ -69,7 +73,7 @@ jobs: platforms: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max - build-args: BIN=capture + build-args: BIN=${{ matrix.image }} - - name: Capture image digest - run: echo ${{ steps.docker_build_capture.outputs.digest }} + - name: Container image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/.github/workflows/docker-hook-api.yml b/.github/workflows/docker-hook-api.yml deleted file mode 100644 index e6f83f1b6b3fb..0000000000000 --- a/.github/workflows/docker-hook-api.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build hook-api docker image - -on: - workflow_dispatch: - push: - branches: - - "main" - -permissions: - packages: write - -jobs: - build: - name: build and publish hook-api image - runs-on: depot-ubuntu-22.04-4 - permissions: - id-token: write # allow issuing OIDC tokens for this workflow run - contents: read # allow reading the repo contents - packages: write # allow push to ghcr.io - - steps: - - name: Check Out Repo - uses: actions/checkout@v3 - - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: posthog - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: ghcr.io/posthog/hog-rs/hook-api - tags: | - type=ref,event=pr - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - - name: Build and push api - id: docker_build_hook_api - uses: depot/build-push-action@v1 - with: - context: ./ - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: BIN=hook-api - - - name: Hook-api image digest - run: echo ${{ steps.docker_build_hook_api.outputs.digest }} diff --git a/.github/workflows/docker-hook-janitor.yml b/.github/workflows/docker-hook-janitor.yml deleted file mode 100644 index 33706d0987637..0000000000000 --- a/.github/workflows/docker-hook-janitor.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build hook-janitor docker image - -on: - workflow_dispatch: - push: - branches: - - "main" - -permissions: - packages: write - -jobs: - build: - name: build and publish hook-janitor image - runs-on: depot-ubuntu-22.04-4 - permissions: - id-token: write # allow issuing OIDC tokens for this workflow run - contents: read # allow reading the repo contents - packages: write # allow push to ghcr.io - - steps: - - name: Check Out Repo - uses: actions/checkout@v3 - - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: posthog - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: ghcr.io/posthog/hog-rs/hook-janitor - tags: | - type=ref,event=pr - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - - name: Build and push janitor - id: docker_build_hook_janitor - uses: depot/build-push-action@v1 - with: - context: ./ - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: BIN=hook-janitor - - - name: Hook-janitor image digest - run: echo ${{ steps.docker_build_hook_janitor.outputs.digest }} diff --git a/.github/workflows/docker-hook-worker.yml b/.github/workflows/docker-hook-worker.yml deleted file mode 100644 index dc5ca53abef88..0000000000000 --- a/.github/workflows/docker-hook-worker.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Build hook-worker docker image - -on: - workflow_dispatch: - push: - branches: - - "main" - -permissions: - packages: write - -jobs: - build: - name: build and publish hook-worker image - runs-on: depot-ubuntu-22.04-4 - permissions: - id-token: write # allow issuing OIDC tokens for this workflow run - contents: read # allow reading the repo contents - packages: write # allow push to ghcr.io - - steps: - - name: Check Out Repo - uses: actions/checkout@v3 - - - name: Set up Depot CLI - uses: depot/setup-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: posthog - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to ghcr.io - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: ghcr.io/posthog/hog-rs/hook-worker - tags: | - type=ref,event=pr - type=ref,event=branch - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v2 - - - name: Build and push worker - id: docker_build_hook_worker - uses: depot/build-push-action@v1 - with: - context: ./ - file: ./Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/arm64 - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: BIN=hook-worker - - - name: Hook-worker image digest - run: echo ${{ steps.docker_build_hook_worker.outputs.digest }} From 281af615b4874da1b89915a6ccd36d74be5a04a0 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 6 May 2024 16:31:25 +0200 Subject: [PATCH 237/249] hook-worker: deny traffic to internal IPs and IPv6 (#35) --- hook-worker/src/config.rs | 3 + hook-worker/src/dns.rs | 140 ++++++++++++++++++++++++++++++++++++++ hook-worker/src/error.rs | 20 +++++- hook-worker/src/lib.rs | 1 + hook-worker/src/main.rs | 1 + hook-worker/src/worker.rs | 113 ++++++++++++++++++++++-------- 6 files changed, 248 insertions(+), 30 deletions(-) diff --git a/hook-worker/src/config.rs b/hook-worker/src/config.rs index ceb690f38846e..51b23b7f273c5 100644 --- a/hook-worker/src/config.rs +++ b/hook-worker/src/config.rs @@ -37,6 +37,9 @@ pub struct Config { #[envconfig(default = "1")] pub dequeue_batch_size: u32, + + #[envconfig(default = "false")] + pub allow_internal_ips: bool, } impl Config { diff --git a/hook-worker/src/dns.rs b/hook-worker/src/dns.rs index e69de29bb2d1d..36fd7a005398e 100644 --- a/hook-worker/src/dns.rs +++ b/hook-worker/src/dns.rs @@ -0,0 +1,140 @@ +use std::error::Error as StdError; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::{fmt, io}; + +use futures::FutureExt; +use reqwest::dns::{Addrs, Name, Resolve, Resolving}; +use tokio::task::spawn_blocking; + +pub struct NoPublicIPv4Error; + +impl std::error::Error for NoPublicIPv4Error {} +impl fmt::Display for NoPublicIPv4Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "No public IPv4 found for specified host") + } +} +impl fmt::Debug for NoPublicIPv4Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "No public IPv4 found for specified host") + } +} + +/// Internal reqwest type, copied here as part of Resolving +pub(crate) type BoxError = Box; + +/// Returns [`true`] if the address appears to be a globally reachable IPv4. +/// +/// Trimmed down version of the unstable IpAddr::is_global, move to it when it's stable. +fn is_global_ipv4(addr: &SocketAddr) -> bool { + match addr.ip() { + IpAddr::V4(ip) => { + !(ip.octets()[0] == 0 // "This network" + || ip.is_private() + || ip.is_loopback() + || ip.is_link_local() + || ip.is_broadcast()) + } + IpAddr::V6(_) => false, // Our network does not currently support ipv6, let's ignore for now + } +} + +/// DNS resolver using the stdlib resolver, but filtering results to only pass public IPv4 results. +/// +/// Private and broadcast addresses are filtered out, so are IPv6 results for now (as our infra +/// does not currently support IPv6 routing anyway). +/// This is adapted from the GaiResolver in hyper and reqwest. +pub struct PublicIPv4Resolver {} + +impl Resolve for PublicIPv4Resolver { + fn resolve(&self, name: Name) -> Resolving { + // Closure to call the system's resolver (blocking call) through the ToSocketAddrs trait. + let resolve_host = move || (name.as_str(), 0).to_socket_addrs(); + + // Execute the blocking call in a separate worker thread then process its result asynchronously. + // spawn_blocking returns a JoinHandle that implements Future>. + let future_result = spawn_blocking(resolve_host).map(|result| match result { + Ok(Ok(all_addrs)) => { + // Resolution succeeded, filter the results + let filtered_addr: Vec = all_addrs.filter(is_global_ipv4).collect(); + if filtered_addr.is_empty() { + // No public IPs found, error out with PermissionDenied + let err: BoxError = Box::new(NoPublicIPv4Error); + Err(err) + } else { + // Pass remaining IPs in a boxed iterator for request to use. + let addrs: Addrs = Box::new(filtered_addr.into_iter()); + Ok(addrs) + } + } + Ok(Err(err)) => { + // Resolution failed, pass error through in a Box + let err: BoxError = Box::new(err); + Err(err) + } + Err(join_err) => { + // The tokio task failed, pass as io::Error in a Box + let err: BoxError = Box::new(io::Error::from(join_err)); + Err(err) + } + }); + + // Box the Future to satisfy the Resolving interface. + Box::pin(future_result) + } +} + +#[cfg(test)] +mod tests { + use crate::dns::{NoPublicIPv4Error, PublicIPv4Resolver}; + use reqwest::dns::{Name, Resolve}; + use std::str::FromStr; + + #[tokio::test] + async fn it_resolves_google_com() { + let resolver: PublicIPv4Resolver = PublicIPv4Resolver {}; + let addrs = resolver + .resolve(Name::from_str("google.com").unwrap()) + .await + .expect("lookup has failed"); + assert!(addrs.count() > 0, "empty address list") + } + + #[tokio::test] + async fn it_denies_ipv6_google_com() { + let resolver: PublicIPv4Resolver = PublicIPv4Resolver {}; + match resolver + .resolve(Name::from_str("ipv6.google.com").unwrap()) + .await + { + Ok(_) => panic!("should have failed"), + Err(err) => assert!(err.is::()), + } + } + + #[tokio::test] + async fn it_denies_localhost() { + let resolver: PublicIPv4Resolver = PublicIPv4Resolver {}; + match resolver.resolve(Name::from_str("localhost").unwrap()).await { + Ok(_) => panic!("should have failed"), + Err(err) => assert!(err.is::()), + } + } + + #[tokio::test] + async fn it_bubbles_up_resolution_error() { + let resolver: PublicIPv4Resolver = PublicIPv4Resolver {}; + match resolver + .resolve(Name::from_str("invalid.domain.unknown").unwrap()) + .await + { + Ok(_) => panic!("should have failed"), + Err(err) => { + assert!(!err.is::()); + assert!(err + .to_string() + .contains("failed to lookup address information")) + } + } + } +} diff --git a/hook-worker/src/error.rs b/hook-worker/src/error.rs index 48468bc65f544..764e8d973499c 100644 --- a/hook-worker/src/error.rs +++ b/hook-worker/src/error.rs @@ -1,6 +1,8 @@ +use std::error::Error; use std::fmt; use std::time; +use crate::dns::NoPublicIPv4Error; use hook_common::{pgqueue, webhook::WebhookJobError}; use thiserror::Error; @@ -64,7 +66,11 @@ impl fmt::Display for WebhookRequestError { Some(m) => m.to_string(), None => "No response from the server".to_string(), }; - writeln!(f, "{}", error)?; + if is_error_source::(error) { + writeln!(f, "{}: {}", error, NoPublicIPv4Error)?; + } else { + writeln!(f, "{}", error)?; + } write!(f, "{}", response_message)?; Ok(()) @@ -132,3 +138,15 @@ pub enum WorkerError { #[error("timed out while waiting for jobs to be available")] TimeoutError, } + +/// Check the error and it's sources (recursively) to return true if an error of the given type is found. +/// TODO: use Error::sources() when stable +pub fn is_error_source(err: &(dyn std::error::Error + 'static)) -> bool { + if err.is::() { + return true; + } + match err.source() { + None => false, + Some(source) => is_error_source::(source), + } +} diff --git a/hook-worker/src/lib.rs b/hook-worker/src/lib.rs index 8488d15b20a36..94a07584f1da5 100644 --- a/hook-worker/src/lib.rs +++ b/hook-worker/src/lib.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod dns; pub mod error; pub mod util; pub mod worker; diff --git a/hook-worker/src/main.rs b/hook-worker/src/main.rs index 8a6eeb37435ab..050e2b947c780 100644 --- a/hook-worker/src/main.rs +++ b/hook-worker/src/main.rs @@ -52,6 +52,7 @@ async fn main() -> Result<(), WorkerError> { config.request_timeout.0, config.max_concurrent_jobs, retry_policy_builder.provide(), + config.allow_internal_ips, worker_liveness, ); diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index 824f1e2f23a87..fdb405a2fd18b 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -14,11 +14,14 @@ use hook_common::{ webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}, }; use http::StatusCode; -use reqwest::header; +use reqwest::{header, Client}; use tokio::sync; use tracing::error; -use crate::error::{WebhookError, WebhookParseError, WebhookRequestError, WorkerError}; +use crate::dns::{NoPublicIPv4Error, PublicIPv4Resolver}; +use crate::error::{ + is_error_source, WebhookError, WebhookParseError, WebhookRequestError, WorkerError, +}; use crate::util::first_n_bytes_of_response; /// A WebhookJob is any `PgQueueJob` with `WebhookJobParameters` and `WebhookJobMetadata`. @@ -74,6 +77,25 @@ pub struct WebhookWorker<'p> { liveness: HealthHandle, } +pub fn build_http_client( + request_timeout: time::Duration, + allow_internal_ips: bool, +) -> reqwest::Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + let mut client_builder = reqwest::Client::builder() + .default_headers(headers) + .user_agent("PostHog Webhook Worker") + .timeout(request_timeout); + if !allow_internal_ips { + client_builder = client_builder.dns_resolver(Arc::new(PublicIPv4Resolver {})) + } + client_builder.build() +} + impl<'p> WebhookWorker<'p> { #[allow(clippy::too_many_arguments)] pub fn new( @@ -84,19 +106,10 @@ impl<'p> WebhookWorker<'p> { request_timeout: time::Duration, max_concurrent_jobs: usize, retry_policy: RetryPolicy, + allow_internal_ips: bool, liveness: HealthHandle, ) -> Self { - let mut headers = header::HeaderMap::new(); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - ); - - let client = reqwest::Client::builder() - .default_headers(headers) - .user_agent("PostHog Webhook Worker") - .timeout(request_timeout) - .build() + let client = build_http_client(request_timeout, allow_internal_ips) .expect("failed to construct reqwest client for webhook worker"); Self { @@ -390,10 +403,19 @@ async fn send_webhook( .body(body) .send() .await - .map_err(|e| WebhookRequestError::RetryableRequestError { - error: e, - response: None, - retry_after: None, + .map_err(|e| { + if is_error_source::(&e) { + WebhookRequestError::NonRetryableRetryableRequestError { + error: e, + response: None, + } + } else { + WebhookRequestError::RetryableRequestError { + error: e, + response: None, + retry_after: None, + } + } })?; let retry_after = parse_retry_after_header(response.headers()); @@ -469,6 +491,7 @@ fn parse_retry_after_header(header_map: &reqwest::header::HeaderMap) -> Option Client { + build_http_client(Duration::from_secs(1), true).expect("failed to create client") + } + #[allow(dead_code)] async fn enqueue_job( queue: &PgQueue, @@ -569,6 +598,7 @@ mod tests { time::Duration::from_millis(5000), 10, RetryPolicy::default(), + false, liveness, ); @@ -594,15 +624,14 @@ mod tests { assert!(registry.get_status().healthy) } - #[sqlx::test(migrations = "../migrations")] - async fn test_send_webhook(_pg: PgPool) { + #[tokio::test] + async fn test_send_webhook() { let method = HttpMethod::POST; let url = "http://localhost:18081/echo"; let headers = collections::HashMap::new(); let body = "a very relevant request body"; - let client = reqwest::Client::new(); - let response = send_webhook(client, &method, url, &headers, body.to_owned()) + let response = send_webhook(localhost_client(), &method, url, &headers, body.to_owned()) .await .expect("send_webhook failed"); @@ -613,15 +642,14 @@ mod tests { ); } - #[sqlx::test(migrations = "../migrations")] - async fn test_error_message_contains_response_body(_pg: PgPool) { + #[tokio::test] + async fn test_error_message_contains_response_body() { let method = HttpMethod::POST; let url = "http://localhost:18081/fail"; let headers = collections::HashMap::new(); let body = "this is an error message"; - let client = reqwest::Client::new(); - let err = send_webhook(client, &method, url, &headers, body.to_owned()) + let err = send_webhook(localhost_client(), &method, url, &headers, body.to_owned()) .await .err() .expect("request didn't fail when it should have failed"); @@ -638,17 +666,16 @@ mod tests { } } - #[sqlx::test(migrations = "../migrations")] - async fn test_error_message_contains_up_to_n_bytes_of_response_body(_pg: PgPool) { + #[tokio::test] + async fn test_error_message_contains_up_to_n_bytes_of_response_body() { let method = HttpMethod::POST; let url = "http://localhost:18081/fail"; let headers = collections::HashMap::new(); // This is double the current hardcoded amount of bytes. // TODO: Make this configurable and change it here too. let body = (0..20 * 1024).map(|_| "a").collect::>().concat(); - let client = reqwest::Client::new(); - let err = send_webhook(client, &method, url, &headers, body.to_owned()) + let err = send_webhook(localhost_client(), &method, url, &headers, body.to_owned()) .await .err() .expect("request didn't fail when it should have failed"); @@ -666,4 +693,32 @@ mod tests { )); } } + + #[tokio::test] + async fn test_private_ips_denied() { + let method = HttpMethod::POST; + let url = "http://localhost:18081/echo"; + let headers = collections::HashMap::new(); + let body = "a very relevant request body"; + let filtering_client = + build_http_client(Duration::from_secs(1), false).expect("failed to create client"); + + let err = send_webhook(filtering_client, &method, url, &headers, body.to_owned()) + .await + .err() + .expect("request didn't fail when it should have failed"); + + assert!(matches!(err, WebhookError::Request(..))); + if let WebhookError::Request(request_error) = err { + assert_eq!(request_error.status(), None); + assert!(request_error + .to_string() + .contains("No public IPv4 found for specified host")); + if let WebhookRequestError::RetryableRequestError { .. } = request_error { + panic!("error should not be retryable") + } + } else { + panic!("unexpected error type {:?}", err) + } + } } From ae707cb2ed4a3d4c97df9f399189579b521f3973 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 6 May 2024 17:25:27 +0200 Subject: [PATCH 238/249] chore: cleanup unnecessary clippy allows (#40) --- hook-common/src/kafka_messages/plugin_logs.rs | 2 -- hook-worker/src/worker.rs | 13 +++---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/hook-common/src/kafka_messages/plugin_logs.rs b/hook-common/src/kafka_messages/plugin_logs.rs index 5a852e6aa3221..039788afe2dc5 100644 --- a/hook-common/src/kafka_messages/plugin_logs.rs +++ b/hook-common/src/kafka_messages/plugin_logs.rs @@ -4,7 +4,6 @@ use uuid::Uuid; use super::serialize_datetime; -#[allow(dead_code)] #[derive(Serialize)] pub enum PluginLogEntrySource { System, @@ -12,7 +11,6 @@ pub enum PluginLogEntrySource { Console, } -#[allow(dead_code)] #[derive(Serialize)] pub enum PluginLogEntryType { Debug, diff --git a/hook-worker/src/worker.rs b/hook-worker/src/worker.rs index fdb405a2fd18b..9dcc4a2f4b7b0 100644 --- a/hook-worker/src/worker.rs +++ b/hook-worker/src/worker.rs @@ -7,9 +7,7 @@ use futures::future::join_all; use health::HealthHandle; use hook_common::pgqueue::PgTransactionBatch; use hook_common::{ - pgqueue::{ - DatabaseError, Job, PgQueue, PgQueueJob, PgTransactionJob, RetryError, RetryInvalidError, - }, + pgqueue::{Job, PgQueue, PgQueueJob, PgTransactionJob, RetryError, RetryInvalidError}, retry::RetryPolicy, webhook::{HttpMethod, WebhookJobError, WebhookJobMetadata, WebhookJobParameters}, }; @@ -489,32 +487,27 @@ fn parse_retry_after_header(header_map: &reqwest::header::HeaderMap) -> Option String { std::process::id().to_string() } /// Get a request client or panic - #[allow(dead_code)] fn localhost_client() -> Client { build_http_client(Duration::from_secs(1), true).expect("failed to create client") } - #[allow(dead_code)] async fn enqueue_job( queue: &PgQueue, max_attempts: i32, From 871441b400d3af766a5a012507dde6c1aaf45c7f Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Tue, 7 May 2024 21:13:01 +0100 Subject: [PATCH 239/249] feat(flags): Basic flags service (#31) --- Cargo.lock | 48 ++++++++++++----- Cargo.toml | 3 +- feature-flags/Cargo.toml | 35 ++++++++++++ feature-flags/src/api.rs | 58 ++++++++++++++++++++ feature-flags/src/config.rs | 24 +++++++++ feature-flags/src/lib.rs | 7 +++ feature-flags/src/main.rs | 39 ++++++++++++++ feature-flags/src/redis.rs | 77 ++++++++++++++++++++++++++ feature-flags/src/router.rs | 19 +++++++ feature-flags/src/server.rs | 31 +++++++++++ feature-flags/src/v0_endpoint.rs | 89 +++++++++++++++++++++++++++++++ feature-flags/src/v0_request.rs | 68 +++++++++++++++++++++++ feature-flags/tests/common.rs | 66 +++++++++++++++++++++++ feature-flags/tests/test_flags.rs | 43 +++++++++++++++ 14 files changed, 594 insertions(+), 13 deletions(-) create mode 100644 feature-flags/Cargo.toml create mode 100644 feature-flags/src/api.rs create mode 100644 feature-flags/src/config.rs create mode 100644 feature-flags/src/lib.rs create mode 100644 feature-flags/src/main.rs create mode 100644 feature-flags/src/redis.rs create mode 100644 feature-flags/src/router.rs create mode 100644 feature-flags/src/server.rs create mode 100644 feature-flags/src/v0_endpoint.rs create mode 100644 feature-flags/src/v0_request.rs create mode 100644 feature-flags/tests/common.rs create mode 100644 feature-flags/tests/test_flags.rs diff --git a/Cargo.lock b/Cargo.lock index 5caeea4c0131f..0f475fa488c04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,7 +182,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.1.0", + "hyper 1.3.1", "hyper-util", "itoa", "matchit", @@ -273,7 +273,7 @@ dependencies = [ "bytes", "http 1.1.0", "http-body 1.0.0", - "hyper 1.1.0", + "hyper 1.3.1", "reqwest 0.11.24", "serde", "tokio", @@ -352,9 +352,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "capture" @@ -691,6 +691,29 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "feature-flags" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert-json-diff", + "async-trait", + "axum 0.7.5", + "axum-client-ip", + "bytes", + "envconfig", + "once_cell", + "rand", + "redis", + "reqwest 0.12.3", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "finl_unicode" version = "1.2.0" @@ -1226,9 +1249,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.1.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -1240,6 +1263,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "smallvec", "tokio", "want", ] @@ -1278,7 +1302,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.1.0", + "hyper 1.3.1", "hyper-util", "native-tls", "tokio", @@ -1297,7 +1321,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "hyper 1.1.0", + "hyper 1.3.1", "pin-project-lite", "socket2 0.5.5", "tokio", @@ -1519,7 +1543,7 @@ checksum = "5d58e362dc7206e9456ddbcdbd53c71ba441020e62104703075a69151e38d85f" dependencies = [ "base64 0.22.0", "http-body-util", - "hyper 1.1.0", + "hyper 1.3.1", "hyper-tls", "hyper-util", "indexmap 2.2.2", @@ -2310,7 +2334,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.1.0", + "hyper 1.3.1", "hyper-tls", "hyper-util", "ipnet", @@ -3054,9 +3078,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 180355b5ba49a..ea5d041027ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "capture", "common/health", + "feature-flags", "hook-api", "hook-common", "hook-janitor", @@ -49,7 +50,7 @@ opentelemetry-otlp = "0.15.0" opentelemetry_sdk = { version = "0.22.1", features = ["trace", "rt-tokio"] } rand = "0.8.5" rdkafka = { version = "0.36.0", features = ["cmake-build", "ssl", "tracing"] } -reqwest = { version = "0.12.3", features = ["stream"] } +reqwest = { version = "0.12.3", features = ["json", "stream"] } serde = { version = "1.0", features = ["derive"] } serde_derive = { version = "1.0" } serde_json = { version = "1.0" } diff --git a/feature-flags/Cargo.toml b/feature-flags/Cargo.toml new file mode 100644 index 0000000000000..ddfe0705a157e --- /dev/null +++ b/feature-flags/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "feature-flags" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +axum = { workspace = true } +axum-client-ip = { workspace = true } +envconfig = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +bytes = { workspace = true } +rand = { workspace = true } +redis = { version = "0.23.3", features = [ + "tokio-comp", + "cluster", + "cluster-async", +] } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } + +[lints] +workspace = true + +[dev-dependencies] +assert-json-diff = { workspace = true } +once_cell = "1.18.0" +reqwest = { workspace = true } + diff --git a/feature-flags/src/api.rs b/feature-flags/src/api.rs new file mode 100644 index 0000000000000..c94eed698357f --- /dev/null +++ b/feature-flags/src/api.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; + +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum FlagsResponseCode { + Ok = 1, +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FlagsResponse { + pub error_while_computing_flags: bool, + // TODO: better typing here, support bool responses + pub feature_flags: HashMap, +} + +#[derive(Error, Debug)] +pub enum FlagError { + #[error("failed to decode request: {0}")] + RequestDecodingError(String), + #[error("failed to parse request: {0}")] + RequestParsingError(#[from] serde_json::Error), + + #[error("Empty distinct_id in request")] + EmptyDistinctId, + #[error("No distinct_id in request")] + MissingDistinctId, + + #[error("No api_key in request")] + NoTokenError, + #[error("API key is not valid")] + TokenValidationError, + + #[error("rate limited")] + RateLimited, +} + +impl IntoResponse for FlagError { + fn into_response(self) -> Response { + match self { + FlagError::RequestDecodingError(_) + | FlagError::RequestParsingError(_) + | FlagError::EmptyDistinctId + | FlagError::MissingDistinctId => (StatusCode::BAD_REQUEST, self.to_string()), + + FlagError::NoTokenError | FlagError::TokenValidationError => { + (StatusCode::UNAUTHORIZED, self.to_string()) + } + + FlagError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, self.to_string()), + } + .into_response() + } +} diff --git a/feature-flags/src/config.rs b/feature-flags/src/config.rs new file mode 100644 index 0000000000000..3fa6f50e878e1 --- /dev/null +++ b/feature-flags/src/config.rs @@ -0,0 +1,24 @@ +use std::net::SocketAddr; + +use envconfig::Envconfig; + +#[derive(Envconfig, Clone)] +pub struct Config { + #[envconfig(default = "127.0.0.1:0")] + pub address: SocketAddr, + + #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] + pub write_database_url: String, + + #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] + pub read_database_url: String, + + #[envconfig(default = "1024")] + pub max_concurrent_jobs: usize, + + #[envconfig(default = "100")] + pub max_pg_connections: u32, + + #[envconfig(default = "redis://localhost:6379/")] + pub redis_url: String, +} diff --git a/feature-flags/src/lib.rs b/feature-flags/src/lib.rs new file mode 100644 index 0000000000000..9175b5c3974af --- /dev/null +++ b/feature-flags/src/lib.rs @@ -0,0 +1,7 @@ +pub mod api; +pub mod config; +pub mod redis; +pub mod router; +pub mod server; +pub mod v0_endpoint; +pub mod v0_request; diff --git a/feature-flags/src/main.rs b/feature-flags/src/main.rs new file mode 100644 index 0000000000000..980db6973893f --- /dev/null +++ b/feature-flags/src/main.rs @@ -0,0 +1,39 @@ +use envconfig::Envconfig; +use tokio::signal; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, Layer}; + +use feature_flags::config::Config; +use feature_flags::server::serve; + +async fn shutdown() { + let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to register SIGTERM handler"); + + let mut interrupt = signal::unix::signal(signal::unix::SignalKind::interrupt()) + .expect("failed to register SIGINT handler"); + + tokio::select! { + _ = term.recv() => {}, + _ = interrupt.recv() => {}, + }; + + tracing::info!("Shutting down gracefully..."); +} + +#[tokio::main] +async fn main() { + let config = Config::init_from_env().expect("Invalid configuration:"); + + // Basic logging for now: + // - stdout with a level configured by the RUST_LOG envvar (default=ERROR) + let log_layer = tracing_subscriber::fmt::layer().with_filter(EnvFilter::from_default_env()); + tracing_subscriber::registry().with(log_layer).init(); + + // Open the TCP port and start the server + let listener = tokio::net::TcpListener::bind(config.address) + .await + .expect("could not bind port"); + serve(config, listener, shutdown()).await +} diff --git a/feature-flags/src/redis.rs b/feature-flags/src/redis.rs new file mode 100644 index 0000000000000..8c038201698e5 --- /dev/null +++ b/feature-flags/src/redis.rs @@ -0,0 +1,77 @@ +use std::time::Duration; + +use anyhow::Result; +use async_trait::async_trait; +use redis::AsyncCommands; +use tokio::time::timeout; + +// average for all commands is <10ms, check grafana +const REDIS_TIMEOUT_MILLISECS: u64 = 10; + +/// A simple redis wrapper +/// Copied from capture/src/redis.rs. +/// TODO: Modify this to support hincrby, get, and set commands. + +#[async_trait] +pub trait Client { + // A very simplified wrapper, but works for our usage + async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result>; +} + +pub struct RedisClient { + client: redis::Client, +} + +impl RedisClient { + pub fn new(addr: String) -> Result { + let client = redis::Client::open(addr)?; + + Ok(RedisClient { client }) + } +} + +#[async_trait] +impl Client for RedisClient { + async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result> { + let mut conn = self.client.get_async_connection().await?; + + let results = conn.zrangebyscore(k, min, max); + let fut = timeout(Duration::from_secs(REDIS_TIMEOUT_MILLISECS), results).await?; + + Ok(fut?) + } +} + +// TODO: Find if there's a better way around this. +#[derive(Clone)] +pub struct MockRedisClient { + zrangebyscore_ret: Vec, +} + +impl MockRedisClient { + pub fn new() -> MockRedisClient { + MockRedisClient { + zrangebyscore_ret: Vec::new(), + } + } + + pub fn zrangebyscore_ret(&mut self, ret: Vec) -> Self { + self.zrangebyscore_ret = ret; + + self.clone() + } +} + +impl Default for MockRedisClient { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl Client for MockRedisClient { + // A very simplified wrapper, but works for our usage + async fn zrangebyscore(&self, _k: String, _min: String, _max: String) -> Result> { + Ok(self.zrangebyscore_ret.clone()) + } +} diff --git a/feature-flags/src/router.rs b/feature-flags/src/router.rs new file mode 100644 index 0000000000000..8824d44efdbde --- /dev/null +++ b/feature-flags/src/router.rs @@ -0,0 +1,19 @@ +use std::sync::Arc; + +use axum::{routing::post, Router}; + +use crate::{redis::Client, v0_endpoint}; + +#[derive(Clone)] +pub struct State { + pub redis: Arc, + // TODO: Add pgClient when ready +} + +pub fn router(redis: Arc) -> Router { + let state = State { redis }; + + Router::new() + .route("/flags", post(v0_endpoint::flags).get(v0_endpoint::flags)) + .with_state(state) +} diff --git a/feature-flags/src/server.rs b/feature-flags/src/server.rs new file mode 100644 index 0000000000000..ffe6b0efb7068 --- /dev/null +++ b/feature-flags/src/server.rs @@ -0,0 +1,31 @@ +use std::future::Future; +use std::net::SocketAddr; +use std::sync::Arc; + +use tokio::net::TcpListener; + +use crate::config::Config; + +use crate::redis::RedisClient; +use crate::router; + +pub async fn serve(config: Config, listener: TcpListener, shutdown: F) +where + F: Future + Send + 'static, +{ + let redis_client = + Arc::new(RedisClient::new(config.redis_url).expect("failed to create redis client")); + + let app = router::router(redis_client); + + // run our app with hyper + // `axum::Server` is a re-export of `hyper::Server` + tracing::info!("listening on {:?}", listener.local_addr().unwrap()); + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(shutdown) + .await + .unwrap() +} diff --git a/feature-flags/src/v0_endpoint.rs b/feature-flags/src/v0_endpoint.rs new file mode 100644 index 0000000000000..8f7761181e050 --- /dev/null +++ b/feature-flags/src/v0_endpoint.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +use axum::{debug_handler, Json}; +use bytes::Bytes; +// TODO: stream this instead +use axum::extract::{MatchedPath, Query, State}; +use axum::http::{HeaderMap, Method}; +use axum_client_ip::InsecureClientIp; +use tracing::instrument; + +use crate::{ + api::{FlagError, FlagsResponse}, + router, + v0_request::{FlagRequest, FlagsQueryParams}, +}; + +/// Feature flag evaluation endpoint. +/// Only supports a specific shape of data, and rejects any malformed data. + +#[instrument( + skip_all, + fields( + path, + token, + batch_size, + user_agent, + content_encoding, + content_type, + version, + compression, + historical_migration + ) +)] +#[debug_handler] +pub async fn flags( + _state: State, + InsecureClientIp(ip): InsecureClientIp, + meta: Query, + headers: HeaderMap, + method: Method, + path: MatchedPath, + body: Bytes, +) -> Result, FlagError> { + let user_agent = headers + .get("user-agent") + .map_or("unknown", |v| v.to_str().unwrap_or("unknown")); + let content_encoding = headers + .get("content-encoding") + .map_or("unknown", |v| v.to_str().unwrap_or("unknown")); + + tracing::Span::current().record("user_agent", user_agent); + tracing::Span::current().record("content_encoding", content_encoding); + tracing::Span::current().record("version", meta.version.clone()); + tracing::Span::current().record("method", method.as_str()); + tracing::Span::current().record("path", path.as_str().trim_end_matches('/')); + tracing::Span::current().record("ip", ip.to_string()); + + let request = match headers + .get("content-type") + .map_or("", |v| v.to_str().unwrap_or("")) + { + "application/x-www-form-urlencoded" => { + return Err(FlagError::RequestDecodingError(String::from( + "invalid form data", + ))); + } + ct => { + tracing::Span::current().record("content_type", ct); + + FlagRequest::from_bytes(body) + } + }?; + + let token = request.extract_and_verify_token()?; + + tracing::Span::current().record("token", &token); + + tracing::debug!("request: {:?}", request); + + // TODO: Some actual processing for evaluating the feature flag + + Ok(Json(FlagsResponse { + error_while_computing_flags: false, + feature_flags: HashMap::from([ + ("beta-feature".to_string(), "variant-1".to_string()), + ("rollout-flag".to_string(), true.to_string()), + ]), + })) +} diff --git a/feature-flags/src/v0_request.rs b/feature-flags/src/v0_request.rs new file mode 100644 index 0000000000000..f2269df1b5f74 --- /dev/null +++ b/feature-flags/src/v0_request.rs @@ -0,0 +1,68 @@ +use std::collections::HashMap; + +use bytes::Bytes; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::instrument; + +use crate::api::FlagError; + +#[derive(Deserialize, Default)] +pub struct FlagsQueryParams { + #[serde(alias = "v")] + pub version: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct FlagRequest { + #[serde( + alias = "$token", + alias = "api_key", + skip_serializing_if = "Option::is_none" + )] + pub token: Option, + #[serde(alias = "$distinct_id", skip_serializing_if = "Option::is_none")] + pub distinct_id: Option, + pub geoip_disable: Option, + #[serde(default)] + pub person_properties: Option>, + #[serde(default)] + pub groups: Option>, + // TODO: better type this since we know its going to be a nested json + #[serde(default)] + pub group_properties: Option>, + #[serde(alias = "$anon_distinct_id", skip_serializing_if = "Option::is_none")] + pub anon_distinct_id: Option, +} + +impl FlagRequest { + /// Takes a request payload and tries to decompress and unmarshall it. + /// While posthog-js sends a compression query param, a sizable portion of requests + /// fail due to it being missing when the body is compressed. + /// Instead of trusting the parameter, we peek at the payload's first three bytes to + /// detect gzip, fallback to uncompressed utf8 otherwise. + #[instrument(skip_all)] + pub fn from_bytes(bytes: Bytes) -> Result { + tracing::debug!(len = bytes.len(), "decoding new request"); + // TODO: Add base64 decoding + let payload = String::from_utf8(bytes.into()).map_err(|e| { + tracing::error!("failed to decode body: {}", e); + FlagError::RequestDecodingError(String::from("invalid body encoding")) + })?; + + tracing::debug!(json = payload, "decoded event data"); + Ok(serde_json::from_str::(&payload)?) + } + + pub fn extract_and_verify_token(&self) -> Result { + let token = match self { + FlagRequest { + token: Some(token), .. + } => token.to_string(), + _ => return Err(FlagError::NoTokenError), + }; + // TODO: Get tokens from redis, confirm this one is valid + // validate_token(&token)?; + Ok(token) + } +} diff --git a/feature-flags/tests/common.rs b/feature-flags/tests/common.rs new file mode 100644 index 0000000000000..f66a11ff37c25 --- /dev/null +++ b/feature-flags/tests/common.rs @@ -0,0 +1,66 @@ +use std::net::SocketAddr; +use std::str::FromStr; +use std::string::ToString; +use std::sync::Arc; + +use once_cell::sync::Lazy; +use rand::distributions::Alphanumeric; +use rand::Rng; +use tokio::net::TcpListener; +use tokio::sync::Notify; + +use feature_flags::config::Config; +use feature_flags::server::serve; + +pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { + address: SocketAddr::from_str("127.0.0.1:0").unwrap(), + redis_url: "redis://localhost:6379/".to_string(), + write_database_url: "postgres://posthog:posthog@localhost:15432/test_database".to_string(), + read_database_url: "postgres://posthog:posthog@localhost:15432/test_database".to_string(), + max_concurrent_jobs: 1024, + max_pg_connections: 100, +}); + +pub struct ServerHandle { + pub addr: SocketAddr, + shutdown: Arc, +} + +impl ServerHandle { + pub async fn for_config(config: Config) -> ServerHandle { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let notify = Arc::new(Notify::new()); + let shutdown = notify.clone(); + + tokio::spawn(async move { + serve(config, listener, async move { notify.notified().await }).await + }); + ServerHandle { addr, shutdown } + } + + pub async fn send_flags_request>(&self, body: T) -> reqwest::Response { + let client = reqwest::Client::new(); + client + .post(format!("http://{:?}/flags", self.addr)) + .body(body) + .send() + .await + .expect("failed to send request") + } +} + +impl Drop for ServerHandle { + fn drop(&mut self) { + self.shutdown.notify_one() + } +} + +pub fn random_string(prefix: &str, length: usize) -> String { + let suffix: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(length) + .map(char::from) + .collect(); + format!("{}_{}", prefix, suffix) +} diff --git a/feature-flags/tests/test_flags.rs b/feature-flags/tests/test_flags.rs new file mode 100644 index 0000000000000..82f41f05d81c6 --- /dev/null +++ b/feature-flags/tests/test_flags.rs @@ -0,0 +1,43 @@ +use anyhow::Result; +use assert_json_diff::assert_json_include; + +use reqwest::StatusCode; +use serde_json::{json, Value}; + +use crate::common::*; +mod common; + +#[tokio::test] +async fn it_sends_flag_request() -> Result<()> { + let token = random_string("token", 16); + let distinct_id = "user_distinct_id".to_string(); + + let config = DEFAULT_CONFIG.clone(); + + let server = ServerHandle::for_config(config).await; + + let payload = json!({ + "token": token, + "distinct_id": distinct_id, + "groups": {"group1": "group1"} + }); + let res = server.send_flags_request(payload.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + // We don't want to deserialize the data into a flagResponse struct here, + // because we want to assert the shape of the raw json data. + let json_data = res.json::().await?; + + assert_json_include!( + actual: json_data, + expected: json!({ + "errorWhileComputingFlags": false, + "featureFlags": { + "beta-feature": "variant-1", + "rollout-flag": "true", + } + }) + ); + + Ok(()) +} From ebaf596ec571f0c2c2cc8bafbdd4ee932ac28b2a Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Mon, 27 May 2024 14:23:07 +0200 Subject: [PATCH 240/249] capture: add overflow_enabled option (#43) --- capture/src/config.rs | 3 +++ capture/src/server.rs | 42 ++++++++++++++++------------- capture/src/sinks/kafka.rs | 14 ++++++---- capture/tests/common.rs | 1 + capture/tests/events.rs | 54 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 23 deletions(-) diff --git a/capture/src/config.rs b/capture/src/config.rs index 07b7f89496d44..d91e7b7241337 100644 --- a/capture/src/config.rs +++ b/capture/src/config.rs @@ -13,6 +13,9 @@ pub struct Config { pub redis_url: String, pub otel_url: Option, + #[envconfig(default = "false")] + pub overflow_enabled: bool, + #[envconfig(default = "100")] pub overflow_per_second_limit: NonZeroU32, diff --git a/capture/src/server.rs b/capture/src/server.rs index 07049874edef9..85850363e762c 100644 --- a/capture/src/server.rs +++ b/capture/src/server.rs @@ -48,24 +48,30 @@ where .register("rdkafka".to_string(), Duration::seconds(30)) .await; - let partition = OverflowLimiter::new( - config.overflow_per_second_limit, - config.overflow_burst_limit, - config.overflow_forced_keys, - ); - if config.export_prometheus { - let partition = partition.clone(); - tokio::spawn(async move { - partition.report_metrics().await; - }); - } - { - // Ensure that the rate limiter state does not grow unbounded - let partition = partition.clone(); - tokio::spawn(async move { - partition.clean_state().await; - }); - } + let partition = match config.overflow_enabled { + false => None, + true => { + let partition = OverflowLimiter::new( + config.overflow_per_second_limit, + config.overflow_burst_limit, + config.overflow_forced_keys, + ); + if config.export_prometheus { + let partition = partition.clone(); + tokio::spawn(async move { + partition.report_metrics().await; + }); + } + { + // Ensure that the rate limiter state does not grow unbounded + let partition = partition.clone(); + tokio::spawn(async move { + partition.clean_state().await; + }); + } + Some(partition) + } + }; let sink = KafkaSink::new(config.kafka, sink_liveness, partition) .expect("failed to start Kafka sink"); diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index 945e581183d37..bc45fa1077f7b 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -80,7 +80,7 @@ impl rdkafka::ClientContext for KafkaContext { #[derive(Clone)] pub struct KafkaSink { producer: FutureProducer, - partition: OverflowLimiter, + partition: Option, main_topic: String, historical_topic: String, } @@ -89,7 +89,7 @@ impl KafkaSink { pub fn new( config: KafkaConfig, liveness: HealthHandle, - partition: OverflowLimiter, + partition: Option, ) -> anyhow::Result { info!("connecting to Kafka brokers at {}...", config.kafka_hosts); @@ -150,7 +150,11 @@ impl KafkaSink { DataType::AnalyticsHistorical => (&self.historical_topic, Some(event_key.as_str())), // We never trigger overflow on historical events DataType::AnalyticsMain => { // TODO: deprecate capture-led overflow or move logic in handler - if self.partition.is_limited(&event_key) { + let is_limited = match &self.partition { + None => false, + Some(partition) => partition.is_limited(&event_key), + }; + if is_limited { (&self.main_topic, None) // Analytics overflow goes to the main topic without locality } else { (&self.main_topic, Some(event_key.as_str())) @@ -280,11 +284,11 @@ mod tests { let handle = registry .register("one".to_string(), Duration::seconds(30)) .await; - let limiter = OverflowLimiter::new( + let limiter = Some(OverflowLimiter::new( NonZeroU32::new(10).unwrap(), NonZeroU32::new(10).unwrap(), None, - ); + )); let cluster = MockCluster::new(1).expect("failed to create mock brokers"); let config = config::KafkaConfig { kafka_producer_linger_ms: 0, diff --git a/capture/tests/common.rs b/capture/tests/common.rs index 788e6e28240c7..868b27c120a7f 100644 --- a/capture/tests/common.rs +++ b/capture/tests/common.rs @@ -29,6 +29,7 @@ pub static DEFAULT_CONFIG: Lazy = Lazy::new(|| Config { print_sink: false, address: SocketAddr::from_str("127.0.0.1:0").unwrap(), redis_url: "redis://localhost:6379/".to_string(), + overflow_enabled: false, overflow_burst_limit: NonZeroU32::new(5).unwrap(), overflow_per_second_limit: NonZeroU32::new(10).unwrap(), overflow_forced_keys: None, diff --git a/capture/tests/events.rs b/capture/tests/events.rs index 111b02c7f2cb1..7d2defcebd5ff 100644 --- a/capture/tests/events.rs +++ b/capture/tests/events.rs @@ -174,6 +174,7 @@ async fn it_overflows_events_on_burst() -> Result<()> { let mut config = DEFAULT_CONFIG.clone(); config.kafka.kafka_topic = topic.topic_name().to_string(); + config.overflow_enabled = true; config.overflow_burst_limit = NonZeroU32::new(2).unwrap(); config.overflow_per_second_limit = NonZeroU32::new(1).unwrap(); @@ -223,6 +224,7 @@ async fn it_does_not_overflow_team_with_different_ids() -> Result<()> { let mut config = DEFAULT_CONFIG.clone(); config.kafka.kafka_topic = topic.topic_name().to_string(); + config.overflow_enabled = true; config.overflow_burst_limit = NonZeroU32::new(1).unwrap(); config.overflow_per_second_limit = NonZeroU32::new(1).unwrap(); @@ -254,6 +256,58 @@ async fn it_does_not_overflow_team_with_different_ids() -> Result<()> { Ok(()) } +#[tokio::test] +async fn it_skips_overflows_when_disabled() -> Result<()> { + setup_tracing(); + + let token = random_string("token", 16); + let distinct_id = random_string("id", 16); + + let topic = EphemeralTopic::new().await; + + let mut config = DEFAULT_CONFIG.clone(); + config.kafka.kafka_topic = topic.topic_name().to_string(); + config.overflow_enabled = false; + config.overflow_burst_limit = NonZeroU32::new(2).unwrap(); + config.overflow_per_second_limit = NonZeroU32::new(1).unwrap(); + + let server = ServerHandle::for_config(config).await; + + let event = json!([{ + "token": token, + "event": "event1", + "distinct_id": distinct_id + },{ + "token": token, + "event": "event2", + "distinct_id": distinct_id + },{ + "token": token, + "event": "event3", + "distinct_id": distinct_id + }]); + + let res = server.capture_events(event.to_string()).await; + assert_eq!(StatusCode::OK, res.status()); + + assert_eq!( + topic.next_message_key()?.unwrap(), + format!("{}:{}", token, distinct_id) + ); + + assert_eq!( + topic.next_message_key()?.unwrap(), + format!("{}:{}", token, distinct_id) + ); + + // Should have triggered overflow, but has not + assert_eq!( + topic.next_message_key()?.unwrap(), + format!("{}:{}", token, distinct_id) + ); + Ok(()) +} + #[tokio::test] async fn it_trims_distinct_id() -> Result<()> { setup_tracing(); From f71dc0867bc0ce80c91953de69f5ca9f65521d84 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Wed, 29 May 2024 14:32:41 +0100 Subject: [PATCH 241/249] feat(flags): Do token validation and extract distinct id (#41) --- Cargo.lock | 39 ++++- feature-flags/Cargo.toml | 1 + feature-flags/src/api.rs | 9 ++ feature-flags/src/config.rs | 2 +- feature-flags/src/lib.rs | 9 ++ feature-flags/src/redis.rs | 73 +++++---- feature-flags/src/team.rs | 139 ++++++++++++++++++ feature-flags/src/test_utils.rs | 50 +++++++ feature-flags/src/v0_endpoint.rs | 23 +-- feature-flags/src/v0_request.rs | 90 ++++++++++-- .../tests/{common.rs => common/mod.rs} | 27 ++-- feature-flags/tests/test_flags.rs | 46 +++++- 12 files changed, 442 insertions(+), 66 deletions(-) create mode 100644 feature-flags/src/team.rs create mode 100644 feature-flags/src/test_utils.rs rename feature-flags/tests/{common.rs => common/mod.rs} (76%) diff --git a/Cargo.lock b/Cargo.lock index 0f475fa488c04..8642adee54b3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,7 @@ dependencies = [ "redis", "reqwest 0.12.3", "serde", + "serde-pickle", "serde_json", "thiserror", "tokio", @@ -1395,6 +1396,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "iter-read" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c397ca3ea05ad509c4ec451fea28b4771236a376ca1c69fd5143aae0cf8f93c4" + [[package]] name = "itertools" version = "0.12.1" @@ -1680,6 +1687,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1705,11 +1722,10 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] @@ -1726,9 +1742,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -2533,6 +2549,19 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-pickle" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762ad136a26407c6a80825813600ceeab5e613660d93d79a41f0ec877171e71" +dependencies = [ + "byteorder", + "iter-read", + "num-bigint", + "num-traits", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.196" diff --git a/feature-flags/Cargo.toml b/feature-flags/Cargo.toml index ddfe0705a157e..1e0c111d71263 100644 --- a/feature-flags/Cargo.toml +++ b/feature-flags/Cargo.toml @@ -24,6 +24,7 @@ redis = { version = "0.23.3", features = [ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +serde-pickle = { version = "1.1.1"} [lints] workspace = true diff --git a/feature-flags/src/api.rs b/feature-flags/src/api.rs index c94eed698357f..ccf4735e5b04a 100644 --- a/feature-flags/src/api.rs +++ b/feature-flags/src/api.rs @@ -37,6 +37,11 @@ pub enum FlagError { #[error("rate limited")] RateLimited, + + #[error("failed to parse redis cache data")] + DataParsingError, + #[error("redis unavailable")] + RedisUnavailable, } impl IntoResponse for FlagError { @@ -52,6 +57,10 @@ impl IntoResponse for FlagError { } FlagError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, self.to_string()), + + FlagError::DataParsingError | FlagError::RedisUnavailable => { + (StatusCode::SERVICE_UNAVAILABLE, self.to_string()) + } } .into_response() } diff --git a/feature-flags/src/config.rs b/feature-flags/src/config.rs index 3fa6f50e878e1..cc7ad37bf72c1 100644 --- a/feature-flags/src/config.rs +++ b/feature-flags/src/config.rs @@ -4,7 +4,7 @@ use envconfig::Envconfig; #[derive(Envconfig, Clone)] pub struct Config { - #[envconfig(default = "127.0.0.1:0")] + #[envconfig(default = "127.0.0.1:3001")] pub address: SocketAddr, #[envconfig(default = "postgres://posthog:posthog@localhost:15432/test_database")] diff --git a/feature-flags/src/lib.rs b/feature-flags/src/lib.rs index 9175b5c3974af..195a55c88095d 100644 --- a/feature-flags/src/lib.rs +++ b/feature-flags/src/lib.rs @@ -3,5 +3,14 @@ pub mod config; pub mod redis; pub mod router; pub mod server; +pub mod team; pub mod v0_endpoint; pub mod v0_request; + +// Test modules don't need to be compiled with main binary +// #[cfg(test)] +// TODO: To use in integration tests, we need to compile with binary +// or make it a separate feature using cfg(feature = "integration-tests") +// and then use this feature only in tests. +// For now, ok to just include in binary +pub mod test_utils; diff --git a/feature-flags/src/redis.rs b/feature-flags/src/redis.rs index 8c038201698e5..89dde421d0abc 100644 --- a/feature-flags/src/redis.rs +++ b/feature-flags/src/redis.rs @@ -2,20 +2,38 @@ use std::time::Duration; use anyhow::Result; use async_trait::async_trait; -use redis::AsyncCommands; +use redis::{AsyncCommands, RedisError}; +use thiserror::Error; use tokio::time::timeout; // average for all commands is <10ms, check grafana const REDIS_TIMEOUT_MILLISECS: u64 = 10; +#[derive(Error, Debug)] +pub enum CustomRedisError { + #[error("Not found in redis")] + NotFound, + + #[error("Pickle error: {0}")] + PickleError(#[from] serde_pickle::Error), + + #[error("Redis error: {0}")] + Other(#[from] RedisError), + + #[error("Timeout error")] + Timeout(#[from] tokio::time::error::Elapsed), +} /// A simple redis wrapper /// Copied from capture/src/redis.rs. -/// TODO: Modify this to support hincrby, get, and set commands. +/// TODO: Modify this to support hincrby #[async_trait] pub trait Client { // A very simplified wrapper, but works for our usage async fn zrangebyscore(&self, k: String, min: String, max: String) -> Result>; + + async fn get(&self, k: String) -> Result; + async fn set(&self, k: String, v: String) -> Result<()>; } pub struct RedisClient { @@ -40,38 +58,39 @@ impl Client for RedisClient { Ok(fut?) } -} -// TODO: Find if there's a better way around this. -#[derive(Clone)] -pub struct MockRedisClient { - zrangebyscore_ret: Vec, -} + async fn get(&self, k: String) -> Result { + let mut conn = self.client.get_async_connection().await?; -impl MockRedisClient { - pub fn new() -> MockRedisClient { - MockRedisClient { - zrangebyscore_ret: Vec::new(), + let results = conn.get(k); + let fut: Result, RedisError> = + timeout(Duration::from_secs(REDIS_TIMEOUT_MILLISECS), results).await?; + + // return NotFound error when empty or not found + if match &fut { + Ok(v) => v.is_empty(), + Err(_) => false, + } { + return Err(CustomRedisError::NotFound); } - } - pub fn zrangebyscore_ret(&mut self, ret: Vec) -> Self { - self.zrangebyscore_ret = ret; + // TRICKY: We serialise data to json, then django pickles it. + // Here we deserialize the bytes using serde_pickle, to get the json string. + let string_response: String = serde_pickle::from_slice(&fut?, Default::default())?; - self.clone() + Ok(string_response) } -} -impl Default for MockRedisClient { - fn default() -> Self { - Self::new() - } -} + async fn set(&self, k: String, v: String) -> Result<()> { + // TRICKY: We serialise data to json, then django pickles it. + // Here we serialize the json string to bytes using serde_pickle. + let bytes = serde_pickle::to_vec(&v, Default::default())?; -#[async_trait] -impl Client for MockRedisClient { - // A very simplified wrapper, but works for our usage - async fn zrangebyscore(&self, _k: String, _min: String, _max: String) -> Result> { - Ok(self.zrangebyscore_ret.clone()) + let mut conn = self.client.get_async_connection().await?; + + let results = conn.set(k, bytes); + let fut = timeout(Duration::from_secs(REDIS_TIMEOUT_MILLISECS), results).await?; + + Ok(fut?) } } diff --git a/feature-flags/src/team.rs b/feature-flags/src/team.rs new file mode 100644 index 0000000000000..ac62ea9ba55cb --- /dev/null +++ b/feature-flags/src/team.rs @@ -0,0 +1,139 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tracing::instrument; + +use crate::{ + api::FlagError, + redis::{Client, CustomRedisError}, +}; + +// TRICKY: This cache data is coming from django-redis. If it ever goes out of sync, we'll bork. +// TODO: Add integration tests across repos to ensure this doesn't happen. +pub const TEAM_TOKEN_CACHE_PREFIX: &str = "posthog:1:team_token:"; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Team { + pub id: i64, + pub name: String, + pub api_token: String, +} + +impl Team { + /// Validates a token, and returns a team if it exists. + + #[instrument(skip_all)] + pub async fn from_redis( + client: Arc, + token: String, + ) -> Result { + // TODO: Instead of failing here, i.e. if not in redis, fallback to pg + let serialized_team = client + .get(format!("{TEAM_TOKEN_CACHE_PREFIX}{}", token)) + .await + .map_err(|e| match e { + CustomRedisError::NotFound => FlagError::TokenValidationError, + CustomRedisError::PickleError(_) => { + tracing::error!("failed to fetch data: {}", e); + FlagError::DataParsingError + } + _ => { + tracing::error!("Unknown redis error: {}", e); + FlagError::RedisUnavailable + } + })?; + + let team: Team = serde_json::from_str(&serialized_team).map_err(|e| { + tracing::error!("failed to parse data to team: {}", e); + FlagError::DataParsingError + })?; + + Ok(team) + } +} + +#[cfg(test)] +mod tests { + use rand::Rng; + use redis::AsyncCommands; + + use super::*; + use crate::{ + team, + test_utils::{insert_new_team_in_redis, random_string, setup_redis_client}, + }; + + #[tokio::test] + async fn test_fetch_team_from_redis() { + let client = setup_redis_client(None); + + let team = insert_new_team_in_redis(client.clone()).await.unwrap(); + + let target_token = team.api_token; + + let team_from_redis = Team::from_redis(client.clone(), target_token.clone()) + .await + .unwrap(); + assert_eq!(team_from_redis.api_token, target_token); + assert_eq!(team_from_redis.id, team.id); + } + + #[tokio::test] + async fn test_fetch_invalid_team_from_redis() { + let client = setup_redis_client(None); + + match Team::from_redis(client.clone(), "banana".to_string()).await { + Err(FlagError::TokenValidationError) => (), + _ => panic!("Expected TokenValidationError"), + }; + } + + #[tokio::test] + async fn test_cant_connect_to_redis_error_is_not_token_validation_error() { + let client = setup_redis_client(Some("redis://localhost:1111/".to_string())); + + match Team::from_redis(client.clone(), "banana".to_string()).await { + Err(FlagError::RedisUnavailable) => (), + _ => panic!("Expected RedisUnavailable"), + }; + } + + #[tokio::test] + async fn test_corrupted_data_in_redis_is_handled() { + // TODO: Extend this test with fallback to pg + let id = rand::thread_rng().gen_range(0..10_000_000); + let token = random_string("phc_", 12); + let team = Team { + id, + name: "team".to_string(), + api_token: token, + }; + let serialized_team = serde_json::to_string(&team).expect("Failed to serialise team"); + + // manually insert non-pickled data in redis + let client = + redis::Client::open("redis://localhost:6379/").expect("Failed to create redis client"); + let mut conn = client + .get_async_connection() + .await + .expect("Failed to get redis connection"); + conn.set::( + format!( + "{}{}", + team::TEAM_TOKEN_CACHE_PREFIX, + team.api_token.clone() + ), + serialized_team, + ) + .await + .expect("Failed to write data to redis"); + + // now get client connection for data + let client = setup_redis_client(None); + + match Team::from_redis(client.clone(), team.api_token.clone()).await { + Err(FlagError::DataParsingError) => (), + Err(other) => panic!("Expected DataParsingError, got {:?}", other), + Ok(_) => panic!("Expected DataParsingError"), + }; + } +} diff --git a/feature-flags/src/test_utils.rs b/feature-flags/src/test_utils.rs new file mode 100644 index 0000000000000..75db86d3878ee --- /dev/null +++ b/feature-flags/src/test_utils.rs @@ -0,0 +1,50 @@ +use anyhow::Error; +use std::sync::Arc; + +use crate::{ + redis::{Client, RedisClient}, + team::{self, Team}, +}; +use rand::{distributions::Alphanumeric, Rng}; + +pub fn random_string(prefix: &str, length: usize) -> String { + let suffix: String = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(length) + .map(char::from) + .collect(); + format!("{}{}", prefix, suffix) +} + +pub async fn insert_new_team_in_redis(client: Arc) -> Result { + let id = rand::thread_rng().gen_range(0..10_000_000); + let token = random_string("phc_", 12); + let team = Team { + id, + name: "team".to_string(), + api_token: token, + }; + + let serialized_team = serde_json::to_string(&team)?; + client + .set( + format!( + "{}{}", + team::TEAM_TOKEN_CACHE_PREFIX, + team.api_token.clone() + ), + serialized_team, + ) + .await?; + + Ok(team) +} + +pub fn setup_redis_client(url: Option) -> Arc { + let redis_url = match url { + Some(value) => value, + None => "redis://localhost:6379/".to_string(), + }; + let client = RedisClient::new(redis_url).expect("Failed to create redis client"); + Arc::new(client) +} diff --git a/feature-flags/src/v0_endpoint.rs b/feature-flags/src/v0_endpoint.rs index 8f7761181e050..ba4bcef8fec47 100644 --- a/feature-flags/src/v0_endpoint.rs +++ b/feature-flags/src/v0_endpoint.rs @@ -33,7 +33,7 @@ use crate::{ )] #[debug_handler] pub async fn flags( - _state: State, + state: State, InsecureClientIp(ip): InsecureClientIp, meta: Query, headers: HeaderMap, @@ -59,21 +59,26 @@ pub async fn flags( .get("content-type") .map_or("", |v| v.to_str().unwrap_or("")) { - "application/x-www-form-urlencoded" => { - return Err(FlagError::RequestDecodingError(String::from( - "invalid form data", - ))); + "application/json" => { + tracing::Span::current().record("content_type", "application/json"); + FlagRequest::from_bytes(body) } ct => { - tracing::Span::current().record("content_type", ct); - - FlagRequest::from_bytes(body) + return Err(FlagError::RequestDecodingError(format!( + "unsupported content type: {}", + ct + ))); } }?; - let token = request.extract_and_verify_token()?; + let token = request + .extract_and_verify_token(state.redis.clone()) + .await?; + + let distinct_id = request.extract_distinct_id()?; tracing::Span::current().record("token", &token); + tracing::Span::current().record("distinct_id", &distinct_id); tracing::debug!("request: {:?}", request); diff --git a/feature-flags/src/v0_request.rs b/feature-flags/src/v0_request.rs index f2269df1b5f74..63b26b455f6f4 100644 --- a/feature-flags/src/v0_request.rs +++ b/feature-flags/src/v0_request.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use bytes::Bytes; use serde::{Deserialize, Serialize}; use serde_json::Value; use tracing::instrument; -use crate::api::FlagError; +use crate::{api::FlagError, redis::Client, team::Team}; #[derive(Deserialize, Default)] pub struct FlagsQueryParams { @@ -36,11 +36,8 @@ pub struct FlagRequest { } impl FlagRequest { - /// Takes a request payload and tries to decompress and unmarshall it. - /// While posthog-js sends a compression query param, a sizable portion of requests - /// fail due to it being missing when the body is compressed. - /// Instead of trusting the parameter, we peek at the payload's first three bytes to - /// detect gzip, fallback to uncompressed utf8 otherwise. + /// Takes a request payload and tries to read it. + /// Only supports base64 encoded payloads or uncompressed utf-8 as json. #[instrument(skip_all)] pub fn from_bytes(bytes: Bytes) -> Result { tracing::debug!(len = bytes.len(), "decoding new request"); @@ -54,15 +51,88 @@ impl FlagRequest { Ok(serde_json::from_str::(&payload)?) } - pub fn extract_and_verify_token(&self) -> Result { + pub async fn extract_and_verify_token( + &self, + redis_client: Arc, + ) -> Result { let token = match self { FlagRequest { token: Some(token), .. } => token.to_string(), _ => return Err(FlagError::NoTokenError), }; - // TODO: Get tokens from redis, confirm this one is valid - // validate_token(&token)?; + + // validate token + Team::from_redis(redis_client, token.clone()).await?; + + // TODO: fallback when token not found in redis + Ok(token) } + + pub fn extract_distinct_id(&self) -> Result { + let distinct_id = match &self.distinct_id { + None => return Err(FlagError::MissingDistinctId), + Some(id) => id, + }; + + match distinct_id.len() { + 0 => Err(FlagError::EmptyDistinctId), + 1..=200 => Ok(distinct_id.to_owned()), + _ => Ok(distinct_id.chars().take(200).collect()), + } + } +} + +#[cfg(test)] +mod tests { + use crate::api::FlagError; + use crate::v0_request::FlagRequest; + use bytes::Bytes; + use serde_json::json; + + #[test] + fn empty_distinct_id_not_accepted() { + let json = json!({ + "distinct_id": "", + "token": "my_token1", + }); + let bytes = Bytes::from(json.to_string()); + + let flag_payload = FlagRequest::from_bytes(bytes).expect("failed to parse request"); + + match flag_payload.extract_distinct_id() { + Err(FlagError::EmptyDistinctId) => (), + _ => panic!("expected empty distinct id error"), + }; + } + + #[test] + fn too_large_distinct_id_is_truncated() { + let json = json!({ + "distinct_id": std::iter::repeat("a").take(210).collect::(), + "token": "my_token1", + }); + let bytes = Bytes::from(json.to_string()); + + let flag_payload = FlagRequest::from_bytes(bytes).expect("failed to parse request"); + + assert_eq!(flag_payload.extract_distinct_id().unwrap().len(), 200); + } + + #[test] + fn distinct_id_is_returned_correctly() { + let json = json!({ + "$distinct_id": "alakazam", + "token": "my_token1", + }); + let bytes = Bytes::from(json.to_string()); + + let flag_payload = FlagRequest::from_bytes(bytes).expect("failed to parse request"); + + match flag_payload.extract_distinct_id() { + Ok(id) => assert_eq!(id, "alakazam"), + _ => panic!("expected distinct id"), + }; + } } diff --git a/feature-flags/tests/common.rs b/feature-flags/tests/common/mod.rs similarity index 76% rename from feature-flags/tests/common.rs rename to feature-flags/tests/common/mod.rs index f66a11ff37c25..c8644fe1f4542 100644 --- a/feature-flags/tests/common.rs +++ b/feature-flags/tests/common/mod.rs @@ -4,8 +4,7 @@ use std::string::ToString; use std::sync::Arc; use once_cell::sync::Lazy; -use rand::distributions::Alphanumeric; -use rand::Rng; +use reqwest::header::CONTENT_TYPE; use tokio::net::TcpListener; use tokio::sync::Notify; @@ -44,6 +43,21 @@ impl ServerHandle { client .post(format!("http://{:?}/flags", self.addr)) .body(body) + .header(CONTENT_TYPE, "application/json") + .send() + .await + .expect("failed to send request") + } + + pub async fn send_invalid_header_for_flags_request>( + &self, + body: T, + ) -> reqwest::Response { + let client = reqwest::Client::new(); + client + .post(format!("http://{:?}/flags", self.addr)) + .body(body) + .header(CONTENT_TYPE, "xyz") .send() .await .expect("failed to send request") @@ -55,12 +69,3 @@ impl Drop for ServerHandle { self.shutdown.notify_one() } } - -pub fn random_string(prefix: &str, length: usize) -> String { - let suffix: String = rand::thread_rng() - .sample_iter(Alphanumeric) - .take(length) - .map(char::from) - .collect(); - format!("{}_{}", prefix, suffix) -} diff --git a/feature-flags/tests/test_flags.rs b/feature-flags/tests/test_flags.rs index 82f41f05d81c6..2ceba24efd712 100644 --- a/feature-flags/tests/test_flags.rs +++ b/feature-flags/tests/test_flags.rs @@ -5,14 +5,20 @@ use reqwest::StatusCode; use serde_json::{json, Value}; use crate::common::*; -mod common; + +use feature_flags::test_utils::{insert_new_team_in_redis, setup_redis_client}; + +pub mod common; #[tokio::test] async fn it_sends_flag_request() -> Result<()> { - let token = random_string("token", 16); + let config = DEFAULT_CONFIG.clone(); + let distinct_id = "user_distinct_id".to_string(); - let config = DEFAULT_CONFIG.clone(); + let client = setup_redis_client(Some(config.redis_url.clone())); + let team = insert_new_team_in_redis(client.clone()).await.unwrap(); + let token = team.api_token; let server = ServerHandle::for_config(config).await; @@ -41,3 +47,37 @@ async fn it_sends_flag_request() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn it_rejects_invalid_headers_flag_request() -> Result<()> { + let config = DEFAULT_CONFIG.clone(); + + let distinct_id = "user_distinct_id".to_string(); + + let client = setup_redis_client(Some(config.redis_url.clone())); + let team = insert_new_team_in_redis(client.clone()).await.unwrap(); + let token = team.api_token; + + let server = ServerHandle::for_config(config).await; + + let payload = json!({ + "token": token, + "distinct_id": distinct_id, + "groups": {"group1": "group1"} + }); + let res = server + .send_invalid_header_for_flags_request(payload.to_string()) + .await; + assert_eq!(StatusCode::BAD_REQUEST, res.status()); + + // We don't want to deserialize the data into a flagResponse struct here, + // because we want to assert the shape of the raw json data. + let response_text = res.text().await?; + + assert_eq!( + response_text, + "failed to decode request: unsupported content type: xyz" + ); + + Ok(()) +} From ff0780cf5badd14c5fdf9a84c27ee222120639cd Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 29 May 2024 16:26:11 +0200 Subject: [PATCH 242/249] capture: add broker rtt latency and timeout metrics (#44) --- capture/src/sinks/kafka.rs | 41 +++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index bc45fa1077f7b..bff61b56419d4 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -36,12 +36,11 @@ impl rdkafka::ClientContext for KafkaContext { for (topic, stats) in stats.topics { gauge!( "capture_kafka_produce_avg_batch_size_bytes", - "topic" => topic.clone() + "topic" => topic.clone() ) .set(stats.batchsize.avg as f64); gauge!( "capture_kafka_produce_avg_batch_size_events", - "topic" => topic ) .set(stats.batchcnt.avg as f64); @@ -49,30 +48,58 @@ impl rdkafka::ClientContext for KafkaContext { for (_, stats) in stats.brokers { let id_string = format!("{}", stats.nodeid); + if let Some(rtt) = stats.rtt { + gauge!( + "capture_kafka_produce_rtt_latency_ms", + "quantile" => "p50", + "broker" => id_string.clone() + ) + .set(rtt.p50 as f64); + gauge!( + "capture_kafka_produce_rtt_latency_ms", + "quantile" => "p90", + "broker" => id_string.clone() + ) + .set(rtt.p90 as f64); + gauge!( + "capture_kafka_produce_rtt_latency_ms", + "quantile" => "p95", + "broker" => id_string.clone() + ) + .set(rtt.p95 as f64); + gauge!( + "capture_kafka_produce_rtt_latency_ms", + "quantile" => "p99", + "broker" => id_string.clone() + ) + .set(rtt.p99 as f64); + } + gauge!( "capture_kafka_broker_requests_pending", - "broker" => id_string.clone() ) .set(stats.outbuf_cnt as f64); gauge!( "capture_kafka_broker_responses_awaiting", - "broker" => id_string.clone() ) .set(stats.waitresp_cnt as f64); counter!( "capture_kafka_broker_tx_errors_total", - "broker" => id_string.clone() ) .absolute(stats.txerrs); counter!( "capture_kafka_broker_rx_errors_total", - - "broker" => id_string + "broker" => id_string.clone() ) .absolute(stats.rxerrs); + counter!( + "capture_kafka_broker_request_timeouts", + "broker" => id_string + ) + .absolute(stats.req_timeouts); } } } From 8d69910b091cb2b9ff2ada1a2946b92804983c89 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 30 May 2024 15:59:20 +0200 Subject: [PATCH 243/249] capture: fix produce_rtt_latency metric unit (#46) --- capture/src/sinks/kafka.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/capture/src/sinks/kafka.rs b/capture/src/sinks/kafka.rs index bff61b56419d4..b82d3c342a115 100644 --- a/capture/src/sinks/kafka.rs +++ b/capture/src/sinks/kafka.rs @@ -50,25 +50,25 @@ impl rdkafka::ClientContext for KafkaContext { let id_string = format!("{}", stats.nodeid); if let Some(rtt) = stats.rtt { gauge!( - "capture_kafka_produce_rtt_latency_ms", + "capture_kafka_produce_rtt_latency_us", "quantile" => "p50", "broker" => id_string.clone() ) .set(rtt.p50 as f64); gauge!( - "capture_kafka_produce_rtt_latency_ms", + "capture_kafka_produce_rtt_latency_us", "quantile" => "p90", "broker" => id_string.clone() ) .set(rtt.p90 as f64); gauge!( - "capture_kafka_produce_rtt_latency_ms", + "capture_kafka_produce_rtt_latency_us", "quantile" => "p95", "broker" => id_string.clone() ) .set(rtt.p95 as f64); gauge!( - "capture_kafka_produce_rtt_latency_ms", + "capture_kafka_produce_rtt_latency_us", "quantile" => "p99", "broker" => id_string.clone() ) From 63db2a6c20182445d5d3cc71aff55fb099e6c60a Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Thu, 30 May 2024 17:18:28 +0100 Subject: [PATCH 244/249] feat(flags): Extract flag definitions from redis (#42) --- feature-flags/src/flag_definitions.rs | 200 ++++++++++++++++++++++++++ feature-flags/src/lib.rs | 1 + feature-flags/src/test_utils.rs | 43 ++++++ 3 files changed, 244 insertions(+) create mode 100644 feature-flags/src/flag_definitions.rs diff --git a/feature-flags/src/flag_definitions.rs b/feature-flags/src/flag_definitions.rs new file mode 100644 index 0000000000000..29ec8d8c38c49 --- /dev/null +++ b/feature-flags/src/flag_definitions.rs @@ -0,0 +1,200 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tracing::instrument; + +use crate::{ + api::FlagError, + redis::{Client, CustomRedisError}, +}; + +// TRICKY: This cache data is coming from django-redis. If it ever goes out of sync, we'll bork. +// TODO: Add integration tests across repos to ensure this doesn't happen. +pub const TEAM_FLAGS_CACHE_PREFIX: &str = "posthog:1:team_feature_flags_"; + +// TODO: Hmm, revisit when dealing with groups, but seems like +// ideal to just treat it as a u8 and do our own validation on top +#[derive(Debug, Deserialize, Serialize)] +pub enum GroupTypeIndex {} + +#[derive(Debug, Deserialize, Serialize)] +pub enum OperatorType { + #[serde(rename = "exact")] + Exact, + #[serde(rename = "is_not")] + IsNot, + #[serde(rename = "icontains")] + Icontains, + #[serde(rename = "not_icontains")] + NotIcontains, + #[serde(rename = "regex")] + Regex, + #[serde(rename = "not_regex")] + NotRegex, + #[serde(rename = "gt")] + Gt, + #[serde(rename = "lt")] + Lt, + #[serde(rename = "gte")] + Gte, + #[serde(rename = "lte")] + Lte, + #[serde(rename = "is_set")] + IsSet, + #[serde(rename = "is_not_set")] + IsNotSet, + #[serde(rename = "is_date_exact")] + IsDateExact, + #[serde(rename = "is_date_after")] + IsDateAfter, + #[serde(rename = "is_date_before")] + IsDateBefore, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PropertyFilter { + pub key: String, + pub value: serde_json::Value, + pub operator: Option, + #[serde(rename = "type")] + pub prop_type: String, + pub group_type_index: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct FlagGroupType { + pub properties: Option>, + pub rollout_percentage: Option, + pub variant: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MultivariateFlagVariant { + pub key: String, + pub name: Option, + pub rollout_percentage: f32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MultivariateFlagOptions { + pub variants: Vec, +} + +// TODO: test name with https://www.fileformat.info/info/charset/UTF-16/list.htm values, like '𝖕𝖗𝖔𝖕𝖊𝖗𝖙𝖞': `𝓿𝓪𝓵𝓾𝓮` + +#[derive(Debug, Deserialize, Serialize)] +pub struct FlagFilters { + pub groups: Vec, + pub multivariate: Option, + pub aggregation_group_type_index: Option, + pub payloads: Option, + pub super_groups: Option>, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct FeatureFlag { + pub id: i64, + pub team_id: i64, + pub name: Option, + pub key: String, + pub filters: FlagFilters, + #[serde(default)] + pub deleted: bool, + #[serde(default)] + pub active: bool, + #[serde(default)] + pub ensure_experience_continuity: bool, +} + +#[derive(Debug, Deserialize, Serialize)] + +pub struct FeatureFlagList { + pub flags: Vec, +} + +impl FeatureFlagList { + /// Returns feature flags given a team_id + + #[instrument(skip_all)] + pub async fn from_redis( + client: Arc, + team_id: i64, + ) -> Result { + // TODO: Instead of failing here, i.e. if not in redis, fallback to pg + let serialized_flags = client + .get(format!("{TEAM_FLAGS_CACHE_PREFIX}{}", team_id)) + .await + .map_err(|e| match e { + CustomRedisError::NotFound => FlagError::TokenValidationError, + CustomRedisError::PickleError(_) => { + tracing::error!("failed to fetch data: {}", e); + println!("failed to fetch data: {}", e); + FlagError::DataParsingError + } + _ => { + tracing::error!("Unknown redis error: {}", e); + FlagError::RedisUnavailable + } + })?; + + let flags_list: Vec = + serde_json::from_str(&serialized_flags).map_err(|e| { + tracing::error!("failed to parse data to flags list: {}", e); + println!("failed to parse data: {}", e); + + FlagError::DataParsingError + })?; + + Ok(FeatureFlagList { flags: flags_list }) + } +} + +#[cfg(test)] +mod tests { + use rand::Rng; + + use super::*; + use crate::test_utils::{ + insert_flags_for_team_in_redis, insert_new_team_in_redis, setup_redis_client, + }; + + #[tokio::test] + async fn test_fetch_flags_from_redis() { + let client = setup_redis_client(None); + + let team = insert_new_team_in_redis(client.clone()).await.unwrap(); + + insert_flags_for_team_in_redis(client.clone(), team.id, None) + .await + .expect("Failed to insert flags"); + + let flags_from_redis = FeatureFlagList::from_redis(client.clone(), team.id) + .await + .unwrap(); + assert_eq!(flags_from_redis.flags.len(), 1); + let flag = flags_from_redis.flags.get(0).unwrap(); + assert_eq!(flag.key, "flag1"); + assert_eq!(flag.team_id, team.id); + assert_eq!(flag.filters.groups.len(), 1); + assert_eq!(flag.filters.groups[0].properties.as_ref().unwrap().len(), 1); + } + + #[tokio::test] + async fn test_fetch_invalid_team_from_redis() { + let client = setup_redis_client(None); + + match FeatureFlagList::from_redis(client.clone(), 1234).await { + Err(FlagError::TokenValidationError) => (), + _ => panic!("Expected TokenValidationError"), + }; + } + + #[tokio::test] + async fn test_cant_connect_to_redis_error_is_not_token_validation_error() { + let client = setup_redis_client(Some("redis://localhost:1111/".to_string())); + + match FeatureFlagList::from_redis(client.clone(), 1234).await { + Err(FlagError::RedisUnavailable) => (), + _ => panic!("Expected RedisUnavailable"), + }; + } +} diff --git a/feature-flags/src/lib.rs b/feature-flags/src/lib.rs index 195a55c88095d..0352c21c3382a 100644 --- a/feature-flags/src/lib.rs +++ b/feature-flags/src/lib.rs @@ -1,5 +1,6 @@ pub mod api; pub mod config; +pub mod flag_definitions; pub mod redis; pub mod router; pub mod server; diff --git a/feature-flags/src/test_utils.rs b/feature-flags/src/test_utils.rs index 75db86d3878ee..0cefb7eda8e7c 100644 --- a/feature-flags/src/test_utils.rs +++ b/feature-flags/src/test_utils.rs @@ -1,7 +1,9 @@ use anyhow::Error; +use serde_json::json; use std::sync::Arc; use crate::{ + flag_definitions, redis::{Client, RedisClient}, team::{self, Team}, }; @@ -40,6 +42,47 @@ pub async fn insert_new_team_in_redis(client: Arc) -> Result, + team_id: i64, + json_value: Option, +) -> Result<(), Error> { + let payload = match json_value { + Some(value) => value, + None => json!([{ + "id": 1, + "key": "flag1", + "name": "flag1 description", + "active": true, + "deleted": false, + "team_id": team_id, + "filters": { + "groups": [ + { + "properties": [ + { + "key": "email", + "value": "a@b.com", + "type": "person", + }, + ] + }, + ], + }, + }]) + .to_string(), + }; + + client + .set( + format!("{}{}", flag_definitions::TEAM_FLAGS_CACHE_PREFIX, team_id), + payload, + ) + .await?; + + Ok(()) +} + pub fn setup_redis_client(url: Option) -> Arc { let redis_url = match url { Some(value) => value, From cf302723ef9e0e2abc8e32307c8e9e7c3072a407 Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Thu, 6 Jun 2024 16:31:06 +0200 Subject: [PATCH 245/249] capture: subdivide process_events_error cause (#48) --- capture/src/v0_endpoint.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/capture/src/v0_endpoint.rs b/capture/src/v0_endpoint.rs index 3849e29328efa..ff4b90f2662e2 100644 --- a/capture/src/v0_endpoint.rs +++ b/capture/src/v0_endpoint.rs @@ -150,7 +150,14 @@ pub async fn event( tracing::debug!(context=?context, events=?events, "decoded request"); if let Err(err) = process_events(state.sink.clone(), &events, &context).await { - report_dropped_events("process_events_error", events.len() as u64); + let cause = match err { + // TODO: automate this with a macro + CaptureError::EmptyDistinctId => "empty_distinct_id", + CaptureError::MissingDistinctId => "missing_distinct_id", + CaptureError::MissingEventName => "missing_event_name", + _ => "process_events_error", + }; + report_dropped_events(cause, events.len() as u64); tracing::log::warn!("rejected invalid payload: {}", err); return Err(err); } From f28466a0f9699441109060ba1ba4e7baf3705a8f Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Mon, 10 Jun 2024 10:12:32 +0100 Subject: [PATCH 246/249] feat(flags): Match flags on rollout percentage (#45) --- Cargo.lock | 1 + feature-flags/Cargo.toml | 1 + feature-flags/src/flag_definitions.rs | 81 +- feature-flags/src/flag_matching.rs | 161 +++ feature-flags/src/lib.rs | 1 + feature-flags/src/team.rs | 1 + feature-flags/src/test_utils.rs | 35 +- .../tests/test_flag_matching_consistency.rs | 1209 +++++++++++++++++ 8 files changed, 1454 insertions(+), 36 deletions(-) create mode 100644 feature-flags/src/flag_matching.rs create mode 100644 feature-flags/tests/test_flag_matching_consistency.rs diff --git a/Cargo.lock b/Cargo.lock index 8642adee54b3a..b9f226bb08bb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -709,6 +709,7 @@ dependencies = [ "serde", "serde-pickle", "serde_json", + "sha1", "thiserror", "tokio", "tracing", diff --git a/feature-flags/Cargo.toml b/feature-flags/Cargo.toml index 1e0c111d71263..4993930362857 100644 --- a/feature-flags/Cargo.toml +++ b/feature-flags/Cargo.toml @@ -25,6 +25,7 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } serde-pickle = { version = "1.1.1"} +sha1 = "0.10.6" [lints] workspace = true diff --git a/feature-flags/src/flag_definitions.rs b/feature-flags/src/flag_definitions.rs index 29ec8d8c38c49..1f4582c606bd7 100644 --- a/feature-flags/src/flag_definitions.rs +++ b/feature-flags/src/flag_definitions.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::sync::Arc; use tracing::instrument; @@ -13,44 +13,30 @@ pub const TEAM_FLAGS_CACHE_PREFIX: &str = "posthog:1:team_feature_flags_"; // TODO: Hmm, revisit when dealing with groups, but seems like // ideal to just treat it as a u8 and do our own validation on top -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize)] pub enum GroupTypeIndex {} -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum OperatorType { - #[serde(rename = "exact")] Exact, - #[serde(rename = "is_not")] IsNot, - #[serde(rename = "icontains")] Icontains, - #[serde(rename = "not_icontains")] NotIcontains, - #[serde(rename = "regex")] Regex, - #[serde(rename = "not_regex")] NotRegex, - #[serde(rename = "gt")] Gt, - #[serde(rename = "lt")] Lt, - #[serde(rename = "gte")] Gte, - #[serde(rename = "lte")] Lte, - #[serde(rename = "is_set")] IsSet, - #[serde(rename = "is_not_set")] IsNotSet, - #[serde(rename = "is_date_exact")] IsDateExact, - #[serde(rename = "is_date_after")] IsDateAfter, - #[serde(rename = "is_date_before")] IsDateBefore, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct PropertyFilter { pub key: String, pub value: serde_json::Value, @@ -60,28 +46,28 @@ pub struct PropertyFilter { pub group_type_index: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct FlagGroupType { pub properties: Option>, - pub rollout_percentage: Option, + pub rollout_percentage: Option, pub variant: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct MultivariateFlagVariant { pub key: String, pub name: Option, - pub rollout_percentage: f32, + pub rollout_percentage: f64, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct MultivariateFlagOptions { pub variants: Vec, } // TODO: test name with https://www.fileformat.info/info/charset/UTF-16/list.htm values, like '𝖕𝖗𝖔𝖕𝖊𝖗𝖙𝖞': `𝓿𝓪𝓵𝓾𝓮` -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct FlagFilters { pub groups: Vec, pub multivariate: Option, @@ -90,7 +76,7 @@ pub struct FlagFilters { pub super_groups: Option>, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize)] pub struct FeatureFlag { pub id: i64, pub team_id: i64, @@ -105,15 +91,31 @@ pub struct FeatureFlag { pub ensure_experience_continuity: bool, } -#[derive(Debug, Deserialize, Serialize)] +impl FeatureFlag { + pub fn get_group_type_index(&self) -> Option { + self.filters.aggregation_group_type_index + } + + pub fn get_conditions(&self) -> &Vec { + &self.filters.groups + } + + pub fn get_variants(&self) -> Vec { + self.filters + .multivariate + .clone() + .map_or(vec![], |m| m.variants) + } +} + +#[derive(Debug, Deserialize)] pub struct FeatureFlagList { pub flags: Vec, } impl FeatureFlagList { - /// Returns feature flags given a team_id - + /// Returns feature flags from redis given a team_id #[instrument(skip_all)] pub async fn from_redis( client: Arc, @@ -126,6 +128,8 @@ impl FeatureFlagList { .map_err(|e| match e { CustomRedisError::NotFound => FlagError::TokenValidationError, CustomRedisError::PickleError(_) => { + // TODO: Implement From trait for FlagError so we don't need to map + // CustomRedisError ourselves tracing::error!("failed to fetch data: {}", e); println!("failed to fetch data: {}", e); FlagError::DataParsingError @@ -150,8 +154,6 @@ impl FeatureFlagList { #[cfg(test)] mod tests { - use rand::Rng; - use super::*; use crate::test_utils::{ insert_flags_for_team_in_redis, insert_new_team_in_redis, setup_redis_client, @@ -161,7 +163,9 @@ mod tests { async fn test_fetch_flags_from_redis() { let client = setup_redis_client(None); - let team = insert_new_team_in_redis(client.clone()).await.unwrap(); + let team = insert_new_team_in_redis(client.clone()) + .await + .expect("Failed to insert team"); insert_flags_for_team_in_redis(client.clone(), team.id, None) .await @@ -169,13 +173,20 @@ mod tests { let flags_from_redis = FeatureFlagList::from_redis(client.clone(), team.id) .await - .unwrap(); + .expect("Failed to fetch flags from redis"); assert_eq!(flags_from_redis.flags.len(), 1); - let flag = flags_from_redis.flags.get(0).unwrap(); + let flag = flags_from_redis.flags.get(0).expect("Empty flags in redis"); assert_eq!(flag.key, "flag1"); assert_eq!(flag.team_id, team.id); assert_eq!(flag.filters.groups.len(), 1); - assert_eq!(flag.filters.groups[0].properties.as_ref().unwrap().len(), 1); + assert_eq!( + flag.filters.groups[0] + .properties + .as_ref() + .expect("Properties don't exist on flag") + .len(), + 1 + ); } #[tokio::test] diff --git a/feature-flags/src/flag_matching.rs b/feature-flags/src/flag_matching.rs new file mode 100644 index 0000000000000..c59b594f32e98 --- /dev/null +++ b/feature-flags/src/flag_matching.rs @@ -0,0 +1,161 @@ +use crate::flag_definitions::{FeatureFlag, FlagGroupType}; +use sha1::{Digest, Sha1}; +use std::fmt::Write; + +#[derive(Debug, PartialEq, Eq)] +pub struct FeatureFlagMatch { + pub matches: bool, + pub variant: Option, + //reason + //condition_index + //payload +} + +// TODO: Rework FeatureFlagMatcher - python has a pretty awkward interface, where we pass in all flags, and then again +// the flag to match. I don't think there's any reason anymore to store the flags in the matcher, since we can just +// pass the flag to match directly to the get_match method. This will also make the matcher more stateless. +// Potentially, we could also make the matcher a long-lived object, with caching for group keys and such. +// It just takes in the flag and distinct_id and returns the match... +// Or, make this fully stateless +// and have a separate cache struct for caching group keys, cohort definitions, etc. - and check size, if we can keep it in memory +// for all teams. If not, we can have a LRU cache, or a cache that stores only the most recent N keys. +// But, this can be a future refactor, for now just focusing on getting the basic matcher working, write lots and lots of tests +// and then we can easily refactor stuff around. +#[derive(Debug)] +pub struct FeatureFlagMatcher { + // pub flags: Vec, + pub distinct_id: String, +} + +const LONG_SCALE: u64 = 0xfffffffffffffff; + +impl FeatureFlagMatcher { + pub fn new(distinct_id: String) -> Self { + FeatureFlagMatcher { + // flags, + distinct_id, + } + } + + pub fn get_match(&self, feature_flag: &FeatureFlag) -> FeatureFlagMatch { + if self.hashed_identifier(feature_flag).is_none() { + return FeatureFlagMatch { + matches: false, + variant: None, + }; + } + + // TODO: super groups for early access + // TODO: Variant overrides condition sort + + for (index, condition) in feature_flag.get_conditions().iter().enumerate() { + let (is_match, _evaluation_reason) = + self.is_condition_match(feature_flag, condition, index); + + if is_match { + // TODO: This is a bit awkward, we should handle overrides only when variants exist. + let variant = match condition.variant.clone() { + Some(variant_override) => { + if feature_flag + .get_variants() + .iter() + .any(|v| v.key == variant_override) + { + Some(variant_override) + } else { + self.get_matching_variant(feature_flag) + } + } + None => self.get_matching_variant(feature_flag), + }; + + // let payload = self.get_matching_payload(is_match, variant, feature_flag); + return FeatureFlagMatch { + matches: true, + variant, + }; + } + } + FeatureFlagMatch { + matches: false, + variant: None, + } + } + + pub fn is_condition_match( + &self, + feature_flag: &FeatureFlag, + condition: &FlagGroupType, + _index: usize, + ) -> (bool, String) { + let rollout_percentage = condition.rollout_percentage.unwrap_or(100.0); + let mut condition_match = true; + if condition.properties.is_some() { + // TODO: Handle matching conditions + if !condition.properties.as_ref().unwrap().is_empty() { + condition_match = false; + } + } + + if !condition_match { + return (false, "NO_CONDITION_MATCH".to_string()); + } else if rollout_percentage == 100.0 { + // TODO: Check floating point schenanigans if any + return (true, "CONDITION_MATCH".to_string()); + } + + if self.get_hash(feature_flag, "") > (rollout_percentage / 100.0) { + return (false, "OUT_OF_ROLLOUT_BOUND".to_string()); + } + + (true, "CONDITION_MATCH".to_string()) + } + + pub fn hashed_identifier(&self, feature_flag: &FeatureFlag) -> Option { + if feature_flag.get_group_type_index().is_none() { + // TODO: Use hash key overrides for experience continuity + Some(self.distinct_id.clone()) + } else { + // TODO: Handle getting group key + Some("".to_string()) + } + } + + /// This function takes a identifier and a feature flag key and returns a float between 0 and 1. + /// Given the same identifier and key, it'll always return the same float. These floats are + /// uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic + /// we can do _hash(key, identifier) < 0.2 + pub fn get_hash(&self, feature_flag: &FeatureFlag, salt: &str) -> f64 { + // check if hashed_identifier is None + let hashed_identifier = self + .hashed_identifier(feature_flag) + .expect("hashed_identifier is None when computing hash"); + let hash_key = format!("{}.{}{}", feature_flag.key, hashed_identifier, salt); + let mut hasher = Sha1::new(); + hasher.update(hash_key.as_bytes()); + let result = hasher.finalize(); + // :TRICKY: Convert the first 15 characters of the digest to a hexadecimal string + // not sure if this is correct, padding each byte as 2 characters + let hex_str: String = result.iter().fold(String::new(), |mut acc, byte| { + let _ = write!(acc, "{:02x}", byte); + acc + })[..15] + .to_string(); + let hash_val = u64::from_str_radix(&hex_str, 16).unwrap(); + + hash_val as f64 / LONG_SCALE as f64 + } + + pub fn get_matching_variant(&self, feature_flag: &FeatureFlag) -> Option { + let hash = self.get_hash(feature_flag, "variant"); + let mut total_percentage = 0.0; + + for variant in feature_flag.get_variants() { + total_percentage += variant.rollout_percentage / 100.0; + if hash < total_percentage { + return Some(variant.key.clone()); + } + } + None + } +} diff --git a/feature-flags/src/lib.rs b/feature-flags/src/lib.rs index 0352c21c3382a..edc2a2963ff3b 100644 --- a/feature-flags/src/lib.rs +++ b/feature-flags/src/lib.rs @@ -1,6 +1,7 @@ pub mod api; pub mod config; pub mod flag_definitions; +pub mod flag_matching; pub mod redis; pub mod router; pub mod server; diff --git a/feature-flags/src/team.rs b/feature-flags/src/team.rs index ac62ea9ba55cb..e872aa477968f 100644 --- a/feature-flags/src/team.rs +++ b/feature-flags/src/team.rs @@ -42,6 +42,7 @@ impl Team { } })?; + // TODO: Consider an LRU cache for teams as well, with small TTL to skip redis/pg lookups let team: Team = serde_json::from_str(&serialized_team).map_err(|e| { tracing::error!("failed to parse data to team: {}", e); FlagError::DataParsingError diff --git a/feature-flags/src/test_utils.rs b/feature-flags/src/test_utils.rs index 0cefb7eda8e7c..92bc8a4ff4494 100644 --- a/feature-flags/src/test_utils.rs +++ b/feature-flags/src/test_utils.rs @@ -3,7 +3,7 @@ use serde_json::json; use std::sync::Arc; use crate::{ - flag_definitions, + flag_definitions::{self, FeatureFlag}, redis::{Client, RedisClient}, team::{self, Team}, }; @@ -91,3 +91,36 @@ pub fn setup_redis_client(url: Option) -> Arc { let client = RedisClient::new(redis_url).expect("Failed to create redis client"); Arc::new(client) } + +pub fn create_flag_from_json(json_value: Option) -> Vec { + let payload = match json_value { + Some(value) => value, + None => json!([{ + "id": 1, + "key": "flag1", + "name": "flag1 description", + "active": true, + "deleted": false, + "team_id": 1, + "filters": { + "groups": [ + { + "properties": [ + { + "key": "email", + "value": "a@b.com", + "type": "person", + }, + ], + "rollout_percentage": 50, + }, + ], + }, + }]) + .to_string(), + }; + + let flags: Vec = + serde_json::from_str(&payload).expect("Failed to parse data to flags list"); + flags +} diff --git a/feature-flags/tests/test_flag_matching_consistency.rs b/feature-flags/tests/test_flag_matching_consistency.rs new file mode 100644 index 0000000000000..4a24b0e16d50e --- /dev/null +++ b/feature-flags/tests/test_flag_matching_consistency.rs @@ -0,0 +1,1209 @@ +/// These tests are common between all libraries doing local evaluation of feature flags. +/// This ensures there are no mismatches between implementations. +use feature_flags::flag_matching::{FeatureFlagMatch, FeatureFlagMatcher}; + +use feature_flags::test_utils::create_flag_from_json; +use serde_json::json; + +#[test] +fn it_is_consistent_with_rollout_calculation_for_simple_flags() { + let flags = create_flag_from_json(Some( + json!([{ + "id": 1, + "key": "simple-flag", + "name": "Simple flag", + "active": true, + "deleted": false, + "team_id": 1, + "filters": { + "groups": [ + { + "properties": [], + "rollout_percentage": 45, + }, + ], + }, + }]) + .to_string(), + )); + + let results = vec![ + false, true, true, false, true, false, false, true, false, true, false, true, true, false, + true, false, false, false, true, true, false, true, false, false, true, false, true, true, + false, false, false, true, true, true, true, false, false, false, false, false, false, + true, true, false, true, true, false, false, false, true, true, false, false, false, false, + true, false, true, false, true, false, true, true, false, true, false, true, false, true, + true, false, false, true, false, false, true, false, true, false, false, true, false, + false, false, true, true, false, true, true, false, true, true, true, true, true, false, + true, true, false, false, true, true, true, true, false, false, true, false, true, true, + true, false, false, false, false, false, true, false, false, true, true, true, false, + false, true, false, true, false, false, true, false, false, false, false, false, false, + false, false, true, true, false, false, true, false, false, true, true, false, false, true, + false, true, false, true, true, true, false, false, false, true, false, false, false, + false, true, true, false, true, true, false, true, false, true, true, false, true, false, + true, true, true, false, true, false, false, true, true, false, true, false, true, true, + false, false, true, true, true, true, false, true, true, false, false, true, false, true, + false, false, true, true, false, true, false, true, false, false, false, false, false, + false, false, true, false, true, true, false, false, true, false, true, false, false, + false, true, false, true, false, false, false, true, false, false, true, false, true, true, + false, false, false, false, true, false, false, false, false, false, false, false, false, + false, false, false, false, false, true, true, false, true, false, true, true, false, true, + false, true, false, false, false, true, true, true, true, false, false, false, false, + false, true, true, true, false, false, true, true, false, false, false, false, false, true, + false, true, true, true, true, false, true, true, true, false, false, true, false, true, + false, false, true, true, true, false, true, false, false, false, true, true, false, true, + false, true, false, true, true, true, true, true, false, false, true, false, true, false, + true, true, true, false, true, false, true, true, false, true, true, true, true, true, + false, false, false, false, false, true, false, true, false, false, true, true, false, + false, false, true, false, true, true, true, true, false, false, false, false, true, true, + false, false, true, true, false, true, true, true, true, false, true, true, true, false, + false, true, true, false, false, true, false, false, true, false, false, false, false, + false, false, false, false, false, false, true, true, false, false, true, false, false, + true, false, true, false, false, true, false, false, false, false, false, false, true, + false, false, false, false, false, false, false, false, false, true, true, true, false, + false, false, true, false, true, false, false, false, true, false, false, false, false, + false, false, false, true, false, false, false, false, false, false, false, false, true, + false, true, false, true, true, true, false, false, false, true, true, true, false, true, + false, true, true, false, false, false, true, false, false, false, false, true, false, + true, false, true, true, false, true, false, false, false, true, false, false, true, true, + false, true, false, false, false, false, false, false, true, true, false, false, true, + false, false, true, true, true, false, false, false, true, false, false, false, false, + true, false, true, false, false, false, true, false, true, true, false, true, false, true, + false, true, false, false, true, false, false, true, false, true, false, true, false, true, + false, false, true, true, true, true, false, true, false, false, false, false, false, true, + false, false, true, false, false, true, true, false, false, false, false, true, true, true, + false, false, true, false, false, true, true, true, true, false, false, false, true, false, + false, false, true, false, false, true, true, true, true, false, false, true, true, false, + true, false, true, false, false, true, true, false, true, true, true, true, false, false, + true, false, false, true, true, false, true, false, true, false, false, true, false, false, + false, false, true, true, true, false, true, false, false, true, false, false, true, false, + false, false, false, true, false, true, false, true, true, false, false, true, false, true, + true, true, false, false, false, false, true, true, false, true, false, false, false, true, + false, false, false, false, true, true, true, false, false, false, true, true, true, true, + false, true, true, false, true, true, true, false, true, false, false, true, false, true, + true, true, true, false, true, false, true, false, true, false, false, true, true, false, + false, true, false, true, false, false, false, false, true, false, true, false, false, + false, true, true, true, false, false, false, true, false, true, true, false, false, false, + false, false, true, false, true, false, false, true, true, false, true, true, true, true, + false, false, true, false, false, true, false, true, false, true, true, false, false, + false, true, false, true, true, false, false, false, true, false, true, false, true, true, + false, true, false, false, true, false, false, false, true, true, true, false, false, + false, false, false, true, false, false, true, true, true, true, true, false, false, false, + false, false, false, false, false, true, true, true, false, false, true, true, false, true, + true, false, true, false, true, false, false, false, true, false, false, true, false, + false, true, true, true, true, false, false, true, false, true, true, false, false, true, + false, false, true, true, false, true, false, false, true, true, true, false, false, false, + false, false, true, false, true, false, false, false, false, false, true, true, false, + true, true, true, false, false, false, false, true, true, true, true, false, true, true, + false, true, false, true, false, true, false, false, false, false, true, true, true, true, + false, false, true, false, true, true, false, false, false, false, false, false, true, + false, true, false, true, true, false, false, true, true, true, true, false, false, true, + false, true, true, false, false, true, true, true, false, true, false, false, true, true, + false, false, false, true, false, false, true, false, false, false, true, true, true, true, + false, true, false, true, false, true, false, true, false, false, true, false, false, true, + false, true, true, + ]; + + for i in 0..1000 { + let distinct_id = format!("distinct_id_{}", i); + + let feature_flag_match = FeatureFlagMatcher::new(distinct_id).get_match(&flags[0]); + + if results[i] { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: true, + variant: None, + } + ); + } else { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: false, + variant: None, + } + ); + } + } +} + +#[test] +fn it_is_consistent_with_rollout_calculation_for_multivariate_flags() { + let flags = create_flag_from_json(Some( + json!([{ + "id": 1, + "key": "multivariate-flag", + "name": "Multivariate flag", + "active": true, + "deleted": false, + "team_id": 1, + "filters": { + "groups": [ + { + "properties": [], + "rollout_percentage": 55, + }, + ], + "multivariate": { + "variants": [ + { + "key": "first-variant", + "name": "First Variant", + "rollout_percentage": 50, + }, + { + "key": "second-variant", + "name": "Second Variant", + "rollout_percentage": 20, + }, + { + "key": "third-variant", + "name": "Third Variant", + "rollout_percentage": 20, + }, + { + "key": "fourth-variant", + "name": "Fourth Variant", + "rollout_percentage": 5, + }, + { + "key": "fifth-variant", + "name": "Fifth Variant", + "rollout_percentage": 5, + }, + ], + }, + }, + }]) + .to_string(), + )); + + let results = vec![ + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("fifth-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("fourth-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("fourth-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + None, + None, + Some("third-variant".to_string()), + Some("fourth-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + None, + Some("fourth-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("fourth-variant".to_string()), + None, + None, + None, + Some("fourth-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + Some("fifth-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + None, + None, + None, + None, + Some("fourth-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("fifth-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fifth-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + Some("second-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("fourth-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("fifth-variant".to_string()), + None, + None, + None, + Some("second-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("second-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + None, + None, + Some("first-variant".to_string()), + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("third-variant".to_string()), + Some("second-variant".to_string()), + Some("third-variant".to_string()), + None, + None, + Some("third-variant".to_string()), + Some("first-variant".to_string()), + None, + Some("first-variant".to_string()), + ]; + + for i in 0..1000 { + let distinct_id = format!("distinct_id_{}", i); + + let feature_flag_match = FeatureFlagMatcher::new(distinct_id).get_match(&flags[0]); + + if results[i].is_some() { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: true, + variant: results[i].clone(), + } + ); + } else { + assert_eq!( + feature_flag_match, + FeatureFlagMatch { + matches: false, + variant: None, + } + ); + } + } +} From f6569a710a8a505aa4d170abc36c0bb49dfa3bdc Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Mon, 10 Jun 2024 10:45:23 +0100 Subject: [PATCH 247/249] feat(flags): Add basic property matching (#47) --- Cargo.lock | 9 +- feature-flags/Cargo.toml | 1 + feature-flags/README.md | 36 + feature-flags/src/flag_definitions.rs | 5 +- feature-flags/src/flag_matching.rs | 1 - feature-flags/src/lib.rs | 1 + feature-flags/src/property_matching.rs | 1647 ++++++++++++++++++++++++ 7 files changed, 1694 insertions(+), 6 deletions(-) create mode 100644 feature-flags/README.md create mode 100644 feature-flags/src/property_matching.rs diff --git a/Cargo.lock b/Cargo.lock index b9f226bb08bb9..804ab47416080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -705,6 +705,7 @@ dependencies = [ "once_cell", "rand", "redis", + "regex", "reqwest 0.12.3", "serde", "serde-pickle", @@ -1033,9 +1034,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.5" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -2250,9 +2251,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", diff --git a/feature-flags/Cargo.toml b/feature-flags/Cargo.toml index 4993930362857..08ff21eaed0d8 100644 --- a/feature-flags/Cargo.toml +++ b/feature-flags/Cargo.toml @@ -26,6 +26,7 @@ serde_json = { workspace = true } thiserror = { workspace = true } serde-pickle = { version = "1.1.1"} sha1 = "0.10.6" +regex = "1.10.4" [lints] workspace = true diff --git a/feature-flags/README.md b/feature-flags/README.md new file mode 100644 index 0000000000000..1c9500900aade --- /dev/null +++ b/feature-flags/README.md @@ -0,0 +1,36 @@ + +# Testing + +``` +cargo test --package feature-flags +``` + +### To watch changes + +``` +brew install cargo-watch +``` + +and then run: + +``` +cargo watch -x test --package feature-flags +``` + +To run a specific test: + +``` +cargo watch -x "test --package feature-flags --lib -- property_matching::tests::test_match_properties_math_operators --exact --show-output" +``` + +# Running + +``` +RUST_LOG=debug cargo run --bin feature-flags +``` + +# Format code + +``` +cargo fmt --package feature-flags +``` \ No newline at end of file diff --git a/feature-flags/src/flag_definitions.rs b/feature-flags/src/flag_definitions.rs index 1f4582c606bd7..fbbd0445b5998 100644 --- a/feature-flags/src/flag_definitions.rs +++ b/feature-flags/src/flag_definitions.rs @@ -16,7 +16,7 @@ pub const TEAM_FLAGS_CACHE_PREFIX: &str = "posthog:1:team_feature_flags_"; #[derive(Debug, Deserialize)] pub enum GroupTypeIndex {} -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(rename_all = "snake_case")] pub enum OperatorType { Exact, @@ -39,6 +39,9 @@ pub enum OperatorType { #[derive(Debug, Clone, Deserialize)] pub struct PropertyFilter { pub key: String, + // TODO: Probably need a default for value? + // incase operators like is_set, is_not_set are used + // not guaranteed to have a value, if say created via api pub value: serde_json::Value, pub operator: Option, #[serde(rename = "type")] diff --git a/feature-flags/src/flag_matching.rs b/feature-flags/src/flag_matching.rs index c59b594f32e98..510fc153dc87a 100644 --- a/feature-flags/src/flag_matching.rs +++ b/feature-flags/src/flag_matching.rs @@ -135,7 +135,6 @@ impl FeatureFlagMatcher { hasher.update(hash_key.as_bytes()); let result = hasher.finalize(); // :TRICKY: Convert the first 15 characters of the digest to a hexadecimal string - // not sure if this is correct, padding each byte as 2 characters let hex_str: String = result.iter().fold(String::new(), |mut acc, byte| { let _ = write!(acc, "{:02x}", byte); acc diff --git a/feature-flags/src/lib.rs b/feature-flags/src/lib.rs index edc2a2963ff3b..7f03747b9ee6d 100644 --- a/feature-flags/src/lib.rs +++ b/feature-flags/src/lib.rs @@ -2,6 +2,7 @@ pub mod api; pub mod config; pub mod flag_definitions; pub mod flag_matching; +pub mod property_matching; pub mod redis; pub mod router; pub mod server; diff --git a/feature-flags/src/property_matching.rs b/feature-flags/src/property_matching.rs new file mode 100644 index 0000000000000..9f7d9ea173963 --- /dev/null +++ b/feature-flags/src/property_matching.rs @@ -0,0 +1,1647 @@ +use std::collections::HashMap; + +use crate::flag_definitions::{OperatorType, PropertyFilter}; +use regex::Regex; +use serde_json::Value; + +#[derive(Debug, PartialEq, Eq)] +pub enum FlagMatchingError { + ValidationError(String), + MissingProperty(String), + InconclusiveOperatorMatch, + InvalidRegexPattern, +} + +pub fn to_string_representation(value: &Value) -> String { + if value.is_string() { + return value + .as_str() + .expect("string slice should always exist for string value") + .to_string(); + } + value.to_string() +} + +pub fn to_f64_representation(value: &Value) -> Option { + if value.is_number() { + return value.as_f64(); + } + to_string_representation(value).parse::().ok() +} + +pub fn match_property( + property: &PropertyFilter, + matching_property_values: &HashMap, + partial_props: bool, +) -> Result { + // only looks for matches where key exists in override_property_values + // doesn't support operator is_not_set with partial_props + + if partial_props && !matching_property_values.contains_key(&property.key) { + return Err(FlagMatchingError::MissingProperty(format!( + "can't match properties without a value. Missing property: {}", + property.key + ))); + } + + let key = &property.key; + let operator = property.operator.clone().unwrap_or(OperatorType::Exact); + let value = &property.value; + let match_value = matching_property_values.get(key); + + match operator { + OperatorType::Exact | OperatorType::IsNot => { + let compute_exact_match = |value: &Value, override_value: &Value| -> bool { + if is_truthy_or_falsy_property_value(value) { + // Do boolean handling, such that passing in "true" or "True" or "false" or "False" as matching value is equivalent + let truthy = is_truthy_property_value(value); + return override_value.to_string().to_lowercase() + == truthy.to_string().to_lowercase(); + } + + if value.is_array() { + return value + .as_array() + .expect("expected array value") + .iter() + .map(|v| to_string_representation(v).to_lowercase()) + .collect::>() + .contains(&to_string_representation(override_value).to_lowercase()); + } + to_string_representation(value).to_lowercase() + == to_string_representation(override_value).to_lowercase() + }; + + if let Some(match_value) = match_value { + if operator == OperatorType::Exact { + Ok(compute_exact_match(value, match_value)) + } else { + Ok(!compute_exact_match(value, match_value)) + } + } else { + Ok(false) + } + } + OperatorType::IsSet => Ok(matching_property_values.contains_key(key)), + OperatorType::IsNotSet => { + if partial_props { + if matching_property_values.contains_key(key) { + Ok(false) + } else { + Err(FlagMatchingError::InconclusiveOperatorMatch) + } + } else { + Ok(!matching_property_values.contains_key(key)) + } + } + OperatorType::Icontains | OperatorType::NotIcontains => { + if let Some(match_value) = match_value { + // TODO: Check eq_ignore_ascii_case and to_ascii_lowercase + // see https://doc.rust-lang.org/std/string/struct.String.html#method.to_lowercase + // do we want to lowercase non-ascii stuff? + let is_contained = to_string_representation(match_value) + .to_lowercase() + .contains(&to_string_representation(value).to_lowercase()); + + if operator == OperatorType::Icontains { + Ok(is_contained) + } else { + Ok(!is_contained) + } + } else { + // When value doesn't exist, it's not a match + Ok(false) + } + } + OperatorType::Regex | OperatorType::NotRegex => { + if match_value.is_none() { + return Ok(false); + } + + let pattern = match Regex::new(&to_string_representation(value)) { + Ok(pattern) => pattern, + Err(_) => return Ok(false), + //TODO: Should we return Err here and handle elsewhere? + //Err(FlagMatchingError::InvalidRegexPattern) + // python just returns false here + }; + let haystack = to_string_representation(match_value.unwrap_or(&Value::Null)); + let match_ = pattern.find(&haystack); + + if operator == OperatorType::Regex { + Ok(match_.is_some()) + } else { + Ok(match_.is_none()) + } + } + OperatorType::Gt | OperatorType::Gte | OperatorType::Lt | OperatorType::Lte => { + if match_value.is_none() { + return Ok(false); + } + // TODO: Move towards only numeric matching of these operators??? + + let compare = |lhs: f64, rhs: f64, operator: OperatorType| -> bool { + match operator { + OperatorType::Gt => lhs > rhs, + OperatorType::Gte => lhs >= rhs, + OperatorType::Lt => lhs < rhs, + OperatorType::Lte => lhs <= rhs, + _ => false, + } + }; + + let parsed_value = match to_f64_representation(match_value.unwrap_or(&Value::Null)) { + Some(parsed_value) => parsed_value, + None => { + return Err(FlagMatchingError::ValidationError( + "value is not a number".to_string(), + )) + } + }; + + if let Some(override_value) = to_f64_representation(value) { + Ok(compare(parsed_value, override_value, operator)) + } else { + Err(FlagMatchingError::ValidationError( + "override value is not a number".to_string(), + )) + } + } + OperatorType::IsDateExact | OperatorType::IsDateAfter | OperatorType::IsDateBefore => { + // TODO: Handle date operators + Ok(false) + // let parsed_date = determine_parsed_date_for_property_matching(match_value); + + // if parsed_date.is_none() { + // return Ok(false); + // } + + // if let Some(override_value) = value.as_str() { + // let override_date = match parser::parse(override_value) { + // Ok(override_date) => override_date, + // Err(_) => return Ok(false), + // }; + + // match operator { + // OperatorType::IsDateBefore => Ok(override_date < parsed_date.unwrap()), + // OperatorType::IsDateAfter => Ok(override_date > parsed_date.unwrap()), + // _ => Ok(false), + // } + // } else { + // Ok(false) + // } + } + } +} + +fn is_truthy_or_falsy_property_value(value: &Value) -> bool { + if value.is_boolean() { + return true; + } + + if value.is_string() { + let parsed_value = value + .as_str() + .expect("expected string value") + .to_lowercase(); + return parsed_value == "true" || parsed_value == "false"; + } + + if value.is_array() { + return value + .as_array() + .expect("expected array value") + .iter() + .all(is_truthy_or_falsy_property_value); + } + + false +} + +fn is_truthy_property_value(value: &Value) -> bool { + if value.is_boolean() { + return value.as_bool().expect("expected boolean value"); + } + + if value.is_string() { + let parsed_value = value + .as_str() + .expect("expected string value") + .to_lowercase(); + return parsed_value == "true"; + } + + if value.is_array() { + return value + .as_array() + .expect("expected array value") + .iter() + .all(is_truthy_property_value); + } + + false +} + +/// Copy of https://github.com/PostHog/posthog/blob/master/posthog/queries/test/test_base.py#L35 +/// with some modifications to match Rust's behavior +/// and to test the match_property function +#[cfg(test)] +mod test_match_properties { + use super::*; + use serde_json::json; + + #[test] + fn test_match_properties_exact_with_partial_props() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!("value"), + operator: None, + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(""))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + false + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key2".to_string(), json!("value"))]), + true + ) + .is_err(), + true + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key2".to_string(), json!("value"))]), + true + ) + .err() + .expect("expected match to exist"), + FlagMatchingError::MissingProperty( + "can't match properties without a value. Missing property: key".to_string() + ) + ); + assert_eq!( + match_property(&property_a, &HashMap::from([]), true).is_err(), + true + ); + + let property_b = PropertyFilter { + key: "key".to_string(), + value: json!("value"), + operator: Some(OperatorType::Exact), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("value"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + false + ); + + let property_c = PropertyFilter { + key: "key".to_string(), + value: json!(["value1", "value2", "value3"]), + operator: Some(OperatorType::Exact), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value1"))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value3"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value4"))]), + true + ) + .expect("expected match to exist"), + false + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key2".to_string(), json!("value"))]), + true + ) + .is_err(), + true + ); + } + + #[test] + fn test_match_properties_is_not() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!("value"), + operator: Some(OperatorType::IsNot), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(""))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + true + ); + + // partial mode returns error when key doesn't exist + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key2".to_string(), json!("value1"))]), + true + ) + .is_err(), + true + ); + + let property_c = PropertyFilter { + key: "key".to_string(), + value: json!(["value1", "value2", "value3"]), + operator: Some(OperatorType::IsNot), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value4"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value5"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value6"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!(""))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + false + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value3"))]), + true + ) + .expect("expected match to exist"), + false + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value1"))]), + true + ) + .expect("expected match to exist"), + false + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key2".to_string(), json!("value1"))]), + true + ) + .is_err(), + true + ); + } + + #[test] + fn test_match_properties_is_set() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!("value"), + operator: Some(OperatorType::IsSet), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(""))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key2".to_string(), json!("value1"))]), + true + ) + .is_err(), + true + ); + + assert_eq!( + match_property(&property_a, &HashMap::from([]), true).is_err(), + true + ); + } + + #[test] + fn test_match_properties_icontains() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!("valUe"), + operator: Some(OperatorType::Icontains), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value3"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("vaLue4"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("343tfvalue5"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("Alakazam"))]), + true + ) + .expect("expected match to exist"), + false + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(123))]), + true + ) + .expect("expected match to exist"), + false + ); + + let property_b = PropertyFilter { + key: "key".to_string(), + value: json!("3"), + operator: Some(OperatorType::Icontains), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("3"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!(323))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("val3"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("three"))]), + true + ) + .expect("expected match to exist"), + false + ); + } + + #[test] + fn test_match_properties_regex() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!(r"\.com$"), + operator: Some(OperatorType::Regex), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value.com"))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("value2.com"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(".com343tfvalue5"))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("Alakazam"))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(123))]), + true + ) + .expect("expected match to exist"), + false + ); + + let property_b = PropertyFilter { + key: "key".to_string(), + value: json!("3"), + operator: Some(OperatorType::Regex), + prop_type: "person".to_string(), + group_type_index: None, + }; + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("3"))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!(323))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("val3"))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("three"))]), + true + ) + .expect("expected match to exist"), + false + ); + + // invalid regex + let property_c = PropertyFilter { + key: "key".to_string(), + value: json!(r"?*"), + operator: Some(OperatorType::Regex), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value"))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("value2"))]), + true + ) + .expect("expected match to exist"), + false + ); + + // non string value + let property_d = PropertyFilter { + key: "key".to_string(), + value: json!(4), + operator: Some(OperatorType::Regex), + prop_type: "person".to_string(), + group_type_index: None, + }; + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!("4"))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!(4))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!("value"))]), + true + ) + .expect("expected match to exist"), + false + ); + } + + #[test] + fn test_match_properties_math_operators() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!(1), + operator: Some(OperatorType::Gt), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(2))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(3))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(0))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(-1))]), + true + ) + .expect("expected match to exist"), + false + ); + + // # we handle type mismatches so this should be true + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("23"))]), + true + ) + .expect("expected match to exist"), + true + ); + + let property_b = PropertyFilter { + key: "key".to_string(), + value: json!(1), + operator: Some(OperatorType::Lt), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!(0))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!(-1))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!(-3))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!(1))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("1"))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!("3"))]), + true + ) + .expect("expected match to exist"), + false + ); + + let property_c = PropertyFilter { + key: "key".to_string(), + value: json!(1), + operator: Some(OperatorType::Gte), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!(1))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!(2))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!(0))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!(-1))]), + true + ) + .expect("expected match to exist"), + false + ); + // # now we handle type mismatches so this should be true + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("3"))]), + true + ) + .expect("expected match to exist"), + true + ); + + let property_d = PropertyFilter { + key: "key".to_string(), + value: json!("43"), + operator: Some(OperatorType::Lt), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!("41"))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!("42"))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!(42))]), + true + ) + .expect("expected match to exist"), + true + ); + + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!("43"))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!("44"))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!(44))]), + true + ) + .expect("expected match to exist"), + false + ); + + let property_e = PropertyFilter { + key: "key".to_string(), + value: json!("30"), + operator: Some(OperatorType::Lt), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_e, + &HashMap::from([("key".to_string(), json!("29"))]), + true + ) + .expect("expected match to exist"), + true + ); + + // # depending on the type of override, we adjust type comparison + // This is wonky, do we want to continue this behavior? :/ + // TODO: Come back to this + // assert_eq!( + // match_property( + // &property_e, + // &HashMap::from([("key".to_string(), json!("100"))]), + // true + // ) + // .expect("expected match to exist"), + // true + // ); + assert_eq!( + match_property( + &property_e, + &HashMap::from([("key".to_string(), json!(100))]), + true + ) + .expect("expected match to exist"), + false + ); + + // let property_f = PropertyFilter { + // key: "key".to_string(), + // value: json!("123aloha"), + // operator: Some(OperatorType::Gt), + // prop_type: "person".to_string(), + // group_type_index: None, + // }; + + // TODO: This test fails because 123aloha is not a number + // and currently we don't support string comparison.. + // assert_eq!( + // match_property( + // &property_f, + // &HashMap::from([("key".to_string(), json!("123"))]), + // true + // ) + // .expect("expected match to exist"), + // false + // ); + // assert_eq!( + // match_property( + // &property_f, + // &HashMap::from([("key".to_string(), json!(122))]), + // true + // ) + // .expect("expected match to exist"), + // false + // ); + + // # this turns into a string comparison + // TODO: Fix + // assert_eq!( + // match_property( + // &property_f, + // &HashMap::from([("key".to_string(), json!(129))]), + // true + // ) + // .expect("expected match to exist"), + // true + // ); + } + + #[test] + fn test_none_property_value_with_all_operators() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!("null"), + operator: Some(OperatorType::IsNot), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + false + ); + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key".to_string(), json!("non"))]), + true + ) + .expect("expected match to exist"), + true + ); + + let property_b = PropertyFilter { + key: "key".to_string(), + value: json!(null), + operator: Some(OperatorType::IsSet), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_b, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + true + ); + + let property_c = PropertyFilter { + key: "key".to_string(), + value: json!("nu"), + operator: Some(OperatorType::Icontains), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + true + ); + assert_eq!( + match_property( + &property_c, + &HashMap::from([("key".to_string(), json!("smh"))]), + true + ) + .expect("expected match to exist"), + false + ); + + let property_d = PropertyFilter { + key: "key".to_string(), + value: json!("Nu"), + operator: Some(OperatorType::Regex), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_d, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + false + ); + + let property_d_upper_case = PropertyFilter { + key: "key".to_string(), + value: json!("Nu"), + operator: Some(OperatorType::Regex), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_d_upper_case, + &HashMap::from([("key".to_string(), json!(null))]), + true + ) + .expect("expected match to exist"), + false + ); + + // TODO: Fails because not a number + // let property_e = PropertyFilter { + // key: "key".to_string(), + // value: json!(1), + // operator: Some(OperatorType::Gt), + // prop_type: "person".to_string(), + // group_type_index: None, + // }; + + // assert_eq!( + // match_property(&property_e, &HashMap::from([("key".to_string(), json!(null))]), true) + // .expect("expected match to exist"), + // true + // ); + } + + #[test] + fn test_match_properties_all_operators_with_full_props() { + let property_a = PropertyFilter { + key: "key".to_string(), + value: json!("value"), + operator: None, + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_a, + &HashMap::from([("key2".to_string(), json!("value"))]), + false + ) + .expect("Expected no errors with full props mode for non-existent keys"), + false + ); + assert_eq!( + match_property(&property_a, &HashMap::from([]), false), + Ok(false) + ); + + let property_exact = PropertyFilter { + key: "key".to_string(), + value: json!(["value1", "value2", "value3"]), + operator: Some(OperatorType::Exact), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_exact, + &HashMap::from([("key2".to_string(), json!("value"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_is_set = PropertyFilter { + key: "key".to_string(), + value: json!("value"), + operator: Some(OperatorType::IsSet), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_is_set, + &HashMap::from([("key2".to_string(), json!("value"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_is_not_set = PropertyFilter { + key: "key".to_string(), + value: json!(null), + operator: Some(OperatorType::IsNotSet), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_is_not_set, + &HashMap::from([("key2".to_string(), json!("value"))]), + false + ) + .expect("Expected no errors with full props mode"), + true + ); + assert_eq!( + match_property( + &property_is_not_set, + &HashMap::from([("key".to_string(), json!("value"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + // is not set with partial props returns false when key exists + assert_eq!( + match_property( + &property_is_not_set, + &HashMap::from([("key".to_string(), json!("value"))]), + true + ) + .expect("Expected no errors with full props mode"), + false + ); + // is not set returns error when key doesn't exist + assert_eq!( + match_property( + &property_is_not_set, + &HashMap::from([("key2".to_string(), json!("value"))]), + true + ) + .is_err(), + true + ); + + let property_icontains = PropertyFilter { + key: "key".to_string(), + value: json!("valUe"), + operator: Some(OperatorType::Icontains), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_icontains, + &HashMap::from([("key2".to_string(), json!("value"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_not_icontains = PropertyFilter { + key: "key".to_string(), + value: json!("valUe"), + operator: Some(OperatorType::NotIcontains), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_not_icontains, + &HashMap::from([("key2".to_string(), json!("value"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_regex = PropertyFilter { + key: "key".to_string(), + value: json!(r"\.com$"), + operator: Some(OperatorType::Regex), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_regex, + &HashMap::from([("key2".to_string(), json!("value.com"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_not_regex = PropertyFilter { + key: "key".to_string(), + value: json!(r"\.com$"), + operator: Some(OperatorType::NotRegex), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_not_regex, + &HashMap::from([("key2".to_string(), json!("value.com"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_gt = PropertyFilter { + key: "key".to_string(), + value: json!(1), + operator: Some(OperatorType::Gt), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_gt, + &HashMap::from([("key2".to_string(), json!(2))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_gte = PropertyFilter { + key: "key".to_string(), + value: json!(1), + operator: Some(OperatorType::Gte), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_gte, + &HashMap::from([("key2".to_string(), json!(2))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_lt = PropertyFilter { + key: "key".to_string(), + value: json!(1), + operator: Some(OperatorType::Lt), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_lt, + &HashMap::from([("key2".to_string(), json!(0))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + let property_lte = PropertyFilter { + key: "key".to_string(), + value: json!(1), + operator: Some(OperatorType::Lte), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_lte, + &HashMap::from([("key2".to_string(), json!(0))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + + // TODO: Handle date operators + let property_is_date_before = PropertyFilter { + key: "key".to_string(), + value: json!("2021-01-01"), + operator: Some(OperatorType::IsDateBefore), + prop_type: "person".to_string(), + group_type_index: None, + }; + + assert_eq!( + match_property( + &property_is_date_before, + &HashMap::from([("key2".to_string(), json!("2021-01-02"))]), + false + ) + .expect("Expected no errors with full props mode"), + false + ); + } +} From ee6cd6d9c2c71ca0fdf056cd776da11669615bb1 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Tue, 11 Jun 2024 16:30:21 +0100 Subject: [PATCH 248/249] chore: adjust ci for rust services, merge master --- .github/workflows/build-and-deploy-prod.yml | 2 + .github/workflows/ci-hog.yml | 2 +- .github/workflows/container-images-cd.yml | 65 +- .../workflows/rust-docker-build.yml | 21 +- .../workflows/rust-hook-migrator-docker.yml | 18 +- .github/workflows/rust.yml | 192 +++ README.md | 8 +- bin/build-schema-python.sh | 3 +- docker-compose.base.yml | 7 + docker-compose.dev-full.yml | 8 + docker-compose.dev.yml | 19 + ee/clickhouse/models/test/test_cohort.py | 40 +- .../queries/enterprise_cohort_query.py | 2 +- ee/clickhouse/queries/event_query.py | 2 +- .../queries/funnels/funnel_correlation.py | 20 +- ee/clickhouse/queries/groups_join_query.py | 4 +- ...est_session_recording_list_from_filters.py | 8 +- ...sion_recording_list_from_session_replay.py | 8 +- ...el-left-to-right-breakdown-edit--light.png | Bin 179111 -> 177354 bytes .../navigation-3000/navigationLogic.tsx | 9 + frontend/src/lib/api.ts | 33 + .../Cards/InsightCard/InsightCard.scss | 4 + .../components/CodeSnippet/CodeSnippet.tsx | 9 +- .../CommandPalette/DebugCHQueries.tsx | 4 +- .../CommandPalette/commandPaletteLogic.tsx | 12 + .../Errors/ErrorDisplay.stories.tsx | 177 ++- .../lib/components/Errors/ErrorDisplay.tsx | 10 +- .../components/PropertyValue.tsx | 2 +- .../lib/components/PropertyFilters/utils.ts | 45 +- .../SocialLoginButton/SocialLoginButton.tsx | 25 +- .../lib/components/Support/supportLogic.ts | 13 +- .../TaxonomicFilter/InfiniteSelectResults.tsx | 2 +- .../TaxonomicFilter/InlineHogQLEditor.tsx | 2 +- .../TaxonomicFilter/taxonomicFilterLogic.tsx | 8 + .../lib/components/TaxonomicFilter/types.ts | 4 +- .../TimeSensitiveAuthentication.tsx | 22 +- .../UniversalFilters.stories.tsx | 5 +- .../UniversalFilters/UniversalFilters.tsx | 15 +- .../universalFiltersLogic.test.ts | 5 +- .../UniversalFilters/universalFiltersLogic.ts | 32 +- frontend/src/lib/constants.tsx | 5 + frontend/src/lib/taxonomy.tsx | 11 + frontend/src/lib/utils/apiHost.ts | 3 +- .../src/models/propertyDefinitionsModel.ts | 25 +- .../queries/nodes/InsightViz/InsightViz.scss | 12 +- .../AndOrFilterSelect.tsx | 5 +- .../src/queries/nodes/InsightViz/utils.ts | 2 +- frontend/src/queries/schema.json | 27 +- frontend/src/scenes/actions/Action.tsx | 3 - frontend/src/scenes/actions/ActionPlugins.tsx | 67 - .../scenes/activity/explore/EventDetails.tsx | 2 +- .../scenes/activity/live/LiveEventsTable.tsx | 2 +- .../activity/live/liveEventsTableLogic.tsx | 53 +- frontend/src/scenes/appScenes.ts | 1 + .../scenes/authentication/InviteSignup.tsx | 2 +- .../scenes/billing/BillingProductAddon.tsx | 2 +- .../src/scenes/dashboard/dashboardLogic.tsx | 62 +- .../scenes/data-warehouse/ViewLinkModal.tsx | 2 +- .../settings/DataWarehouseSourcesTable.tsx | 32 +- .../settings/dataWarehouseSettingsLogic.ts | 11 +- .../src/scenes/heatmaps/HeatmapsBrowser.tsx | 30 +- .../sdks/sdk-install-instructions/nuxt.tsx | 2 +- frontend/src/scenes/pipeline/Destinations.tsx | 10 +- .../pipeline/PipelineNodeConfiguration.tsx | 5 +- .../src/scenes/pipeline/PipelineNodeNew.tsx | 87 +- .../src/scenes/pipeline/destinationsLogic.tsx | 71 +- .../src/scenes/pipeline/pipelineNodeLogic.tsx | 23 +- .../scenes/pipeline/pipelineNodeNewLogic.tsx | 8 +- frontend/src/scenes/pipeline/types.ts | 30 +- frontend/src/scenes/sceneTypes.ts | 1 + frontend/src/scenes/scenes.ts | 8 +- .../player/controller/Seekbar.scss | 4 +- .../player/inspector/components/ItemEvent.tsx | 2 +- .../player/playerSettingsLogic.ts | 10 +- .../playlist/SessionRecordingsPlaylist.tsx | 51 +- .../sessionRecordingsPlaylistLogic.ts | 89 +- .../settings/user/personalAPIKeysLogic.tsx | 4 +- .../scenes/surveys/SurveyEditQuestionRow.tsx | 8 + frontend/src/scenes/surveys/surveyLogic.tsx | 64 + frontend/src/scenes/urls.ts | 6 +- .../web-analytics/WebAnalyticsNotice.tsx | 4 +- .../scenes/web-analytics/WebAnalyticsTile.tsx | 2 +- frontend/src/styles/vars.scss | 23 +- frontend/src/types.ts | 108 +- hogql_parser/HogQLParser.cpp | 1157 ++++++++-------- hogql_parser/HogQLParser.h | 10 +- hogql_parser/HogQLParser.interp | 2 +- hogql_parser/parser.cpp | 27 +- hogql_parser/setup.py | 2 +- hogvm/typescript/package.json | 8 +- hogvm/typescript/src/execute.ts | 10 +- latest_migrations.manifest | 2 +- mypy-baseline.txt | 45 +- package.json | 2 +- plugin-server/package.json | 1 + plugin-server/pnpm-lock.yaml | 7 + plugin-server/src/capabilities.ts | 7 + plugin-server/src/config/config.ts | 1 - .../session-recordings-consumer.ts | 12 +- plugin-server/src/main/pluginsServer.ts | 30 +- plugin-server/src/types.ts | 4 +- plugin-server/src/utils/db/db.ts | 10 +- plugin-server/src/utils/db/hub.ts | 8 +- .../event-pipeline/processPersonsStep.ts | 7 +- .../worker/ingestion/event-pipeline/runner.ts | 27 +- .../src/worker/ingestion/person-state.ts | 175 ++- plugin-server/src/worker/ingestion/utils.ts | 2 +- plugin-server/src/worker/rusty-hook.ts | 23 +- plugin-server/tests/main/db.test.ts | 14 +- ...events-ingestion-overflow-consumer.test.ts | 2 +- .../session-recording/utils.test.ts | 4 +- .../tests/main/process-event.test.ts | 13 +- .../worker/ingestion/person-state.test.ts | 184 ++- .../worker/ingestion/postgres-parity.test.ts | 14 +- pnpm-lock.yaml | 8 +- posthog/api/__init__.py | 8 + posthog/api/app_metrics.py | 21 +- posthog/api/team.py | 4 +- .../api/test/__snapshots__/test_decide.ambr | 385 +++++- posthog/api/test/test_cohort.py | 4 +- posthog/api/test/test_insight.py | 12 +- posthog/api/test/test_query.py | 6 +- posthog/celery.py | 6 +- posthog/hogql/ast.py | 1 + posthog/hogql/autocomplete.py | 8 +- posthog/hogql/database/database.py | 50 +- posthog/hogql/database/schema/persons.py | 8 +- posthog/hogql/database/schema/sessions.py | 2 +- .../database/schema/test/test_sessions.py | 2 +- .../test_person_where_clause_extractor.py | 4 +- posthog/hogql/database/test/test_database.py | 2 +- posthog/hogql/functions/mapping.py | 1 + posthog/hogql/grammar/HogQLParser.g4 | 6 +- posthog/hogql/grammar/HogQLParser.interp | 2 +- posthog/hogql/grammar/HogQLParser.py | 1184 +++++++++-------- posthog/hogql/modifiers.py | 16 +- posthog/hogql/parser.py | 6 +- posthog/hogql/printer.py | 35 +- posthog/hogql/property.py | 80 +- posthog/hogql/test/_test_parser.py | 30 +- posthog/hogql/test/test_modifiers.py | 52 +- posthog/hogql/test/test_printer.py | 20 +- posthog/hogql/test/test_property.py | 20 +- posthog/hogql/transforms/property_types.py | 2 +- .../hogql/transforms/test/test_in_cohort.py | 20 +- .../hogql/transforms/test/test_lazy_tables.py | 4 +- posthog/hogql/visitor.py | 7 +- .../hogql_queries/apply_dashboard_filters.py | 2 +- .../hogql_queries/insights/funnels/base.py | 22 +- .../hogql_queries/insights/funnels/funnel.py | 6 +- .../funnel_correlation_query_runner.py | 12 +- .../insights/funnels/funnel_query_context.py | 8 +- .../insights/funnels/funnel_strict.py | 6 +- .../insights/funnels/funnel_unordered.py | 6 +- .../insights/funnels/funnels_query_runner.py | 4 +- .../funnels/test/test_funnel_correlation.py | 58 +- .../test/test_funnel_correlations_persons.py | 10 +- .../hogql_queries/insights/funnels/utils.py | 10 +- .../insights/paths_query_runner.py | 24 +- .../insights/retention_query_runner.py | 20 +- .../test/test_lifecycle_query_runner.py | 50 +- .../insights/test/test_paginators.py | 2 +- .../test/test_stickiness_query_runner.py | 24 +- .../insights/trends/aggregation_operations.py | 4 +- .../insights/trends/breakdown.py | 2 +- .../insights/trends/breakdown_values.py | 2 +- .../hogql_queries/insights/trends/display.py | 18 +- .../test/test_aggregation_operations.py | 84 +- .../test/test_trends_actors_query_builder.py | 42 +- .../test/test_trends_dashboard_filters.py | 44 +- .../test/test_trends_data_warehouse_query.py | 20 +- .../trends/test/test_trends_persons.py | 26 +- .../trends/test/test_trends_query_builder.py | 34 +- .../trends/test/test_trends_query_runner.py | 250 ++-- .../insights/trends/test/test_utils.py | 26 +- .../trends/trends_actors_query_builder.py | 6 +- .../insights/trends/trends_query_builder.py | 10 +- .../insights/trends/trends_query_runner.py | 14 +- .../insights/utils/test/test_entities.py | 44 +- .../legacy_compatibility/filter_to_query.py | 12 +- .../test/test_filter_to_query.py | 64 +- posthog/hogql_queries/query_runner.py | 53 +- .../test/test_actors_query_runner.py | 8 +- .../test/test_events_query_runner.py | 8 +- .../hogql_queries/test/test_query_runner.py | 4 +- .../hogql_queries/utils/query_date_range.py | 20 +- .../utils/test/test_query_date_range.py | 46 +- .../web_analytics/stats_table.py | 72 +- .../web_analytics/stats_table_legacy.py | 52 +- .../test/test_web_analytics_query_runner.py | 12 +- .../test/test_web_stats_table.py | 34 +- posthog/jwt.py | 2 +- .../commands/compare_hogql_insights.py | 6 +- .../commands/start_temporal_worker.py | 1 - posthog/models/__init__.py | 2 + posthog/models/team/team.py | 16 +- posthog/permissions.py | 37 + posthog/queries/breakdown_props.py | 16 +- posthog/queries/event_query/event_query.py | 8 +- posthog/queries/foss_cohort_query.py | 24 +- posthog/queries/funnels/base.py | 12 +- posthog/queries/funnels/funnel_event_query.py | 10 +- .../groups_join_query/groups_join_query.py | 2 +- posthog/queries/paths/paths_event_query.py | 6 +- posthog/queries/retention/retention.py | 4 +- .../retention/retention_events_query.py | 24 +- .../stickiness/stickiness_event_query.py | 6 +- posthog/queries/trends/breakdown.py | 56 +- posthog/queries/trends/lifecycle.py | 8 +- posthog/queries/trends/total_volume.py | 14 +- posthog/queries/trends/trends_actors.py | 20 +- posthog/queries/trends/trends_event_query.py | 4 +- .../queries/trends/trends_event_query_base.py | 6 +- posthog/queries/trends/util.py | 4 +- posthog/queries/util.py | 4 +- posthog/schema.py | 700 +++++----- posthog/schema_helpers.py | 16 +- ...sion_recording_list_from_replay_summary.py | 26 +- posthog/tasks/tasks.py | 10 +- .../data_imports/pipelines/stripe/__init__.py | 225 ++++ .../data_imports/pipelines/stripe/helpers.py | 178 --- .../pipelines/test/test_pipeline.py | 15 +- .../workflow_activities/import_data.py | 24 +- .../external_data/test_external_data_job.py | 197 +-- posthog/test/test_migration_0410.py | 67 - posthog/test/test_schema_helpers.py | 50 +- posthog/test/test_team.py | 6 +- posthog/types.py | 4 +- posthog/warehouse/api/external_data_source.py | 33 +- posthog/warehouse/api/table.py | 2 +- .../api/test/test_external_data_source.py | 49 +- posthog/warehouse/data_load/service.py | 17 +- .../warehouse/models/external_data_source.py | 40 + .../models/external_table_definitions.py | 4 - posthog/warehouse/models/table.py | 16 +- production.Dockerfile | 2 +- requirements-dev.in | 1 + requirements-dev.txt | 45 +- requirements.in | 2 +- requirements.txt | 2 +- rust/.github/workflows/rust.yml | 105 -- rust/Dockerfile.migrate | 4 +- rust/depot.json | 2 +- 243 files changed, 5309 insertions(+), 3772 deletions(-) rename rust/.github/workflows/docker-build.yml => .github/workflows/rust-docker-build.yml (78%) rename rust/.github/workflows/docker-migrator.yml => .github/workflows/rust-hook-migrator-docker.yml (80%) create mode 100644 .github/workflows/rust.yml delete mode 100644 frontend/src/scenes/actions/ActionPlugins.tsx delete mode 100644 posthog/temporal/data_imports/pipelines/stripe/helpers.py delete mode 100644 posthog/test/test_migration_0410.py delete mode 100644 rust/.github/workflows/rust.yml diff --git a/.github/workflows/build-and-deploy-prod.yml b/.github/workflows/build-and-deploy-prod.yml index 3e491cd25dcc5..3ca21341d11aa 100644 --- a/.github/workflows/build-and-deploy-prod.yml +++ b/.github/workflows/build-and-deploy-prod.yml @@ -7,6 +7,8 @@ on: push: branches: - master + paths-ignore: + - 'rust/**' jobs: slack: diff --git a/.github/workflows/ci-hog.yml b/.github/workflows/ci-hog.yml index 7e6a5caf94005..db26722eb7e84 100644 --- a/.github/workflows/ci-hog.yml +++ b/.github/workflows/ci-hog.yml @@ -51,7 +51,7 @@ jobs: # If this run wasn't initiated by the bot (meaning: snapshot update) and we've determined # there are backend changes, cancel previous runs - uses: n1hility/cancel-previous-runs@v3 - if: github.actor != 'posthog-bot' && needs.changes.outputs.backend == 'true' + if: github.actor != 'posthog-bot' && needs.changes.outputs.hog == 'true' with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/container-images-cd.yml b/.github/workflows/container-images-cd.yml index 9611588bc1700..ec353909ae486 100644 --- a/.github/workflows/container-images-cd.yml +++ b/.github/workflows/container-images-cd.yml @@ -13,6 +13,8 @@ on: push: branches: - master + paths-ignore: + - 'rust/**' workflow_dispatch: jobs: @@ -120,17 +122,22 @@ jobs: - name: Trigger Batch Exports Temporal Worker Cloud deployment if: steps.check_changes_batch_exports_temporal_worker.outputs.changed == 'true' - uses: mvasigh/dispatch-action@main + uses: peter-evans/repository-dispatch@v3 with: token: ${{ steps.deployer.outputs.token }} - repo: charts - owner: PostHog - event_type: temporal_worker_deploy - message: | + repository: PostHog/charts + event-type: commit_state_update + client-payload: | { - "image_tag": "${{ steps.build.outputs.digest }}", - "worker_name": "temporal-worker", - "context": ${{ toJson(github) }} + "values": { + "image": { + "sha": "${{ steps.build.outputs.digest }}" + } + }, + "release": "temporal-worker", + "commit": ${{ toJson(github.event.head_commit) }}, + "repository": ${{ toJson(github.repository) }}, + "labels": ${{ steps.labels.outputs.labels }} } - name: Check for changes that affect general purpose temporal worker @@ -140,17 +147,22 @@ jobs: - name: Trigger General Purpose Temporal Worker Cloud deployment if: steps.check_changes_general_purpose_temporal_worker.outputs.changed == 'true' - uses: mvasigh/dispatch-action@main + uses: peter-evans/repository-dispatch@v3 with: token: ${{ steps.deployer.outputs.token }} - repo: charts - owner: PostHog - event_type: temporal_worker_deploy - message: | + repository: PostHog/charts + event-type: commit_state_update + client-payload: | { - "image_tag": "${{ steps.build.outputs.digest }}", - "worker_name": "temporal-worker-general-purpose", - "context": ${{ toJson(github) }} + "values": { + "image": { + "sha": "${{ steps.build.outputs.digest }}" + } + }, + "release": "temporal-worker-general-purpose", + "commit": ${{ toJson(github.event.head_commit) }}, + "repository": ${{ toJson(github.repository) }}, + "labels": ${{ steps.labels.outputs.labels }} } - name: Check for changes that affect data warehouse temporal worker @@ -160,15 +172,20 @@ jobs: - name: Trigger Data Warehouse Temporal Worker Cloud deployment if: steps.check_changes_data_warehouse_temporal_worker.outputs.changed == 'true' - uses: mvasigh/dispatch-action@main + uses: peter-evans/repository-dispatch@v3 with: token: ${{ steps.deployer.outputs.token }} - repo: charts - owner: PostHog - event_type: temporal_worker_deploy - message: | + repository: PostHog/charts + event-type: commit_state_update + client-payload: | { - "image_tag": "${{ steps.build.outputs.digest }}", - "worker_name": "temporal-worker-data-warehouse", - "context": ${{ toJson(github) }} + "values": { + "image": { + "sha": "${{ steps.build.outputs.digest }}" + } + }, + "release": "temporal-worker-data-warehouse", + "commit": ${{ toJson(github.event.head_commit) }}, + "repository": ${{ toJson(github.repository) }}, + "labels": ${{ steps.labels.outputs.labels }} } diff --git a/rust/.github/workflows/docker-build.yml b/.github/workflows/rust-docker-build.yml similarity index 78% rename from rust/.github/workflows/docker-build.yml rename to .github/workflows/rust-docker-build.yml index 94f36ef0d249b..d44e5d847e601 100644 --- a/rust/.github/workflows/docker-build.yml +++ b/.github/workflows/rust-docker-build.yml @@ -3,8 +3,10 @@ name: Build container images on: workflow_dispatch: push: + paths: + - 'rust/**' branches: - - 'main' + - 'master' jobs: build: @@ -22,9 +24,19 @@ jobs: contents: read # allow reading the repo contents packages: write # allow push to ghcr.io + defaults: + run: + working-directory: rust + steps: - name: Check Out Repo + # Checkout project code + # Use sparse checkout to only select files in rust directory + # Turning off cone mode ensures that files in the project root are not included during checkout uses: actions/checkout@v3 + with: + sparse-checkout: 'rust/' + sparse-checkout-cone-mode: false - name: Set up Depot CLI uses: depot/setup-action@v1 @@ -41,6 +53,7 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + logout: false - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -49,7 +62,7 @@ jobs: id: meta uses: docker/metadata-action@v4 with: - images: ghcr.io/posthog/hog-rs/${{ matrix.image }} + images: ghcr.io/posthog/posthog/${{ matrix.image }} tags: | type=ref,event=pr type=ref,event=branch @@ -65,8 +78,8 @@ jobs: id: docker_build uses: depot/build-push-action@v1 with: - context: ./ - file: ./Dockerfile + context: ./rust/ + file: ./rust/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/rust/.github/workflows/docker-migrator.yml b/.github/workflows/rust-hook-migrator-docker.yml similarity index 80% rename from rust/.github/workflows/docker-migrator.yml rename to .github/workflows/rust-hook-migrator-docker.yml index 69f14acd83fa8..2dd7c01d015dc 100644 --- a/rust/.github/workflows/docker-migrator.yml +++ b/.github/workflows/rust-hook-migrator-docker.yml @@ -3,8 +3,10 @@ name: Build hook-migrator docker image on: workflow_dispatch: push: + paths: + - 'rust/**' branches: - - 'main' + - 'master' permissions: packages: write @@ -18,9 +20,19 @@ jobs: contents: read # allow reading the repo contents packages: write # allow push to ghcr.io + defaults: + run: + working-directory: rust + steps: - name: Check Out Repo + # Checkout project code + # Use sparse checkout to only select files in rust directory + # Turning off cone mode ensures that files in the project root are not included during checkout uses: actions/checkout@v3 + with: + sparse-checkout: 'rust/' + sparse-checkout-cone-mode: false - name: Set up Depot CLI uses: depot/setup-action@v1 @@ -61,8 +73,8 @@ jobs: id: docker_build_hook_migrator uses: depot/build-push-action@v1 with: - context: ./ - file: ./Dockerfile.migrate + context: ./rust/ + file: ./rust/Dockerfile.migrate push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000000000..c2c379334980e --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,192 @@ +name: Rust + +on: + workflow_dispatch: + push: + branches: [master, main] + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + # Job to decide if we should run rust ci + # See https://github.com/dorny/paths-filter#conditional-execution for more details + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.repository == 'PostHog/posthog' + name: Determine need to run rust checks + # Set job outputs to values from filter step + outputs: + rust: ${{ steps.filter.outputs.rust }} + steps: + # For pull requests it's not necessary to checkout the code, but we + # also want this to run on master so we need to checkout + - uses: actions/checkout@v3 + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + rust: + # Avoid running rust tests for irrelevant changes + - 'rust/**' + + build: + needs: changes + runs-on: depot-ubuntu-22.04-4 + + defaults: + run: + working-directory: rust + + steps: + # Checkout project code + # Use sparse checkout to only select files in rust directory + # Turning off cone mode ensures that files in the project root are not included during checkout + - uses: actions/checkout@v3 + if: needs.changes.outputs.rust == 'true' + with: + sparse-checkout: 'rust/' + sparse-checkout-cone-mode: false + + - name: Install rust + if: needs.changes.outputs.rust == 'true' + uses: dtolnay/rust-toolchain@1.77 + + - uses: actions/cache@v3 + if: needs.changes.outputs.rust == 'true' + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo build + if: needs.changes.outputs.rust == 'true' + run: cargo build --all --locked --release && find target/release/ -maxdepth 1 -executable -type f | xargs strip + + test: + needs: changes + runs-on: depot-ubuntu-22.04-4 + timeout-minutes: 10 + + defaults: + run: + working-directory: rust + + steps: + # Checkout project code + # Use sparse checkout to only select files in rust directory + # Turning off cone mode ensures that files in the project root are not included during checkout + - uses: actions/checkout@v3 + if: needs.changes.outputs.rust == 'true' + with: + sparse-checkout: 'rust/' + sparse-checkout-cone-mode: false + + - name: Login to DockerHub + if: needs.changes.outputs.rust == 'true' + uses: docker/login-action@v2 + with: + username: posthog + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Setup dependencies + if: needs.changes.outputs.rust == 'true' + run: | + docker compose up kafka redis db echo_server -d --wait + docker compose up setup_test_db + echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts + + - name: Install rust + if: needs.changes.outputs.rust == 'true' + uses: dtolnay/rust-toolchain@1.77 + + - uses: actions/cache@v3 + if: needs.changes.outputs.rust == 'true' + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${ runner.os }-cargo-debug-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo test + if: needs.changes.outputs.rust == 'true' + run: cargo test --all-features + + linting: + needs: changes + runs-on: depot-ubuntu-22.04-4 + + defaults: + run: + working-directory: rust + + steps: + # Checkout project code + # Use sparse checkout to only select files in rust directory + # Turning off cone mode ensures that files in the project root are not included during checkout + - uses: actions/checkout@v3 + if: needs.changes.outputs.rust == 'true' + with: + sparse-checkout: 'rust/' + sparse-checkout-cone-mode: false + + - name: Install rust + if: needs.changes.outputs.rust == 'true' + uses: dtolnay/rust-toolchain@1.77 + with: + components: clippy,rustfmt + + - uses: actions/cache@v3 + if: needs.changes.outputs.rust == 'true' + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} + + - name: Check format + if: needs.changes.outputs.rust == 'true' + run: cargo fmt -- --check + + - name: Run clippy + if: needs.changes.outputs.rust == 'true' + run: cargo clippy -- -D warnings + + - name: Run cargo check + if: needs.changes.outputs.rust == 'true' + run: cargo check --all-features + + shear: + needs: changes + runs-on: depot-ubuntu-22.04-4 + + defaults: + run: + working-directory: rust + + steps: + # Checkout project code + # Use sparse checkout to only select files in rust directory + # Turning off cone mode ensures that files in the project root are not included during checkout + - uses: actions/checkout@v3 + if: needs.changes.outputs.rust == 'true' + with: + sparse-checkout: 'rust/' + sparse-checkout-cone-mode: false + + - name: Install cargo-binstall + if: needs.changes.outputs.rust == 'true' + uses: cargo-bins/cargo-binstall@main + + - name: Install cargo-shear + if: needs.changes.outputs.rust == 'true' + run: cargo binstall --no-confirm cargo-shear + + - run: cargo shear + if: needs.changes.outputs.rust == 'true' diff --git a/README.md b/README.md index f261c5f98d466..fcdeaa17cdadc 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,15 @@ PostHog Demonstration - See PostHog in action +
See PostHog in action

## PostHog is an all-in-one, open source platform for building better products - Specify events manually, or use autocapture to get started quickly - Analyze data with ready-made visualizations, or do it yourself with SQL +- Only capture properties on the people you want to track, save money when you don't - Gather insights by capturing session replays, console logs, and network monitoring - Improve your product with A/B testing that automatically analyzes performance - Safely roll out features to select users or cohorts with feature flags @@ -38,7 +40,7 @@ PostHog is available with hosting in the EU or US and is fully SOC 2 compliant. - 1 million feature flag requests - 250 survey responses -We're constantly adding new features, with web analytics and data warehouse now in beta! +We're constantly adding new features, with web analytics and data warehouse now in beta! ## Table of Contents @@ -73,7 +75,7 @@ PostHog brings all the tools and data you need to build better products. ### Analytics and optimization tools - **Event-based analytics:** Capture your product's usage [automatically](https://posthog.com/docs/libraries/js#autocapture), or [customize](https://posthog.com/docs/getting-started/install) it to your needs -- **User and group tracking:** Understand the [people](https://posthog.com/manual/persons) and [groups](https://posthog.com/manual/group-analytics) behind the events and track properties about them +- **User and group tracking:** Understand the [people](https://posthog.com/manual/persons) and [groups](https://posthog.com/manual/group-analytics) behind the events and track properties about them when needed - **Data visualizations:** Create and share [graphs](https://posthog.com/docs/features/trends), [funnels](https://posthog.com/docs/features/funnels), [paths](https://posthog.com/docs/features/paths), [retention](https://posthog.com/docs/features/retention), and [dashboards](https://posthog.com/docs/features/dashboards) - **SQL access:** Use [SQL](https://posthog.com/docs/product-analytics/sql) to get a deeper understanding of your users, breakdown information and create completely tailored visualizations - **Session replays:** [Watch videos](https://posthog.com/docs/features/session-recording) of your users' behavior, with fine-grained filters and privacy controls, as well as network monitoring and captured console logs diff --git a/bin/build-schema-python.sh b/bin/build-schema-python.sh index d32c4caedfda9..7937731b55116 100755 --- a/bin/build-schema-python.sh +++ b/bin/build-schema-python.sh @@ -9,7 +9,8 @@ datamodel-codegen \ --input frontend/src/queries/schema.json --input-file-type jsonschema \ --output posthog/schema.py --output-model-type pydantic_v2.BaseModel \ --custom-file-header "# mypy: disable-error-code=\"assignment\"" \ - --set-default-enum-member + --set-default-enum-member --capitalise-enum-members \ + --wrap-string-literal # Format schema.py ruff format posthog/schema.py diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 2edb94dd9bc78..15adbe9d5febe 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -115,6 +115,13 @@ services: CLICKHOUSE_SECURE: 'false' CLICKHOUSE_VERIFY: 'false' + livestream: + image: 'ghcr.io/posthog/livestream:main' + restart: on-failure + depends_on: + kafka: + condition: service_started + migrate: <<: *worker command: sh -c " diff --git a/docker-compose.dev-full.yml b/docker-compose.dev-full.yml index cdb2eee4ec285..002553728d815 100644 --- a/docker-compose.dev-full.yml +++ b/docker-compose.dev-full.yml @@ -71,6 +71,14 @@ services: - '1080:1080' - '1025:1025' + webhook-tester: + image: tarampampam/webhook-tester:1.1.0 + restart: on-failure + ports: + - '2080:2080' + environment: + - PORT=2080 + worker: extends: file: docker-compose.base.yml diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 25d30840b83ee..f79697c73fbfd 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -95,6 +95,14 @@ services: - '1080:1080' - '1025:1025' + webhook-tester: + image: tarampampam/webhook-tester:1.1.0 + restart: on-failure + ports: + - '2080:2080' + environment: + - LISTEN_PORT=2080 + # Optional capture capture: profiles: ['capture-rs'] @@ -109,6 +117,17 @@ services: - redis - kafka + livestream: + extends: + file: docker-compose.base.yml + service: livestream + environment: + - JWT.TOKEN=${SECRET_KEY} + ports: + - '8666:8080' + volumes: + - ./docker/livestream/configs-dev.yml:/configs/configs.yml + # Temporal containers elasticsearch: extends: diff --git a/ee/clickhouse/models/test/test_cohort.py b/ee/clickhouse/models/test/test_cohort.py index eb2956f1f8078..8af41154c48a5 100644 --- a/ee/clickhouse/models/test/test_cohort.py +++ b/ee/clickhouse/models/test/test_cohort.py @@ -142,9 +142,11 @@ def test_prop_cohort_basic_action(self): query, params = parse_prop_grouped_clauses( team_id=self.team.pk, property_group=filter.property_groups, - person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonsOnEventsMode.disabled - else PersonPropertiesMode.DIRECT_ON_EVENTS, + person_properties_mode=( + PersonPropertiesMode.USING_SUBQUERY + if self.team.person_on_events_mode == PersonsOnEventsMode.DISABLED + else PersonPropertiesMode.DIRECT_ON_EVENTS + ), hogql_context=filter.hogql_context, ) final_query = "SELECT uuid FROM events WHERE team_id = %(team_id)s {}".format(query) @@ -197,9 +199,11 @@ def test_prop_cohort_basic_event_days(self): query, params = parse_prop_grouped_clauses( team_id=self.team.pk, property_group=filter.property_groups, - person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonsOnEventsMode.disabled - else PersonPropertiesMode.DIRECT_ON_EVENTS, + person_properties_mode=( + PersonPropertiesMode.USING_SUBQUERY + if self.team.person_on_events_mode == PersonsOnEventsMode.DISABLED + else PersonPropertiesMode.DIRECT_ON_EVENTS + ), hogql_context=filter.hogql_context, ) final_query = "SELECT uuid FROM events WHERE team_id = %(team_id)s {}".format(query) @@ -222,9 +226,11 @@ def test_prop_cohort_basic_event_days(self): query, params = parse_prop_grouped_clauses( team_id=self.team.pk, property_group=filter.property_groups, - person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonsOnEventsMode.disabled - else PersonPropertiesMode.DIRECT_ON_EVENTS, + person_properties_mode=( + PersonPropertiesMode.USING_SUBQUERY + if self.team.person_on_events_mode == PersonsOnEventsMode.DISABLED + else PersonPropertiesMode.DIRECT_ON_EVENTS + ), hogql_context=filter.hogql_context, ) final_query = "SELECT uuid FROM events WHERE team_id = %(team_id)s {}".format(query) @@ -273,9 +279,11 @@ def test_prop_cohort_basic_action_days(self): query, params = parse_prop_grouped_clauses( team_id=self.team.pk, property_group=filter.property_groups, - person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonsOnEventsMode.disabled - else PersonPropertiesMode.DIRECT_ON_EVENTS, + person_properties_mode=( + PersonPropertiesMode.USING_SUBQUERY + if self.team.person_on_events_mode == PersonsOnEventsMode.DISABLED + else PersonPropertiesMode.DIRECT_ON_EVENTS + ), hogql_context=filter.hogql_context, ) final_query = "SELECT uuid FROM events WHERE team_id = %(team_id)s {}".format(query) @@ -294,9 +302,11 @@ def test_prop_cohort_basic_action_days(self): query, params = parse_prop_grouped_clauses( team_id=self.team.pk, property_group=filter.property_groups, - person_properties_mode=PersonPropertiesMode.USING_SUBQUERY - if self.team.person_on_events_mode == PersonsOnEventsMode.disabled - else PersonPropertiesMode.DIRECT_ON_EVENTS, + person_properties_mode=( + PersonPropertiesMode.USING_SUBQUERY + if self.team.person_on_events_mode == PersonsOnEventsMode.DISABLED + else PersonPropertiesMode.DIRECT_ON_EVENTS + ), hogql_context=filter.hogql_context, ) final_query = "SELECT uuid FROM events WHERE team_id = %(team_id)s {}".format(query) diff --git a/ee/clickhouse/queries/enterprise_cohort_query.py b/ee/clickhouse/queries/enterprise_cohort_query.py index 814b61e9a8bf5..72b0ed9bf5e6a 100644 --- a/ee/clickhouse/queries/enterprise_cohort_query.py +++ b/ee/clickhouse/queries/enterprise_cohort_query.py @@ -319,7 +319,7 @@ def _get_sequence_query(self) -> tuple[str, dict[str, Any], str]: event_param_name = f"{self._cohort_pk}_event_ids" - if self.should_pushdown_persons and self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self.should_pushdown_persons and self._person_on_events_mode != PersonsOnEventsMode.DISABLED: person_prop_query, person_prop_params = self._get_prop_groups( self._inner_property_groups, person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS, diff --git a/ee/clickhouse/queries/event_query.py b/ee/clickhouse/queries/event_query.py index 0e16abc780049..977ec53e74314 100644 --- a/ee/clickhouse/queries/event_query.py +++ b/ee/clickhouse/queries/event_query.py @@ -37,7 +37,7 @@ def __init__( extra_event_properties: Optional[list[PropertyName]] = None, extra_person_fields: Optional[list[ColumnName]] = None, override_aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, **kwargs, ) -> None: if extra_person_fields is None: diff --git a/ee/clickhouse/queries/funnels/funnel_correlation.py b/ee/clickhouse/queries/funnels/funnel_correlation.py index ff69e53d2e01e..efa84347730d7 100644 --- a/ee/clickhouse/queries/funnels/funnel_correlation.py +++ b/ee/clickhouse/queries/funnels/funnel_correlation.py @@ -152,7 +152,7 @@ def __init__( def properties_to_include(self) -> list[str]: props_to_include = [] if ( - self._team.person_on_events_mode != PersonsOnEventsMode.disabled + self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED and self._filter.correlation_type == FunnelCorrelationType.PROPERTIES ): # When dealing with properties, make sure funnel response comes with properties @@ -432,7 +432,7 @@ def get_properties_query(self) -> tuple[str, dict[str, Any]]: return query, params def _get_aggregation_target_join_query(self) -> str: - if self._team.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + if self._team.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: aggregation_person_join = f""" JOIN funnel_actors as actors ON event.person_id = actors.actor_id @@ -499,7 +499,7 @@ def _get_events_join_query(self) -> str: def _get_aggregation_join_query(self): if self._filter.aggregation_group_type_index is None: - if self._team.person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): + if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED and groups_on_events_querying_enabled(): return "", {} person_query, person_query_params = PersonQuery( @@ -519,7 +519,7 @@ def _get_aggregation_join_query(self): return GroupsJoinQuery(self._filter, self._team.pk, join_key="funnel_actors.actor_id").get_join_query() def _get_properties_prop_clause(self): - if self._team.person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): + if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED and groups_on_events_querying_enabled(): group_properties_field = f"group{self._filter.aggregation_group_type_index}_properties" aggregation_properties_alias = ( "person_properties" if self._filter.aggregation_group_type_index is None else group_properties_field @@ -546,7 +546,7 @@ def _get_properties_prop_clause(self): param_name = f"property_name_{index}" if self._filter.aggregation_group_type_index is not None: expression, _ = get_property_string_expr( - "groups" if self._team.person_on_events_mode == PersonsOnEventsMode.disabled else "events", + "groups" if self._team.person_on_events_mode == PersonsOnEventsMode.DISABLED else "events", property_name, f"%({param_name})s", aggregation_properties_alias, @@ -554,13 +554,15 @@ def _get_properties_prop_clause(self): ) else: expression, _ = get_property_string_expr( - "person" if self._team.person_on_events_mode == PersonsOnEventsMode.disabled else "events", + "person" if self._team.person_on_events_mode == PersonsOnEventsMode.DISABLED else "events", property_name, f"%({param_name})s", aggregation_properties_alias, - materialised_table_column=aggregation_properties_alias - if self._team.person_on_events_mode != PersonsOnEventsMode.disabled - else "properties", + materialised_table_column=( + aggregation_properties_alias + if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED + else "properties" + ), ) person_property_params[param_name] = property_name person_property_expressions.append(expression) diff --git a/ee/clickhouse/queries/groups_join_query.py b/ee/clickhouse/queries/groups_join_query.py index 7a3dc46daf993..ddb7d193d6d9b 100644 --- a/ee/clickhouse/queries/groups_join_query.py +++ b/ee/clickhouse/queries/groups_join_query.py @@ -27,7 +27,7 @@ def __init__( team_id: int, column_optimizer: Optional[EnterpriseColumnOptimizer] = None, join_key: Optional[str] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, ) -> None: self._filter = filter self._team_id = team_id @@ -38,7 +38,7 @@ def __init__( def get_join_query(self) -> tuple[str, dict]: join_queries, params = [], {} - if self._person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED and groups_on_events_querying_enabled(): return "", {} for group_type_index in self._column_optimizer.group_types_to_query: diff --git a/ee/session_recordings/queries/test/test_session_recording_list_from_filters.py b/ee/session_recordings/queries/test/test_session_recording_list_from_filters.py index 84b378bdf5959..8de7d89abee6b 100644 --- a/ee/session_recordings/queries/test/test_session_recording_list_from_filters.py +++ b/ee/session_recordings/queries/test/test_session_recording_list_from_filters.py @@ -64,7 +64,7 @@ def create_event( True, False, False, - PersonsOnEventsMode.person_id_no_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, { "kperson_filter_pre__0": "rgInternal", "kpersonquery_person_filter_fin__0": "rgInternal", @@ -80,7 +80,7 @@ def create_event( False, False, False, - PersonsOnEventsMode.disabled, + PersonsOnEventsMode.DISABLED, { "kperson_filter_pre__0": "rgInternal", "kpersonquery_person_filter_fin__0": "rgInternal", @@ -96,7 +96,7 @@ def create_event( False, True, False, - PersonsOnEventsMode.person_id_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS, { "event_names": [], "event_start_time": mock.ANY, @@ -112,7 +112,7 @@ def create_event( False, True, True, - PersonsOnEventsMode.person_id_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS, { "event_end_time": mock.ANY, "event_names": [], diff --git a/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py b/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py index 797ac453e69e0..b743302f896bf 100644 --- a/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py +++ b/ee/session_recordings/queries/test/test_session_recording_list_from_session_replay.py @@ -62,7 +62,7 @@ def create_event( True, False, False, - PersonsOnEventsMode.person_id_no_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, { "kperson_filter_pre__0": "rgInternal", "kpersonquery_person_filter_fin__0": "rgInternal", @@ -78,7 +78,7 @@ def create_event( False, False, False, - PersonsOnEventsMode.disabled, + PersonsOnEventsMode.DISABLED, { "kperson_filter_pre__0": "rgInternal", "kpersonquery_person_filter_fin__0": "rgInternal", @@ -94,7 +94,7 @@ def create_event( False, True, False, - PersonsOnEventsMode.person_id_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS, { "event_names": [], "event_start_time": mock.ANY, @@ -110,7 +110,7 @@ def create_event( False, True, True, - PersonsOnEventsMode.person_id_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS, { "event_end_time": mock.ANY, "event_names": [], diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png index 47914be1ffc6ad8a960db5917a592648f297465d..87e964fc8ec9ca9b0ca4d5ee12b162115e17b18b 100644 GIT binary patch literal 177354 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­Dq?MxX~s(u`OnzLC_`#Ae$59WK<{k{0U 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 G9{xYF*9r{) literal 179111 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 a{xkExN_^FEbS|hUVDNPHb6Mw<&;$UC_u0$< diff --git a/frontend/src/layout/navigation-3000/navigationLogic.tsx b/frontend/src/layout/navigation-3000/navigationLogic.tsx index af344d75ed176..b09cc1a3f6948 100644 --- a/frontend/src/layout/navigation-3000/navigationLogic.tsx +++ b/frontend/src/layout/navigation-3000/navigationLogic.tsx @@ -17,6 +17,7 @@ import { IconServer, IconTestTube, IconToggle, + IconWarning, } from '@posthog/icons' import { lemonToast, Spinner } from '@posthog/lemon-ui' import { captureException } from '@sentry/react' @@ -450,6 +451,14 @@ export const navigation3000Logic = kea([ icon: , to: urls.replay(), }, + featureFlags[FEATURE_FLAGS.ERROR_TRACKING] + ? { + identifier: Scene.ErrorTracking, + label: 'Error tracking', + icon: , + to: urls.errorTracking(), + } + : null, featureFlags[FEATURE_FLAGS.HEATMAPS_UI] ? { identifier: Scene.Heatmaps, diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 3f44c3b06eeb0..d0a94739c95f4 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -43,6 +43,7 @@ import { FeatureFlagType, Group, GroupListParams, + HogFunctionType, InsightModel, IntegrationType, ListOrganizationMembersParams, @@ -320,6 +321,14 @@ class ApiRequest { return this.pluginConfig(pluginConfigId, teamId).addPathComponent('logs') } + public hogFunctions(teamId?: TeamType['id']): ApiRequest { + return this.projectsDetail(teamId).addPathComponent('hog_functions') + } + + public hogFunction(id: HogFunctionType['id'], teamId?: TeamType['id']): ApiRequest { + return this.hogFunctions(teamId).addPathComponent(id) + } + // # Actions public actions(teamId?: TeamType['id']): ApiRequest { return this.projectsDetail(teamId).addPathComponent('actions') @@ -1634,6 +1643,24 @@ const api = { }, }, + hogFunctions: { + async listTemplates(): Promise> { + return await new ApiRequest().hogFunctions().get() + }, + async list(): Promise> { + return await new ApiRequest().hogFunctions().get() + }, + async get(id: HogFunctionType['id']): Promise { + return await new ApiRequest().hogFunction(id).get() + }, + async create(data: Partial): Promise { + return await new ApiRequest().hogFunctions().create({ data }) + }, + async update(id: HogFunctionType['id'], data: Partial): Promise { + return await new ApiRequest().hogFunction(id).update({ data }) + }, + }, + annotations: { async get(annotationId: RawAnnotationType['id']): Promise { return await new ApiRequest().annotation(annotationId).get() @@ -1978,6 +2005,12 @@ const api = { async reload(sourceId: ExternalDataStripeSource['id']): Promise { await new ApiRequest().externalDataSource(sourceId).withAction('reload').create() }, + async update( + sourceId: ExternalDataStripeSource['id'], + data: Partial + ): Promise { + return await new ApiRequest().externalDataSource(sourceId).update({ data }) + }, async database_schema( source_type: ExternalDataSourceType, payload: Record diff --git a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss index 2773867f90eb8..06a06efbe3ef1 100644 --- a/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss +++ b/frontend/src/lib/components/Cards/InsightCard/InsightCard.scss @@ -51,6 +51,10 @@ border: none; border-radius: 0; } + + .WebAnalyticsDashboard .InsightVizDisplay & { + min-height: var(--insight-viz-min-height); + } } .InsightDetails, diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx index ec1a6ca6f1608..cf7033aff1046 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx @@ -5,7 +5,7 @@ import clsx from 'clsx' import { useValues } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { copyToClipboard } from 'lib/utils/copyToClipboard' -import { CSSProperties, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter' import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash' import dart from 'react-syntax-highlighter/dist/esm/languages/prism/dart' @@ -81,7 +81,7 @@ export interface CodeSnippetProps { wrap?: boolean compact?: boolean actions?: JSX.Element - style?: CSSProperties + className?: string /** What is being copied. @example 'link' */ thing?: string /** If set, the snippet becomes expandable when there's more than this number of lines. */ @@ -93,7 +93,7 @@ export function CodeSnippet({ language = Language.Text, wrap = false, compact = false, - style, + className, actions, thing = 'snippet', maxLinesWithoutExpansion, @@ -120,8 +120,7 @@ export function CodeSnippet({ } return ( - // eslint-disable-next-line react/forbid-dom-props -

+
{actions} {item.query} @@ -263,7 +263,7 @@ function DebugCHQueries(): JSX.Element { language={Language.JSON} maxLinesWithoutExpansion={0} key={item.query_id} - style={{ fontSize: 12, marginBottom: '0.25rem' }} + className="text-sm mb-2" > {JSON.stringify(event, null, 2)} diff --git a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx index 77e9e2c33a701..8084a2a07d650 100644 --- a/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx +++ b/frontend/src/lib/components/CommandPalette/commandPaletteLogic.tsx @@ -38,6 +38,7 @@ import { IconTrends, IconUnlock, IconUserPaths, + IconWarning, IconX, } from '@posthog/icons' import { Parser } from 'expr-eval' @@ -581,6 +582,17 @@ export const commandPaletteLogic = kea([ }, ] : []), + ...(values.featureFlags[FEATURE_FLAGS.ERROR_TRACKING] + ? [ + { + icon: IconWarning, + display: 'Go to Error tracking', + executor: () => { + push(urls.errorTracking()) + }, + }, + ] + : []), { display: 'Go to Session replay', icon: IconRewindPlay, diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx index 86915996ae5f6..b0aa04cc27800 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx @@ -1,7 +1,7 @@ import { Meta } from '@storybook/react' import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' -import { EventType, RecordingEventType } from '~/types' +import { EventType } from '~/types' const meta: Meta = { title: 'Components/Errors/Error Display', @@ -9,104 +9,95 @@ const meta: Meta = { } export default meta -function errorEvent(properties: Record): EventType | RecordingEventType { +function errorProperties(properties: Record): EventType['properties'] { return { - id: '12345', - elements: [], - uuid: '018880b6-b781-0008-a2e5-629b2624fd2f', - event: '$exception', - properties: { - $os: 'Windows', - $os_version: '10.0', - $browser: 'Chrome', - $device_type: 'Desktop', - $current_url: 'https://app.posthog.com/home', - $host: 'app.posthog.com', - $pathname: '/home', - $browser_version: 113, - $browser_language: 'es-ES', - $screen_height: 1080, - $screen_width: 1920, - $viewport_height: 929, - $viewport_width: 1920, - $lib: 'web', - $lib_version: '1.63.3', - distinct_id: 'iOizUPH4RH65nZjvGVBz5zZUmwdHvq2mxzNySQqqYkG', - $device_id: '186144e7357245-0cfe8bf1b5b877-26021051-1fa400-186144e7358d3', - $active_feature_flags: ['are-the-flags', 'important-for-the-error'], - $feature_flag_payloads: { - 'are-the-flags': '{\n "flag": "payload"\n}', - }, - $user_id: 'iOizUPH4RH65nZjvGVBz5zZUmwdHvq2mxzNySQqqYkG', - $groups: { - project: '00000000-0000-0000-1847-88f0ffa23444', - organization: '00000000-0000-0000-a050-5d4557279956', - customer: 'the-customer', - instance: 'https://app.posthog.com', - }, - $exception_message: 'ResizeObserver loop limit exceeded', - $exception_type: 'Error', - $exception_personURL: 'https://app.posthog.com/person/the-person-id', - $sentry_event_id: 'id-from-the-sentry-integration', - $sentry_exception: { - values: [ - { - value: 'ResizeObserver loop limit exceeded', - type: 'Error', - mechanism: { - type: 'onerror', - handled: false, - synthetic: true, - }, - stacktrace: { - frames: [ - { - colno: 0, - filename: 'https://app.posthog.com/home', - function: '?', - in_app: true, - lineno: 0, - }, - ], - }, + $os: 'Windows', + $os_version: '10.0', + $browser: 'Chrome', + $device_type: 'Desktop', + $current_url: 'https://app.posthog.com/home', + $host: 'app.posthog.com', + $pathname: '/home', + $browser_version: 113, + $browser_language: 'es-ES', + $screen_height: 1080, + $screen_width: 1920, + $viewport_height: 929, + $viewport_width: 1920, + $lib: 'web', + $lib_version: '1.63.3', + distinct_id: 'iOizUPH4RH65nZjvGVBz5zZUmwdHvq2mxzNySQqqYkG', + $device_id: '186144e7357245-0cfe8bf1b5b877-26021051-1fa400-186144e7358d3', + $active_feature_flags: ['are-the-flags', 'important-for-the-error'], + $feature_flag_payloads: { + 'are-the-flags': '{\n "flag": "payload"\n}', + }, + $user_id: 'iOizUPH4RH65nZjvGVBz5zZUmwdHvq2mxzNySQqqYkG', + $groups: { + project: '00000000-0000-0000-1847-88f0ffa23444', + organization: '00000000-0000-0000-a050-5d4557279956', + customer: 'the-customer', + instance: 'https://app.posthog.com', + }, + $exception_message: 'ResizeObserver loop limit exceeded', + $exception_type: 'Error', + $exception_personURL: 'https://app.posthog.com/person/the-person-id', + $sentry_event_id: 'id-from-the-sentry-integration', + $sentry_exception: { + values: [ + { + value: 'ResizeObserver loop limit exceeded', + type: 'Error', + mechanism: { + type: 'onerror', + handled: false, + synthetic: true, }, - ], - }, - $sentry_exception_message: 'ResizeObserver loop limit exceeded', - $sentry_exception_type: 'Error', - $sentry_tags: { - 'PostHog Person URL': 'https://app.posthog.com/person/the-person-id', - 'PostHog Recording URL': 'https://app.posthog.com/replay/the-session-id?t=866', - }, - $sentry_url: - 'https://sentry.io/organizations/posthog/issues/?project=the-sentry-project-id&query=the-sentry-id', - $session_id: 'the-session-id', - $window_id: 'the-window-id', - $pageview_id: 'the-pageview-id', - $sent_at: '2023-06-03T10:03:57.787000+00:00', - $geoip_city_name: 'Whoville', - $geoip_country_name: 'Wholand', - $geoip_country_code: 'WH', - $geoip_continent_name: 'Mystery', - $geoip_continent_code: 'MY', - $geoip_latitude: -30.5023, - $geoip_longitude: -71.1545, - $geoip_time_zone: 'UTC', - $lib_version__major: 1, - $lib_version__minor: 63, - $lib_version__patch: 3, - ...properties, + stacktrace: { + frames: [ + { + colno: 0, + filename: 'https://app.posthog.com/home', + function: '?', + in_app: true, + lineno: 0, + }, + ], + }, + }, + ], + }, + $sentry_exception_message: 'ResizeObserver loop limit exceeded', + $sentry_exception_type: 'Error', + $sentry_tags: { + 'PostHog Person URL': 'https://app.posthog.com/person/the-person-id', + 'PostHog Recording URL': 'https://app.posthog.com/replay/the-session-id?t=866', }, - timestamp: '2023-06-03T03:03:57.316-07:00', - distinct_id: 'the-distinct-id', - elements_chain: '', + $sentry_url: + 'https://sentry.io/organizations/posthog/issues/?project=the-sentry-project-id&query=the-sentry-id', + $session_id: 'the-session-id', + $window_id: 'the-window-id', + $pageview_id: 'the-pageview-id', + $sent_at: '2023-06-03T10:03:57.787000+00:00', + $geoip_city_name: 'Whoville', + $geoip_country_name: 'Wholand', + $geoip_country_code: 'WH', + $geoip_continent_name: 'Mystery', + $geoip_continent_code: 'MY', + $geoip_latitude: -30.5023, + $geoip_longitude: -71.1545, + $geoip_time_zone: 'UTC', + $lib_version__major: 1, + $lib_version__minor: 63, + $lib_version__patch: 3, + ...properties, } } export function ResizeObserverLoopLimitExceeded(): JSX.Element { return ( ) } } -export function ErrorDisplay({ event }: { event: EventType | RecordingEventType }): JSX.Element { - if (event.event !== '$exception') { - return <>Unknown type of error - } - +export function ErrorDisplay({ eventProperties }: { eventProperties: EventType['properties'] }): JSX.Element { const { $exception_type, $exception_message, @@ -175,7 +171,7 @@ export function ErrorDisplay({ event }: { event: EventType | RecordingEventType $sentry_url, $exception_stack_trace_raw, $level, - } = getExceptionPropertiesFrom(event.properties) + } = getExceptionPropertiesFrom(eventProperties) return (
diff --git a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx index 673bd426629bf..427ad6daf6b5a 100644 --- a/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx +++ b/frontend/src/lib/components/PropertyFilters/components/PropertyValue.tsx @@ -128,7 +128,7 @@ export function PropertyValue({ loading={options[propertyKey]?.status === 'loading'} value={formattedValues} mode={isMultiSelect ? 'multiple' : 'single'} - allowCustomValues + allowCustomValues={options[propertyKey]?.allowCustomValues} onChange={(nextVal) => (isMultiSelect ? setValue(nextVal) : setValue(nextVal[0]))} onInputChange={onSearchTextChange} placeholder={placeholder} diff --git a/frontend/src/lib/components/PropertyFilters/utils.ts b/frontend/src/lib/components/PropertyFilters/utils.ts index 504c32e178748..da753040497ca 100644 --- a/frontend/src/lib/components/PropertyFilters/utils.ts +++ b/frontend/src/lib/components/PropertyFilters/utils.ts @@ -27,7 +27,7 @@ import { PropertyGroupFilterValue, PropertyOperator, PropertyType, - RecordingDurationFilter, + RecordingPropertyFilter, SessionPropertyFilter, } from '~/types' @@ -89,22 +89,21 @@ export function convertPropertyGroupToProperties( return properties } -export const PROPERTY_FILTER_TYPE_TO_TAXONOMIC_FILTER_GROUP_TYPE: Omit< - Record, - PropertyFilterType.Recording // Recording filters are not part of the taxonomic filter, only Replay-specific UI -> = { - [PropertyFilterType.Meta]: TaxonomicFilterGroupType.Metadata, - [PropertyFilterType.Person]: TaxonomicFilterGroupType.PersonProperties, - [PropertyFilterType.Event]: TaxonomicFilterGroupType.EventProperties, - [PropertyFilterType.Feature]: TaxonomicFilterGroupType.EventFeatureFlags, - [PropertyFilterType.Cohort]: TaxonomicFilterGroupType.Cohorts, - [PropertyFilterType.Element]: TaxonomicFilterGroupType.Elements, - [PropertyFilterType.Session]: TaxonomicFilterGroupType.SessionProperties, - [PropertyFilterType.HogQL]: TaxonomicFilterGroupType.HogQLExpression, - [PropertyFilterType.Group]: TaxonomicFilterGroupType.GroupsPrefix, - [PropertyFilterType.DataWarehouse]: TaxonomicFilterGroupType.DataWarehouse, - [PropertyFilterType.DataWarehousePersonProperty]: TaxonomicFilterGroupType.DataWarehousePersonProperties, -} +export const PROPERTY_FILTER_TYPE_TO_TAXONOMIC_FILTER_GROUP_TYPE: Record = + { + [PropertyFilterType.Meta]: TaxonomicFilterGroupType.Metadata, + [PropertyFilterType.Person]: TaxonomicFilterGroupType.PersonProperties, + [PropertyFilterType.Event]: TaxonomicFilterGroupType.EventProperties, + [PropertyFilterType.Feature]: TaxonomicFilterGroupType.EventFeatureFlags, + [PropertyFilterType.Cohort]: TaxonomicFilterGroupType.Cohorts, + [PropertyFilterType.Element]: TaxonomicFilterGroupType.Elements, + [PropertyFilterType.Session]: TaxonomicFilterGroupType.SessionProperties, + [PropertyFilterType.HogQL]: TaxonomicFilterGroupType.HogQLExpression, + [PropertyFilterType.Group]: TaxonomicFilterGroupType.GroupsPrefix, + [PropertyFilterType.DataWarehouse]: TaxonomicFilterGroupType.DataWarehouse, + [PropertyFilterType.DataWarehousePersonProperty]: TaxonomicFilterGroupType.DataWarehousePersonProperties, + [PropertyFilterType.Recording]: TaxonomicFilterGroupType.Replay, + } export function formatPropertyLabel( item: Record, @@ -200,7 +199,7 @@ export function isElementPropertyFilter(filter?: AnyFilterLike | null): filter i export function isSessionPropertyFilter(filter?: AnyFilterLike | null): filter is SessionPropertyFilter { return filter?.type === PropertyFilterType.Session } -export function isRecordingDurationFilter(filter?: AnyFilterLike | null): filter is RecordingDurationFilter { +export function isRecordingPropertyFilter(filter?: AnyFilterLike | null): filter is RecordingPropertyFilter { return filter?.type === PropertyFilterType.Recording } export function isGroupPropertyFilter(filter?: AnyFilterLike | null): filter is GroupPropertyFilter { @@ -223,7 +222,7 @@ export function isAnyPropertyfilter(filter?: AnyFilterLike | null): filter is An isElementPropertyFilter(filter) || isSessionPropertyFilter(filter) || isCohortPropertyFilter(filter) || - isRecordingDurationFilter(filter) || + isRecordingPropertyFilter(filter) || isFeaturePropertyFilter(filter) || isGroupPropertyFilter(filter) ) @@ -236,7 +235,7 @@ export function isPropertyFilterWithOperator( | PersonPropertyFilter | ElementPropertyFilter | SessionPropertyFilter - | RecordingDurationFilter + | RecordingPropertyFilter | FeaturePropertyFilter | GroupPropertyFilter | DataWarehousePropertyFilter { @@ -246,7 +245,7 @@ export function isPropertyFilterWithOperator( isPersonPropertyFilter(filter) || isElementPropertyFilter(filter) || isSessionPropertyFilter(filter) || - isRecordingDurationFilter(filter) || + isRecordingPropertyFilter(filter) || isFeaturePropertyFilter(filter) || isGroupPropertyFilter(filter) || isDataWarehousePropertyFilter(filter)) @@ -345,6 +344,10 @@ export function taxonomicFilterTypeToPropertyFilterType( return PropertyFilterType.DataWarehousePersonProperty } + if (filterType == TaxonomicFilterGroupType.Replay) { + return PropertyFilterType.Recording + } + return Object.entries(propertyFilterMapping).find(([, v]) => v === filterType)?.[0] as | PropertyFilterType | undefined diff --git a/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx b/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx index ea54e1ddfa2f7..6edef668ef432 100644 --- a/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx +++ b/frontend/src/lib/components/SocialLoginButton/SocialLoginButton.tsx @@ -39,10 +39,10 @@ function SocialLoginLink({ provider, extraQueryParams, children }: SocialLoginLi interface SocialLoginButtonProps { provider: SSOProvider - redirectQueryParams?: Record + extraQueryParams?: Record } -export function SocialLoginButton({ provider, redirectQueryParams }: SocialLoginButtonProps): JSX.Element | null { +export function SocialLoginButton({ provider, extraQueryParams }: SocialLoginButtonProps): JSX.Element | null { const { preflight } = useValues(preflightLogic) if (!preflight?.available_social_auth_providers[provider]) { @@ -50,7 +50,7 @@ export function SocialLoginButton({ provider, redirectQueryParams }: SocialLogin } return ( - + }> {SSO_PROVIDER_NAMES[provider]} @@ -65,7 +65,7 @@ interface SocialLoginButtonsProps { className?: string topDivider?: boolean bottomDivider?: boolean - redirectQueryParams?: Record + extraQueryParams?: Record } export function SocialLoginButtons({ @@ -109,14 +109,19 @@ export function SocialLoginButtons({ ) } -interface SSOEnforcedLoginButtonProps extends Partial { - provider: SSOProvider - email: string -} +type SSOEnforcedLoginButtonProps = SocialLoginButtonProps & + Partial & { + email: string + } -export function SSOEnforcedLoginButton({ provider, email, ...props }: SSOEnforcedLoginButtonProps): JSX.Element { +export function SSOEnforcedLoginButton({ + provider, + email, + extraQueryParams, + ...props +}: SSOEnforcedLoginButtonProps): JSX.Element { return ( - + ([ @@ -340,13 +341,14 @@ export const supportLogic = kea([ values.sendSupportRequest.kind ?? '', values.sendSupportRequest.target_area ?? '', values.sendSupportRequest.severity_level ?? '', + values.isEmailFormOpen ?? 'false', ].join(':') if (panelOptions !== ':') { actions.setSidePanelOptions(panelOptions) } }, - openSupportForm: async ({ name, email, kind, target_area, severity_level, message }) => { + openSupportForm: async ({ name, email, isEmailFormOpen, kind, target_area, severity_level, message }) => { let area = target_area ?? getURLPathToTargetArea(window.location.pathname) if (!userLogic.values.user) { area = 'login' @@ -361,6 +363,12 @@ export const supportLogic = kea([ message: message ?? '', }) + if (isEmailFormOpen === 'true' || isEmailFormOpen === true) { + actions.openEmailForm() + } else { + actions.closeEmailForm() + } + if (values.sidePanelAvailable) { const panelOptions = [kind ?? '', area ?? ''].join(':') actions.openSidePanel(SidePanelTab.Support, panelOptions === ':' ? undefined : panelOptions) @@ -509,12 +517,13 @@ export const supportLogic = kea([ const [panel, ...panelOptions] = (hashParams['panel'] ?? '').split(':') if (panel === SidePanelTab.Support) { - const [kind, area, severity] = panelOptions + const [kind, area, severity, isEmailFormOpen] = panelOptions actions.openSupportForm({ kind: Object.keys(SUPPORT_KIND_TO_SUBJECT).includes(kind) ? kind : null, target_area: Object.keys(TARGET_AREA_TO_NAME).includes(area) ? area : null, severity_level: Object.keys(SEVERITY_LEVEL_TO_NAME).includes(severity) ? severity : null, + isEmailFormOpen: isEmailFormOpen ?? 'false', }) return } diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx index 89bf8bbd9b2f3..f9579316b153f 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx @@ -68,7 +68,7 @@ export function InfiniteSelectResults({ selectItem(activeTaxonomicGroup, newValue, newValue)} + onChange={(newValue, item) => selectItem(activeTaxonomicGroup, newValue, item)} /> ) : ( diff --git a/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx b/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx index afc7919bbe72b..b90fe2aadf5d9 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InlineHogQLEditor.tsx @@ -5,7 +5,7 @@ import { AnyDataNode } from '~/queries/schema' export interface InlineHogQLEditorProps { value?: TaxonomicFilterValue - onChange: (value: TaxonomicFilterValue) => void + onChange: (value: TaxonomicFilterValue, item?: any) => void metadataSource?: AnyDataNode } diff --git a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx index 8b328a689d389..4c97fc48fc3e8 100644 --- a/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/taxonomicFilterLogic.tsx @@ -22,6 +22,7 @@ import { dataWarehouseSceneLogic } from 'scenes/data-warehouse/external/dataWare import { experimentsLogic } from 'scenes/experiments/experimentsLogic' import { featureFlagsLogic } from 'scenes/feature-flags/featureFlagsLogic' import { groupDisplayId } from 'scenes/persons/GroupActorDisplay' +import { ReplayTaxonomicFilters } from 'scenes/session-recordings/filters/ReplayTaxonomicFilters' import { teamLogic } from 'scenes/teamLogic' import { actionsModel } from '~/models/actionsModel' @@ -506,6 +507,13 @@ export const taxonomicFilterLogic = kea([ getPopoverHeader: () => 'HogQL', componentProps: { metadataSource }, }, + { + name: 'Replay', + searchPlaceholder: 'Replay', + type: TaxonomicFilterGroupType.Replay, + render: ReplayTaxonomicFilters, + getPopoverHeader: () => 'Replay', + }, ...groupAnalyticsTaxonomicGroups, ...groupAnalyticsTaxonomicGroupNames, ] diff --git a/frontend/src/lib/components/TaxonomicFilter/types.ts b/frontend/src/lib/components/TaxonomicFilter/types.ts index 787112ca04ea1..f9743bb18764e 100644 --- a/frontend/src/lib/components/TaxonomicFilter/types.ts +++ b/frontend/src/lib/components/TaxonomicFilter/types.ts @@ -47,7 +47,7 @@ export type TaxonomicFilterValue = string | number | null export type TaxonomicFilterRender = (props: { value?: TaxonomicFilterValue - onChange: (value: TaxonomicFilterValue) => void + onChange: (value: TaxonomicFilterValue, item: any) => void }) => JSX.Element | null export interface TaxonomicFilterGroup { @@ -108,6 +108,8 @@ export enum TaxonomicFilterGroupType { SessionProperties = 'session_properties', HogQLExpression = 'hogql_expression', Notebooks = 'notebooks', + // Misc + Replay = 'replay', } export interface InfiniteListLogicProps extends TaxonomicFilterLogicProps { diff --git a/frontend/src/lib/components/TimeSensitiveAuthentication/TimeSensitiveAuthentication.tsx b/frontend/src/lib/components/TimeSensitiveAuthentication/TimeSensitiveAuthentication.tsx index 6188a9f443bb7..f977f943754e7 100644 --- a/frontend/src/lib/components/TimeSensitiveAuthentication/TimeSensitiveAuthentication.tsx +++ b/frontend/src/lib/components/TimeSensitiveAuthentication/TimeSensitiveAuthentication.tsx @@ -21,6 +21,10 @@ export function TimeSensitiveAuthenticationModal(): JSX.Element { const ssoEnforcement = precheckResponse?.sso_enforcement const showPassword = !ssoEnforcement && user?.has_password + const extraQueryParams = { + next: window.location.pathname, + } + return ( - + ) : showPassword ? ( {precheckResponse?.saml_available ? ( - + ) : null}
) : null} diff --git a/frontend/src/lib/components/UniversalFilters/UniversalFilters.stories.tsx b/frontend/src/lib/components/UniversalFilters/UniversalFilters.stories.tsx index d1019f26cf5b0..0fb9248356ff0 100644 --- a/frontend/src/lib/components/UniversalFilters/UniversalFilters.stories.tsx +++ b/frontend/src/lib/components/UniversalFilters/UniversalFilters.stories.tsx @@ -58,8 +58,9 @@ export const Default: StoryFn = ({ group }) => { void - taxonomicEntityFilterGroupTypes: TaxonomicFilterGroupType[] - taxonomicPropertyFilterGroupTypes: TaxonomicFilterGroupType[] + taxonomicGroupTypes: TaxonomicFilterGroupType[] children?: React.ReactNode } @@ -35,8 +34,7 @@ function UniversalFilters({ rootKey, group = null, onChange, - taxonomicEntityFilterGroupTypes, - taxonomicPropertyFilterGroupTypes, + taxonomicGroupTypes, children, }: UniversalFiltersProps): JSX.Element { return ( @@ -46,8 +44,7 @@ function UniversalFilters({ rootKey, group, onChange, - taxonomicEntityFilterGroupTypes, - taxonomicPropertyFilterGroupTypes, + taxonomicGroupTypes, }} > {children} @@ -64,8 +61,7 @@ function Group({ index: number children: React.ReactNode }): JSX.Element { - const { rootKey, taxonomicEntityFilterGroupTypes, taxonomicPropertyFilterGroupTypes } = - useValues(universalFiltersLogic) + const { rootKey, taxonomicGroupTypes } = useValues(universalFiltersLogic) const { replaceGroupValue } = useActions(universalFiltersLogic) return ( @@ -74,8 +70,7 @@ function Group({ rootKey={`${rootKey}.group_${index}`} group={group} onChange={(group) => replaceGroupValue(index, group)} - taxonomicEntityFilterGroupTypes={taxonomicEntityFilterGroupTypes} - taxonomicPropertyFilterGroupTypes={taxonomicPropertyFilterGroupTypes} + taxonomicGroupTypes={taxonomicGroupTypes} > {children} diff --git a/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.test.ts b/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.test.ts index 448647033b49c..82b52b2e38053 100644 --- a/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.test.ts +++ b/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.test.ts @@ -32,8 +32,9 @@ describe('universalFiltersLogic', () => { logic = universalFiltersLogic({ rootKey: 'test', group: defaultFilter, - taxonomicEntityFilterGroupTypes: [TaxonomicFilterGroupType.Events, TaxonomicFilterGroupType.Actions], - taxonomicPropertyFilterGroupTypes: [ + taxonomicGroupTypes: [ + TaxonomicFilterGroupType.Events, + TaxonomicFilterGroupType.Actions, TaxonomicFilterGroupType.EventProperties, TaxonomicFilterGroupType.PersonProperties, ], diff --git a/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.ts b/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.ts index f6242e3d67447..9f40b436c3e3a 100644 --- a/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.ts +++ b/frontend/src/lib/components/UniversalFilters/universalFiltersLogic.ts @@ -6,7 +6,7 @@ import { import { taxonomicFilterGroupTypeToEntityType } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' import { propertyDefinitionsModel } from '~/models/propertyDefinitionsModel' -import { ActionFilter, FilterLogicalOperator } from '~/types' +import { ActionFilter, FilterLogicalOperator, PropertyFilterType } from '~/types' import { TaxonomicFilterGroup, TaxonomicFilterGroupType, TaxonomicFilterValue } from '../TaxonomicFilter/types' import { UniversalFiltersGroup, UniversalFiltersGroupValue } from './UniversalFilters' @@ -26,8 +26,7 @@ export type UniversalFiltersLogicProps = { rootKey: string group: UniversalFiltersGroup | null onChange: (group: UniversalFiltersGroup) => void - taxonomicEntityFilterGroupTypes: TaxonomicFilterGroupType[] - taxonomicPropertyFilterGroupTypes: TaxonomicFilterGroupType[] + taxonomicGroupTypes: TaxonomicFilterGroupType[] } export const universalFiltersLogic = kea([ @@ -50,7 +49,11 @@ export const universalFiltersLogic = kea([ }), removeGroupValue: (index: number) => ({ index }), - addGroupFilter: (taxonomicGroup: TaxonomicFilterGroup, propertyKey: TaxonomicFilterValue, item: any) => ({ + addGroupFilter: ( + taxonomicGroup: TaxonomicFilterGroup, + propertyKey: TaxonomicFilterValue, + item: { propertyFilterType?: PropertyFilterType; name?: string } + ) => ({ taxonomicGroup, propertyKey, item, @@ -83,11 +86,20 @@ export const universalFiltersLogic = kea([ selectors({ rootKey: [(_, p) => [p.rootKey], (rootKey) => rootKey], - taxonomicEntityFilterGroupTypes: [(_, p) => [p.taxonomicEntityFilterGroupTypes], (types) => types], - taxonomicPropertyFilterGroupTypes: [(_, p) => [p.taxonomicPropertyFilterGroupTypes], (types) => types], - taxonomicGroupTypes: [ - (_, p) => [p.taxonomicEntityFilterGroupTypes, p.taxonomicPropertyFilterGroupTypes], - (entityTypes, propertyTypes) => [...entityTypes, ...propertyTypes], + taxonomicGroupTypes: [(_, p) => [p.taxonomicGroupTypes], (types) => types], + taxonomicPropertyFilterGroupTypes: [ + (_, p) => [p.taxonomicGroupTypes], + (types) => + types.filter((t) => + [ + TaxonomicFilterGroupType.EventProperties, + TaxonomicFilterGroupType.PersonProperties, + TaxonomicFilterGroupType.EventFeatureFlags, + TaxonomicFilterGroupType.Cohorts, + TaxonomicFilterGroupType.Elements, + TaxonomicFilterGroupType.HogQLExpression, + ].includes(t) + ), ], }), @@ -112,7 +124,7 @@ export const universalFiltersLogic = kea([ newValues.push(newPropertyFilter) } else { - const entityType = item.PropertyFilterType ?? taxonomicFilterGroupTypeToEntityType(taxonomicGroup.type) + const entityType = taxonomicFilterGroupTypeToEntityType(taxonomicGroup.type) if (entityType) { const newEntityFilter: ActionFilter = { id: propertyKey, diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index cdca3add1b2cd..07e8c6d51b86f 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -167,6 +167,7 @@ export const FEATURE_FLAGS = { APPS_AND_EXPORTS_UI: 'apps-and-exports-ui', // owner: @benjackwhite HOGQL_INSIGHT_LIVE_COMPARE: 'hogql-insight-live-compare', // owner: @mariusandra HOGQL_DASHBOARD_CARDS: 'hogql-dashboard-cards', // owner: @thmsobrmlr + HOGQL_DASHBOARD_ASYNC: 'hogql-dashboard-async', // owner: @webjunkie WEBHOOKS_DENYLIST: 'webhooks-denylist', // owner: #team-pipeline PERSONS_HOGQL_QUERY: 'persons-hogql-query', // owner: @mariusandra PIPELINE_UI: 'pipeline-ui', // owner: #team-pipeline @@ -205,9 +206,13 @@ export const FEATURE_FLAGS = { SESSION_REPLAY_NETWORK_VIEW: 'session-replay-network-view', // owner: #team-replay SETTINGS_PERSONS_JOIN_MODE: 'settings-persons-join-mode', // owner: @robbie-c HOG: 'hog', // owner: @mariusandra + HOG_FUNCTIONS: 'hog-functions', // owner: #team-cdp PERSONLESS_EVENTS_NOT_SUPPORTED: 'personless-events-not-supported', // owner: @raquelmsmith + SESSION_REPLAY_UNIVERSAL_FILTERS: 'session-replay-universal-filters', // owner: #team-replay ALERTS: 'alerts', // owner: github.com/nikitaevg + ERROR_TRACKING: 'error-tracking', // owner: #team-replay SETTINGS_BOUNCE_RATE_PAGE_VIEW_MODE: 'settings-bounce-rate-page-view-mode', // owner: @robbie-c + SURVEYS_BRANCHING_LOGIC: 'surveys-branching-logic', // owner: @jurajmajerik #team-feature-success } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/lib/taxonomy.tsx b/frontend/src/lib/taxonomy.tsx index 59abaaa055fca..17fd94ac793de 100644 --- a/frontend/src/lib/taxonomy.tsx +++ b/frontend/src/lib/taxonomy.tsx @@ -1075,6 +1075,17 @@ export const CORE_FILTER_DEFINITIONS_BY_GROUP = { description: 'Specified group key', }, }, + replay: { + console_log_level: { + label: 'Log level', + description: 'Level of console logs captured', + examples: ['info', 'warn', 'error'], + }, + console_log_query: { + label: 'Console log', + description: 'Text of console logs captured', + }, + }, } satisfies Partial>> CORE_FILTER_DEFINITIONS_BY_GROUP.numerical_event_properties = CORE_FILTER_DEFINITIONS_BY_GROUP.event_properties diff --git a/frontend/src/lib/utils/apiHost.ts b/frontend/src/lib/utils/apiHost.ts index bece963761284..75d18c2bf060f 100644 --- a/frontend/src/lib/utils/apiHost.ts +++ b/frontend/src/lib/utils/apiHost.ts @@ -15,6 +15,5 @@ export function liveEventsHostOrigin(): string | null { } else if (appOrigin === 'https://eu.posthog.com') { return 'https://live.eu.posthog.com' } - // TODO(@zach): add dev and local env support - return null + return 'http://localhost:8666' } diff --git a/frontend/src/models/propertyDefinitionsModel.ts b/frontend/src/models/propertyDefinitionsModel.ts index 497a218b3eaaf..1e9a165e02614 100644 --- a/frontend/src/models/propertyDefinitionsModel.ts +++ b/frontend/src/models/propertyDefinitionsModel.ts @@ -55,6 +55,7 @@ export type Option = { label?: string name?: string status?: 'loading' | 'loaded' + allowCustomValues?: boolean values?: PropValue[] } @@ -149,7 +150,11 @@ export const propertyDefinitionsModel = kea([ eventNames?: string[] }) => payload, setOptionsLoading: (key: string) => ({ key }), - setOptions: (key: string, values: PropValue[]) => ({ key, values }), + setOptions: (key: string, values: PropValue[], allowCustomValues: boolean) => ({ + key, + values, + allowCustomValues, + }), // internal fetchAllPendingDefinitions: true, abortAnyRunningQuery: true, @@ -170,11 +175,12 @@ export const propertyDefinitionsModel = kea([ {} as Record, { setOptionsLoading: (state, { key }) => ({ ...state, [key]: { ...state[key], status: 'loading' } }), - setOptions: (state, { key, values }) => ({ + setOptions: (state, { key, values, allowCustomValues }) => ({ ...state, [key]: { values: [...Array.from(new Set(values))], status: 'loaded', + allowCustomValues, }, }), }, @@ -317,6 +323,19 @@ export const propertyDefinitionsModel = kea([ if (!propertyKey || values.currentTeamId === null) { return } + if (propertyKey === 'console_log_level') { + actions.setOptions( + propertyKey, + [ + // id is not used so can be arbitrarily chosen + { id: 0, name: 'info' }, + { id: 1, name: 'warn' }, + { id: 2, name: 'error' }, + ], + false + ) + return + } const start = performance.now() @@ -334,7 +353,7 @@ export const propertyDefinitionsModel = kea([ methodOptions ) breakpoint() - actions.setOptions(propertyKey, propValues) + actions.setOptions(propertyKey, propValues, true) cache.abortController = null await captureTimeToSeeData(teamLogic.values.currentTeamId, { diff --git a/frontend/src/queries/nodes/InsightViz/InsightViz.scss b/frontend/src/queries/nodes/InsightViz/InsightViz.scss index 3d3dde0915dff..67c5fe6145298 100644 --- a/frontend/src/queries/nodes/InsightViz/InsightViz.scss +++ b/frontend/src/queries/nodes/InsightViz/InsightViz.scss @@ -29,7 +29,8 @@ flex-direction: column; .NotebookNode &, - .InsightCard & { + .InsightCard &, + .WebAnalyticsDashboard & { flex: 1; height: 100%; @@ -102,7 +103,8 @@ } .NotebookNode &, - .InsightCard & { + .InsightCard &, + .WebAnalyticsDashboard & { .LineGraph { position: relative; min-height: 100px; @@ -119,7 +121,8 @@ margin: 0.5rem; .NotebookNode &, - .InsightCard & { + .InsightCard &, + .WebAnalyticsDashboard & { min-height: auto; } @@ -149,7 +152,8 @@ min-height: var(--insight-viz-min-height); .NotebookNode &, - .InsightCard & { + .InsightCard &, + .WebAnalyticsDashboard & { min-height: auto; } } diff --git a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx index cde744f031014..2d3ef56c69272 100644 --- a/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx +++ b/frontend/src/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect.tsx @@ -1,4 +1,4 @@ -import { LemonSelect } from '@posthog/lemon-ui' +import { LemonButtonProps, LemonSelect } from '@posthog/lemon-ui' import { FilterLogicalOperator } from '~/types' @@ -8,6 +8,7 @@ interface AndOrFilterSelectProps { topLevelFilter?: boolean prefix?: React.ReactNode suffix?: [singular: string, plural: string] + disabledReason?: LemonButtonProps['disabledReason'] } export function AndOrFilterSelect({ @@ -16,6 +17,7 @@ export function AndOrFilterSelect({ topLevelFilter, prefix = 'Match', suffix = ['filter in this group', 'filters in this group'], + disabledReason, }: AndOrFilterSelectProps): JSX.Element { return (
@@ -25,6 +27,7 @@ export function AndOrFilterSelect({ size="small" value={value} onChange={(type) => onChange(type as FilterLogicalOperator)} + disabledReason={disabledReason} options={[ { label: 'all', diff --git a/frontend/src/queries/nodes/InsightViz/utils.ts b/frontend/src/queries/nodes/InsightViz/utils.ts index 06d27e0801fd7..77c671d0abcf1 100644 --- a/frontend/src/queries/nodes/InsightViz/utils.ts +++ b/frontend/src/queries/nodes/InsightViz/utils.ts @@ -167,7 +167,7 @@ export function getQueryBasedInsightModel(insight: let query if (insight.query) { query = insight.query - } else if (insight.filters && Object.keys(insight.filters).length > 0) { + } else if (insight.filters && Object.keys(insight.filters).filter((k) => k != 'filter_test_accounts').length > 0) { query = { kind: NodeKind.InsightVizNode, source: filtersToQueryNode(insight.filters) } as InsightVizNode } else { query = null diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 38b26efae384c..d4015b9fa212e 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -279,7 +279,7 @@ "$ref": "#/definitions/CohortPropertyFilter" }, { - "$ref": "#/definitions/RecordingDurationFilter" + "$ref": "#/definitions/RecordingPropertyFilter" }, { "$ref": "#/definitions/GroupPropertyFilter" @@ -2639,6 +2639,10 @@ "Day": { "type": "integer" }, + "DurationType": { + "enum": ["duration", "active_seconds", "inactive_seconds"], + "type": "string" + }, "ElementPropertyFilter": { "additionalProperties": false, "description": "Sync with plugin-server/src/types.ts", @@ -6964,12 +6968,23 @@ "required": ["k", "t"], "type": "object" }, - "RecordingDurationFilter": { + "RecordingPropertyFilter": { "additionalProperties": false, "properties": { "key": { - "const": "duration", - "type": "string" + "anyOf": [ + { + "$ref": "#/definitions/DurationType" + }, + { + "const": "console_log_level", + "type": "string" + }, + { + "const": "console_log_query", + "type": "string" + } + ] }, "label": { "type": "string" @@ -6982,10 +6997,10 @@ "type": "string" }, "value": { - "type": "number" + "$ref": "#/definitions/PropertyFilterValue" } }, - "required": ["key", "operator", "type", "value"], + "required": ["key", "operator", "type"], "type": "object" }, "RefreshType": { diff --git a/frontend/src/scenes/actions/Action.tsx b/frontend/src/scenes/actions/Action.tsx index 05080a78324e4..5ff2bdc067eb0 100644 --- a/frontend/src/scenes/actions/Action.tsx +++ b/frontend/src/scenes/actions/Action.tsx @@ -11,7 +11,6 @@ import { NodeKind } from '~/queries/schema' import { ActionType } from '~/types' import { ActionEdit } from './ActionEdit' -import { ActionPlugins } from './ActionPlugins' export const scene: SceneExport = { logic: actionLogic, @@ -47,8 +46,6 @@ export function Action({ id }: { id?: ActionType['id'] } = {}): JSX.Element { {id && ( <> - - {isComplete ? (

Matching events

diff --git a/frontend/src/scenes/actions/ActionPlugins.tsx b/frontend/src/scenes/actions/ActionPlugins.tsx deleted file mode 100644 index 56d6807cf35a8..0000000000000 --- a/frontend/src/scenes/actions/ActionPlugins.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { LemonButton, LemonTable } from '@posthog/lemon-ui' -import { useActions, useValues } from 'kea' -import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' -import { useEffect } from 'react' -import { actionLogic } from 'scenes/actions/actionLogic' -import { PluginImage } from 'scenes/plugins/plugin/PluginImage' -import { urls } from 'scenes/urls' - -import { PipelineNodeTab, PipelineStage } from '~/types' - -export function ActionPlugins(): JSX.Element | null { - const { matchingPluginConfigs } = useValues(actionLogic) - const { loadMatchingPluginConfigs } = useActions(actionLogic) - - useEffect(() => { - loadMatchingPluginConfigs() - }, []) - - if (!matchingPluginConfigs?.length) { - return null - } - - return ( - <> -

Connected data pipelines

- - ( -
- - -
- ), - }, - { - title: '', - width: 0, - render: (_, config) => ( - - Configure - - ), - }, - ]} - /> - - ) -} diff --git a/frontend/src/scenes/activity/explore/EventDetails.tsx b/frontend/src/scenes/activity/explore/EventDetails.tsx index e512838f0020a..8226a7289e266 100644 --- a/frontend/src/scenes/activity/explore/EventDetails.tsx +++ b/frontend/src/scenes/activity/explore/EventDetails.tsx @@ -93,7 +93,7 @@ export function EventDetails({ event, tableProps }: EventDetailsProps): JSX.Elem label: 'Exception', content: (
- +
), }) diff --git a/frontend/src/scenes/activity/live/LiveEventsTable.tsx b/frontend/src/scenes/activity/live/LiveEventsTable.tsx index 9ced6ffbc69a7..a53ff7a1a1a5d 100644 --- a/frontend/src/scenes/activity/live/LiveEventsTable.tsx +++ b/frontend/src/scenes/activity/live/LiveEventsTable.tsx @@ -62,7 +62,7 @@ export function LiveEventsTable(): JSX.Element { - Active users: {stats?.users_on_product ?? '—'} + Users active right now: {stats?.users_on_product ?? '—'}
diff --git a/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx b/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx index efc76ece220f3..a3fe1e9651635 100644 --- a/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx +++ b/frontend/src/scenes/activity/live/liveEventsTableLogic.tsx @@ -18,7 +18,6 @@ export const liveEventsTableLogic = kea([ addEvents: (events) => ({ events }), clearEvents: true, setFilters: (filters) => ({ filters }), - updateEventsSource: (source) => ({ source }), updateEventsConnection: true, pauseStream: true, resumeStream: true, @@ -54,12 +53,6 @@ export const liveEventsTableLogic = kea([ setClientSideFilters: (_, { clientSideFilters }) => clientSideFilters, }, ], - eventsSource: [ - null as EventSource | null, - { - updateEventsSource: (_, { source }) => source, - }, - ], streamPaused: [ false, { @@ -110,8 +103,8 @@ export const liveEventsTableLogic = kea([ actions.updateEventsConnection() }, updateEventsConnection: async () => { - if (values.eventsSource) { - values.eventsSource.close() + if (cache.eventsSource) { + cache.eventsSource.close() } if (values.streamPaused) { @@ -124,14 +117,13 @@ export const liveEventsTableLogic = kea([ const { eventType } = values.filters const url = new URL(`${liveEventsHostOrigin()}/events`) - url.searchParams.append('teamId', values.currentTeam.id.toString()) if (eventType) { url.searchParams.append('eventType', eventType) } const source = new window.EventSourcePolyfill(url.toString(), { headers: { - Authorization: `Bearer ${values.currentTeam?.live_events_token}`, + Authorization: `Bearer ${values.currentTeam.live_events_token}`, }, }) @@ -158,11 +150,11 @@ export const liveEventsTableLogic = kea([ } } - actions.updateEventsSource(source) + cache.eventsSource = source }, pauseStream: () => { - if (values.eventsSource) { - values.eventsSource.close() + if (cache.eventsSource) { + cache.eventsSource.close() } }, resumeStream: () => { @@ -174,14 +166,11 @@ export const liveEventsTableLogic = kea([ return } - const response = await fetch( - `${liveEventsHostOrigin()}/stats?teamId=${values.currentTeam.id.toString()}`, - { - headers: { - Authorization: `Bearer ${values.currentTeam?.live_events_token}`, - }, - } - ) + const response = await fetch(`${liveEventsHostOrigin()}/stats`, { + headers: { + Authorization: `Bearer ${values.currentTeam.live_events_token}`, + }, + }) const data = await response.json() actions.setStats(data) } catch (error) { @@ -189,21 +178,19 @@ export const liveEventsTableLogic = kea([ } }, })), - events(({ actions, values }) => ({ + events(({ actions, cache }) => ({ afterMount: () => { - if (!liveEventsHostOrigin()) { - return - } - actions.updateEventsConnection() - const interval = setInterval(() => { + cache.statsInterval = setInterval(() => { actions.pollStats() }, 1500) - return () => { - if (values.eventsSource) { - values.eventsSource.close() - } - clearInterval(interval) + }, + beforeUnmount: () => { + if (cache.eventsSource) { + cache.eventsSource.close() + } + if (cache.statsInterval) { + clearInterval(cache.statsInterval) } }, })), diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index 07da32a054160..01903f190d5c2 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -37,6 +37,7 @@ export const appScenes: Record any> = { [Scene.FeatureFlag]: () => import('./feature-flags/FeatureFlag'), [Scene.EarlyAccessFeatures]: () => import('./early-access-features/EarlyAccessFeatures'), [Scene.EarlyAccessFeature]: () => import('./early-access-features/EarlyAccessFeature'), + [Scene.ErrorTracking]: () => import('./error-tracking/ErrorTrackingScene'), [Scene.Surveys]: () => import('./surveys/Surveys'), [Scene.Survey]: () => import('./surveys/Survey'), [Scene.SurveyTemplates]: () => import('./surveys/SurveyTemplates'), diff --git a/frontend/src/scenes/authentication/InviteSignup.tsx b/frontend/src/scenes/authentication/InviteSignup.tsx index af96ad730bd15..32a612d36e93b 100644 --- a/frontend/src/scenes/authentication/InviteSignup.tsx +++ b/frontend/src/scenes/authentication/InviteSignup.tsx @@ -285,7 +285,7 @@ function UnauthenticatedAcceptInvite({ invite }: { invite: PrevalidatedInvite }) caption={`Remember to log in with ${invite?.target_email}`} captionLocation="bottom" topDivider - redirectQueryParams={invite ? { invite_id: invite.id } : undefined} + extraQueryParams={invite ? { invite_id: invite.id } : undefined} /> ) diff --git a/frontend/src/scenes/billing/BillingProductAddon.tsx b/frontend/src/scenes/billing/BillingProductAddon.tsx index b39fd32f0d51d..57a9c2edb2803 100644 --- a/frontend/src/scenes/billing/BillingProductAddon.tsx +++ b/frontend/src/scenes/billing/BillingProductAddon.tsx @@ -97,7 +97,7 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp {is_enhanced_persons_og_customer && (

([ }, ], })), - events(({ actions, cache, props }) => ({ + events(({ actions, cache, props, values }) => ({ afterMount: () => { if (props.id) { if (props.dashboard) { @@ -843,7 +844,7 @@ export const dashboardLogic = kea([ actions.loadDashboardSuccess(props.dashboard) } else { actions.loadDashboard({ - refresh: 'force_cache', + refresh: values.featureFlags[FEATURE_FLAGS.HOGQL_DASHBOARD_ASYNC] ? 'async' : 'force_cache', action: 'initial_load', }) } @@ -966,7 +967,7 @@ export const dashboardLogic = kea([ const insightsToRefresh = values .sortTilesByLayout(tiles || values.insightTiles || []) .filter((t) => { - if (!initialLoad || !t.last_refresh) { + if (!initialLoad || !t.last_refresh || !!t.insight?.query_status) { return true } @@ -1016,7 +1017,13 @@ export const dashboardLogic = kea([ const queryId = `${dashboardQueryId}::${uuid()}` const queryStartTime = performance.now() const apiUrl = `api/projects/${values.currentTeamId}/insights/${insight.id}/?${toParams({ - refresh: hardRefreshWithoutCache ? 'force_blocking' : 'blocking', + refresh: values.featureFlags[FEATURE_FLAGS.HOGQL_DASHBOARD_ASYNC] + ? hardRefreshWithoutCache + ? 'force_async' + : 'async' + : hardRefreshWithoutCache + ? 'force_blocking' + : 'blocking', from_dashboard: dashboardId, // needed to load insight in correct context client_query_id: queryId, session_id: currentSessionId(), @@ -1056,29 +1063,22 @@ export const dashboardLogic = kea([ } if (refreshedInsight.query_status) { - pollForResults(refreshedInsight.query_status.id, false, methodOptions) - .then(async () => { - const apiUrl = `api/projects/${values.currentTeamId}/insights/${insight.id}/?${toParams( - { - refresh: 'async', - from_dashboard: dashboardId, // needed to load insight in correct context - client_query_id: queryId, - session_id: currentSessionId(), - } - )}` - // TODO: We get the insight again here to get everything in the right format (e.g. because of result vs results) - const refreshedInsightResponse: Response = await api.getResponse(apiUrl, methodOptions) - const refreshedInsight: InsightModel = await getJSONOrNull(refreshedInsightResponse) - dashboardsModel.actions.updateDashboardInsight( - refreshedInsight, - [], - props.id ? [props.id] : undefined - ) - actions.setRefreshStatus(insight.short_id) - }) - .catch(() => { - actions.setRefreshError(insight.short_id) - }) + await pollForResults(refreshedInsight.query_status.id, false, methodOptions) + const apiUrl = `api/projects/${values.currentTeamId}/insights/${insight.id}/?${toParams({ + refresh: 'async', + from_dashboard: dashboardId, // needed to load insight in correct context + client_query_id: queryId, + session_id: currentSessionId(), + })}` + // TODO: We get the insight again here to get everything in the right format (e.g. because of result vs results) + const polledInsightResponse: Response = await api.getResponse(apiUrl, methodOptions) + const polledInsight: InsightModel = await getJSONOrNull(polledInsightResponse) + dashboardsModel.actions.updateDashboardInsight( + polledInsight, + [], + props.id ? [props.id] : undefined + ) + actions.setRefreshStatus(insight.short_id) } else { actions.setRefreshStatus(insight.short_id) } @@ -1184,6 +1184,7 @@ export const dashboardLogic = kea([ // Initial load of actual data for dashboard items after general dashboard is fetched if ( + !values.featureFlags[FEATURE_FLAGS.HOGQL_DASHBOARD_ASYNC] && // with async we straight up want to loop through all items values.oldestRefreshed && values.oldestRefreshed.isBefore(now().subtract(AUTO_REFRESH_DASHBOARD_THRESHOLD_HOURS, 'hours')) && !process.env.STORYBOOK // allow mocking of date in storybook without triggering refresh @@ -1191,11 +1192,12 @@ export const dashboardLogic = kea([ actions.refreshAllDashboardItems({ action: 'refresh', initialLoad, dashboardQueryId }) allLoaded = false } else { - const tilesWithNoResults = values.tiles?.filter((t) => !!t.insight && !t.insight.result) || [] + const tilesWithNoOrQueuedResults = + values.tiles?.filter((t) => !!t.insight && (!t.insight.result || !!t.insight.query_status)) || [] - if (tilesWithNoResults.length) { + if (tilesWithNoOrQueuedResults.length) { actions.refreshAllDashboardItems({ - tiles: tilesWithNoResults, + tiles: tilesWithNoOrQueuedResults, action: 'load_missing', initialLoad, dashboardQueryId, diff --git a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx index 5a01206bd52cf..df06fc5df3054 100644 --- a/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx +++ b/frontend/src/scenes/data-warehouse/ViewLinkModal.tsx @@ -184,7 +184,7 @@ export function ViewLinkForm(): JSX.Element {

- + {sqlCodeSnippet}
diff --git a/frontend/src/scenes/data-warehouse/settings/DataWarehouseSourcesTable.tsx b/frontend/src/scenes/data-warehouse/settings/DataWarehouseSourcesTable.tsx index d4e9082c55c49..2a0dd8b31314b 100644 --- a/frontend/src/scenes/data-warehouse/settings/DataWarehouseSourcesTable.tsx +++ b/frontend/src/scenes/data-warehouse/settings/DataWarehouseSourcesTable.tsx @@ -1,5 +1,15 @@ import { TZLabel } from '@posthog/apps-common' -import { LemonButton, LemonDialog, LemonSwitch, LemonTable, LemonTag, Link, Spinner, Tooltip } from '@posthog/lemon-ui' +import { + LemonButton, + LemonDialog, + LemonSelect, + LemonSwitch, + LemonTable, + LemonTag, + Link, + Spinner, + Tooltip, +} from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { router } from 'kea-router' import { ProductIntroduction } from 'lib/components/ProductIntroduction/ProductIntroduction' @@ -14,10 +24,10 @@ import { urls } from 'scenes/urls' import { DataTableNode, NodeKind } from '~/queries/schema' import { + DataWarehouseSyncInterval, ExternalDataSourceSchema, ExternalDataSourceType, ExternalDataStripeSource, - PipelineInterval, ProductKey, } from '~/types' @@ -33,7 +43,7 @@ const StatusTagSetting = { export function DataWarehouseSourcesTable(): JSX.Element { const { dataWarehouseSources, dataWarehouseSourcesLoading, sourceReloadingById } = useValues(dataWarehouseSettingsLogic) - const { deleteSource, reloadSource } = useActions(dataWarehouseSettingsLogic) + const { deleteSource, reloadSource, updateSource } = useActions(dataWarehouseSettingsLogic) const renderExpandable = (source: ExternalDataStripeSource): JSX.Element => { return ( @@ -90,8 +100,20 @@ export function DataWarehouseSourcesTable(): JSX.Element { { title: 'Sync Frequency', key: 'frequency', - render: function RenderFrequency() { - return 'day' as PipelineInterval + render: function RenderFrequency(_, source) { + return ( + + updateSource({ ...source, sync_frequency: value as DataWarehouseSyncInterval }) + } + options={[ + { value: 'day' as DataWarehouseSyncInterval, label: 'Daily' }, + { value: 'week' as DataWarehouseSyncInterval, label: 'Weekly' }, + { value: 'month' as DataWarehouseSyncInterval, label: 'Monthly' }, + ]} + /> + ) }, }, { diff --git a/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts b/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts index a9cd46d0360da..19795ffe3c37a 100644 --- a/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts +++ b/frontend/src/scenes/data-warehouse/settings/dataWarehouseSettingsLogic.ts @@ -25,7 +25,7 @@ export const dataWarehouseSettingsLogic = kea([ updateSchema: (schema: ExternalDataSourceSchema) => ({ schema }), abortAnyRunningQuery: true, }), - loaders(({ cache, actions }) => ({ + loaders(({ cache, actions, values }) => ({ dataWarehouseSources: [ null as PaginatedResponse | null, { @@ -45,6 +45,15 @@ export const dataWarehouseSettingsLogic = kea([ return res }, + updateSource: async (source: ExternalDataStripeSource) => { + const updatedSource = await api.externalDataSources.update(source.id, source) + return { + ...values.dataWarehouseSources, + results: + values.dataWarehouseSources?.results.map((s) => (s.id === updatedSource.id ? source : s)) || + [], + } + }, }, ], })), diff --git a/frontend/src/scenes/heatmaps/HeatmapsBrowser.tsx b/frontend/src/scenes/heatmaps/HeatmapsBrowser.tsx index c1d395eb7ff8c..af088a550cacd 100644 --- a/frontend/src/scenes/heatmaps/HeatmapsBrowser.tsx +++ b/frontend/src/scenes/heatmaps/HeatmapsBrowser.tsx @@ -1,4 +1,4 @@ -import { IconCollapse } from '@posthog/icons' +import { IconCollapse, IconGear } from '@posthog/icons' import { LemonBanner, LemonButton, LemonInputSelect, LemonSkeleton, Spinner, Tooltip } from '@posthog/lemon-ui' import { BindLogic, useActions, useValues } from 'kea' import { AuthorizedUrlList } from 'lib/components/AuthorizedUrlList/AuthorizedUrlList' @@ -10,6 +10,9 @@ import { DetectiveHog } from 'lib/components/hedgehogs' import { useResizeObserver } from 'lib/hooks/useResizeObserver' import { IconChevronRight, IconOpenInNew } from 'lib/lemon-ui/icons' import React, { useEffect, useRef } from 'react' +import { teamLogic } from 'scenes/teamLogic' + +import { sidePanelSettingsLogic } from '~/layout/navigation-3000/sidepanel/panels/sidePanelSettingsLogic' import { heatmapsBrowserLogic } from './heatmapsBrowserLogic' @@ -260,6 +263,28 @@ function EmbeddedHeatmapBrowser({ ) : null } +function Warnings(): JSX.Element | null { + const { currentTeam } = useValues(teamLogic) + const heatmapsEnabled = currentTeam?.heatmaps_opt_in + + const { openSettingsPanel } = useActions(sidePanelSettingsLogic) + + return !heatmapsEnabled ? ( + , + onClick: () => openSettingsPanel({ settingId: 'heatmaps' }), + children: 'Configure', + }} + dismissKey="heatmaps-might-be-disabled-warning" + > + You aren't collecting heatmaps data. Enable heatmaps in your project. + + ) : null +} + export function HeatmapsBrowser(): JSX.Element { const iframeRef = useRef(null) @@ -271,7 +296,8 @@ export function HeatmapsBrowser(): JSX.Element { return ( -
+
+
diff --git a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nuxt.tsx b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nuxt.tsx index e57a46e534d61..baa4947ad4950 100644 --- a/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nuxt.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdk-install-instructions/nuxt.tsx @@ -35,7 +35,7 @@ import posthog from 'posthog-js' export default defineNuxtPlugin(nuxtApp => { const runtimeConfig = useRuntimeConfig(); const posthogClient = posthog.init(runtimeConfig.public.posthogPublicKey, { - api_host: runtimeConfig.public.posthogHost', + api_host: runtimeConfig.public.posthogHost, ${ isPersonProfilesDisabled ? `` diff --git a/frontend/src/scenes/pipeline/Destinations.tsx b/frontend/src/scenes/pipeline/Destinations.tsx index 61af57f3061c5..500111bdc5d1b 100644 --- a/frontend/src/scenes/pipeline/Destinations.tsx +++ b/frontend/src/scenes/pipeline/Destinations.tsx @@ -57,10 +57,14 @@ export function DestinationsTable({ inOverview = false }: { inOverview?: boolean title: 'App', width: 0, render: function RenderAppInfo(_, destination) { - if (destination.backend === 'plugin') { - return + switch (destination.backend) { + case 'plugin': + return + case 'batch_export': + return + default: + return null } - return }, }, { diff --git a/frontend/src/scenes/pipeline/PipelineNodeConfiguration.tsx b/frontend/src/scenes/pipeline/PipelineNodeConfiguration.tsx index 61d09e36015c4..926469a85ffd6 100644 --- a/frontend/src/scenes/pipeline/PipelineNodeConfiguration.tsx +++ b/frontend/src/scenes/pipeline/PipelineNodeConfiguration.tsx @@ -1,6 +1,7 @@ import { useValues } from 'kea' import { NotFound } from 'lib/components/NotFound' +import { PipelineHogFunctionConfiguration } from './hogfunctions/PipelineHogFunctionConfiguration' import { PipelineBatchExportConfiguration } from './PipelineBatchExportConfiguration' import { pipelineNodeLogic } from './pipelineNodeLogic' import { PipelinePluginConfiguration } from './PipelinePluginConfiguration' @@ -15,7 +16,9 @@ export function PipelineNodeConfiguration(): JSX.Element { return (
- {node.backend === PipelineBackend.Plugin ? ( + {node.backend === PipelineBackend.HogFunction ? ( + + ) : node.backend === PipelineBackend.Plugin ? ( ) : ( diff --git a/frontend/src/scenes/pipeline/PipelineNodeNew.tsx b/frontend/src/scenes/pipeline/PipelineNodeNew.tsx index 30504da3fe51d..76eea7518a6b0 100644 --- a/frontend/src/scenes/pipeline/PipelineNodeNew.tsx +++ b/frontend/src/scenes/pipeline/PipelineNodeNew.tsx @@ -1,17 +1,20 @@ import { IconPlusSmall } from '@posthog/icons' import { useValues } from 'kea' +import { combineUrl, router } from 'kea-router' import { NotFound } from 'lib/components/NotFound' import { PayGateMini } from 'lib/components/PayGateMini/PayGateMini' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonTable } from 'lib/lemon-ui/LemonTable' import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { AvailableFeature, BatchExportService, PipelineStage, PluginType } from '~/types' +import { AvailableFeature, BatchExportService, HogFunctionTemplateType, PipelineStage, PluginType } from '~/types' import { pipelineDestinationsLogic } from './destinationsLogic' import { frontendAppsLogic } from './frontendAppsLogic' +import { PipelineHogFunctionConfiguration } from './hogfunctions/PipelineHogFunctionConfiguration' import { PipelineBatchExportConfiguration } from './PipelineBatchExportConfiguration' import { PIPELINE_TAB_TO_NODE_STAGE } from './PipelineNode' import { pipelineNodeNewLogic, PipelineNodeNewLogicProps } from './pipelineNodeNewLogic' @@ -21,21 +24,20 @@ import { PipelineBackend } from './types' import { getBatchExportUrl, RenderApp, RenderBatchExportIcon } from './utils' const paramsToProps = ({ - params: { stage, pluginIdOrBatchExportDestination }, + params: { stage, id }, }: { - params: { stage?: string; pluginIdOrBatchExportDestination?: string } + params: { stage?: string; id?: string } }): PipelineNodeNewLogicProps => { - const numericId = - pluginIdOrBatchExportDestination && /^\d+$/.test(pluginIdOrBatchExportDestination) - ? parseInt(pluginIdOrBatchExportDestination) - : undefined + const numericId = id && /^\d+$/.test(id) ? parseInt(id) : undefined const pluginId = numericId && !isNaN(numericId) ? numericId : null - const batchExportDestination = pluginId ? null : pluginIdOrBatchExportDestination ?? null + const hogFunctionId = pluginId ? null : id?.startsWith('hog-') ? id.slice(4) : null + const batchExportDestination = hogFunctionId ? null : id ?? null return { stage: PIPELINE_TAB_TO_NODE_STAGE[stage + 's'] || null, // pipeline tab has stage plural here we have singular - pluginId: pluginId, - batchExportDestination: batchExportDestination, + pluginId, + batchExportDestination, + hogFunctionId, } } @@ -45,32 +47,22 @@ export const scene: SceneExport = { paramsToProps, } -type PluginEntry = { - backend: PipelineBackend.Plugin - id: number +type TableEntry = { + backend: PipelineBackend + id: string | number name: string description: string - plugin: PluginType url?: string + icon: JSX.Element } -type BatchExportEntry = { - backend: PipelineBackend.BatchExport - id: BatchExportService['type'] - name: string - description: string - url: string -} - -type TableEntry = PluginEntry | BatchExportEntry - function convertPluginToTableEntry(plugin: PluginType): TableEntry { return { backend: PipelineBackend.Plugin, id: plugin.id, name: plugin.name, description: plugin.description || '', - plugin: plugin, + icon: , // TODO: ideally we'd link to docs instead of GitHub repo, so it can open in panel // Same for transformations and destinations tables url: plugin.url, @@ -80,17 +72,26 @@ function convertPluginToTableEntry(plugin: PluginType): TableEntry { function convertBatchExportToTableEntry(service: BatchExportService['type']): TableEntry { return { backend: PipelineBackend.BatchExport, - id: service, + id: service as string, name: service, description: `${service} batch export`, + icon: , url: getBatchExportUrl(service), } } -export function PipelineNodeNew( - params: { stage?: string; pluginIdOrBatchExportDestination?: string } = {} -): JSX.Element { - const { stage, pluginId, batchExportDestination } = paramsToProps({ params }) +function convertHogFunctionToTableEntry(hogFunction: HogFunctionTemplateType): TableEntry { + return { + backend: PipelineBackend.HogFunction, + id: `hog-${hogFunction.id}`, // TODO: This weird identifier thing isn't great + name: hogFunction.name, + description: hogFunction.description, + icon: 🦔, + } +} + +export function PipelineNodeNew(params: { stage?: string; id?: string } = {}): JSX.Element { + const { stage, pluginId, batchExportDestination, hogFunctionId } = paramsToProps({ params }) if (!stage) { return @@ -103,6 +104,7 @@ export function PipelineNodeNew( } return res } + if (batchExportDestination) { if (stage !== PipelineStage.Destination) { return @@ -114,6 +116,14 @@ export function PipelineNodeNew( ) } + if (hogFunctionId) { + const res = + if (stage === PipelineStage.Destination) { + return {res} + } + return res + } + if (stage === PipelineStage.Transformation) { return } else if (stage === PipelineStage.Destination) { @@ -135,11 +145,15 @@ function TransformationOptionsTable(): JSX.Element { } function DestinationOptionsTable(): JSX.Element { + const hogFunctionsEnabled = !!useFeatureFlag('HOG_FUNCTIONS') const { batchExportServiceNames } = useValues(pipelineNodeNewLogic) - const { plugins, loading } = useValues(pipelineDestinationsLogic) + const { plugins, loading, hogFunctionTemplates } = useValues(pipelineDestinationsLogic) const pluginTargets = Object.values(plugins).map(convertPluginToTableEntry) const batchExportTargets = Object.values(batchExportServiceNames).map(convertBatchExportToTableEntry) - const targets = [...batchExportTargets, ...pluginTargets] + const hogFunctionTargets = hogFunctionsEnabled + ? Object.values(hogFunctionTemplates).map(convertHogFunctionToTableEntry) + : [] + const targets = [...batchExportTargets, ...pluginTargets, ...hogFunctionTargets] return } @@ -158,6 +172,7 @@ function NodeOptionsTable({ targets: TableEntry[] loading: boolean }): JSX.Element { + const { hashParams } = useValues(router) return ( <> - } - return + return target.icon }, }, { @@ -198,7 +210,8 @@ function NodeOptionsTable({ type="primary" data-attr={`new-${stage}-${target.id}`} icon={} - to={urls.pipelineNodeNew(stage, target.id)} + // Preserve hash params to pass config in + to={combineUrl(urls.pipelineNodeNew(stage, target.id), {}, hashParams).url} > Create diff --git a/frontend/src/scenes/pipeline/destinationsLogic.tsx b/frontend/src/scenes/pipeline/destinationsLogic.tsx index d50e6b5de5091..05d4e2ff1d1e6 100644 --- a/frontend/src/scenes/pipeline/destinationsLogic.tsx +++ b/frontend/src/scenes/pipeline/destinationsLogic.tsx @@ -8,6 +8,8 @@ import { userLogic } from 'scenes/userLogic' import { BatchExportConfiguration, + HogFunctionTemplateType, + HogFunctionType, PipelineStage, PluginConfigTypeNew, PluginConfigWithPluginInfoNew, @@ -16,6 +18,7 @@ import { } from '~/types' import type { pipelineDestinationsLogicType } from './destinationsLogicType' +import { HOG_FUNCTION_TEMPLATES } from './hogfunctions/templates/hog-templates' import { pipelineAccessLogic } from './pipelineAccessLogic' import { BatchExportDestination, convertToPipelineNode, Destination, PipelineBackend } from './types' import { captureBatchExportEvent, capturePluginEvent, loadPluginsFromUrl } from './utils' @@ -116,28 +119,68 @@ export const pipelineDestinationsLogic = kea([ }, }, ], + + hogFunctionTemplates: [ + {} as Record, + { + loadHogFunctionTemplates: async () => { + return HOG_FUNCTION_TEMPLATES.reduce((acc, template) => { + acc[template.id] = template + return acc + }, {} as Record) + }, + }, + ], + hogFunctions: [ + [] as HogFunctionType[], + { + loadHogFunctions: async () => { + // TODO: Support pagination? + return (await api.hogFunctions.list()).results + }, + }, + ], })), selectors({ loading: [ - (s) => [s.pluginsLoading, s.pluginConfigsLoading, s.batchExportConfigsLoading], - (pluginsLoading, pluginConfigsLoading, batchExportConfigsLoading) => - pluginsLoading || pluginConfigsLoading || batchExportConfigsLoading, + (s) => [ + s.pluginsLoading, + s.pluginConfigsLoading, + s.batchExportConfigsLoading, + s.hogFunctionTemplatesLoading, + s.hogFunctionsLoading, + ], + ( + pluginsLoading, + pluginConfigsLoading, + batchExportConfigsLoading, + hogFunctionTemplatesLoading, + hogFunctionsLoading + ) => + pluginsLoading || + pluginConfigsLoading || + batchExportConfigsLoading || + hogFunctionTemplatesLoading || + hogFunctionsLoading, ], destinations: [ - (s) => [s.pluginConfigs, s.plugins, s.batchExportConfigs, s.user], - (pluginConfigs, plugins, batchExportConfigs, user): Destination[] => { + (s) => [s.pluginConfigs, s.plugins, s.batchExportConfigs, s.hogFunctions, s.user], + (pluginConfigs, plugins, batchExportConfigs, hogFunctions, user): Destination[] => { // Migrations are shown only in impersonation mode, for us to be able to trigger them. const rawBatchExports = Object.values(batchExportConfigs).filter( (config) => config.destination.type !== 'HTTP' || user?.is_impersonated ) - const rawDestinations: (PluginConfigWithPluginInfoNew | BatchExportConfiguration)[] = Object.values( - pluginConfigs - ) - .map((pluginConfig) => ({ - ...pluginConfig, - plugin_info: plugins[pluginConfig.plugin] || null, - })) - .concat(rawBatchExports) + + const rawDestinations: (PluginConfigWithPluginInfoNew | BatchExportConfiguration | HogFunctionType)[] = + Object.values(pluginConfigs) + .map( + (pluginConfig) => ({ + ...pluginConfig, + plugin_info: plugins[pluginConfig.plugin] || null, + }) + ) + .concat(rawBatchExports) + .concat(hogFunctions) const convertedDestinations = rawDestinations.map((d) => convertToPipelineNode(d, PipelineStage.Destination) ) @@ -183,5 +226,7 @@ export const pipelineDestinationsLogic = kea([ actions.loadPlugins() actions.loadPluginConfigs() actions.loadBatchExports() + actions.loadHogFunctionTemplates() + actions.loadHogFunctions() }), ]) diff --git a/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx b/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx index 38d5acba5fcd3..f9a4d7d66b824 100644 --- a/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx +++ b/frontend/src/scenes/pipeline/pipelineNodeLogic.tsx @@ -24,7 +24,11 @@ type BatchExportNodeId = { backend: PipelineBackend.BatchExport id: string } -export type PipelineNodeLimitedType = PluginNodeId | BatchExportNodeId +type HogFunctionNodeId = { + backend: PipelineBackend.HogFunction + id: string +} +export type PipelineNodeLimitedType = PluginNodeId | BatchExportNodeId | HogFunctionNodeId export const pipelineNodeLogic = kea([ props({} as PipelineNodeLogicProps), @@ -61,18 +65,23 @@ export const pipelineNodeLogic = kea([ }, ], ], + + nodeBackend: [ + (s) => [s.node], + (node): PipelineBackend => { + return node.backend + }, + ], node: [ (_, p) => [p.id], (id): PipelineNodeLimitedType => { return typeof id === 'string' - ? { backend: PipelineBackend.BatchExport, id: id } - : { backend: PipelineBackend.Plugin, id: id } + ? id.indexOf('hog-') === 0 + ? { backend: PipelineBackend.HogFunction, id: `${id}`.replace('hog-', '') } + : { backend: PipelineBackend.BatchExport, id } + : { backend: PipelineBackend.Plugin, id } }, ], - nodeBackend: [ - (_, p) => [p.id], - (id): PipelineBackend => (typeof id === 'string' ? PipelineBackend.BatchExport : PipelineBackend.Plugin), - ], tabs: [ (s) => [s.nodeBackend], (nodeBackend) => { diff --git a/frontend/src/scenes/pipeline/pipelineNodeNewLogic.tsx b/frontend/src/scenes/pipeline/pipelineNodeNewLogic.tsx index 395055b913a9b..81e45ff15d394 100644 --- a/frontend/src/scenes/pipeline/pipelineNodeNewLogic.tsx +++ b/frontend/src/scenes/pipeline/pipelineNodeNewLogic.tsx @@ -18,6 +18,7 @@ export interface PipelineNodeNewLogicProps { stage: PipelineStage | null pluginId: number | null batchExportDestination: string | null + hogFunctionId: string | null } export const pipelineNodeNewLogic = kea([ @@ -25,12 +26,7 @@ export const pipelineNodeNewLogic = kea([ connect({ values: [userLogic, ['user']], }), - path((pluginIdOrBatchExportDestination) => [ - 'scenes', - 'pipeline', - 'pipelineNodeNewLogic', - pluginIdOrBatchExportDestination, - ]), + path((id) => ['scenes', 'pipeline', 'pipelineNodeNewLogic', id]), actions({ createNewButtonPressed: (stage: PipelineStage, id: number | BatchExportService['type']) => ({ stage, id }), }), diff --git a/frontend/src/scenes/pipeline/types.ts b/frontend/src/scenes/pipeline/types.ts index dc6ac93442df9..f958ebb887ca6 100644 --- a/frontend/src/scenes/pipeline/types.ts +++ b/frontend/src/scenes/pipeline/types.ts @@ -1,6 +1,7 @@ import { BatchExportConfiguration, BatchExportService, + HogFunctionType, PipelineStage, PluginConfigWithPluginInfoNew, PluginType, @@ -9,6 +10,7 @@ import { export enum PipelineBackend { BatchExport = 'batch_export', Plugin = 'plugin', + HogFunction = 'hog_function', } // Base - we're taking a discriminated union approach here, so that TypeScript can discern types for free @@ -39,6 +41,11 @@ export interface BatchExportBasedNode extends PipelineNodeBase { interval: BatchExportConfiguration['interval'] } +export interface HogFunctionBasedNode extends PipelineNodeBase { + backend: PipelineBackend.HogFunction + id: string +} + // Stage: Transformations export interface Transformation extends PluginBasedNode { @@ -55,7 +62,11 @@ export interface WebhookDestination extends PluginBasedNode { export interface BatchExportDestination extends BatchExportBasedNode { stage: PipelineStage.Destination } -export type Destination = BatchExportDestination | WebhookDestination +export interface FunctionDestination extends HogFunctionBasedNode { + stage: PipelineStage.Destination + interval: 'realtime' +} +export type Destination = BatchExportDestination | WebhookDestination | FunctionDestination export interface DataImportApp extends PluginBasedNode { stage: PipelineStage.DataImport @@ -84,7 +95,7 @@ function isPluginConfig( } export function convertToPipelineNode( - candidate: PluginConfigWithPluginInfoNew | BatchExportConfiguration, + candidate: PluginConfigWithPluginInfoNew | BatchExportConfiguration | HogFunctionType, stage: S ): S extends PipelineStage.Transformation ? Transformation @@ -98,7 +109,20 @@ export function convertToPipelineNode( ? ImportApp : never { let node: PipelineNode - if (isPluginConfig(candidate)) { + // check if type is a hog function + if ('hog' in candidate) { + node = { + stage: stage as PipelineStage.Destination, + backend: PipelineBackend.HogFunction, + interval: 'realtime', + id: `hog-${candidate.id}`, + name: candidate.name, + description: candidate.description, + enabled: candidate.enabled, + created_at: candidate.created_at, + updated_at: candidate.created_at, + } + } else if (isPluginConfig(candidate)) { const almostNode: Omit< Transformation | WebhookDestination | SiteApp | ImportApp | DataImportApp, 'frequency' | 'order' diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index c80897e8dd192..1dcb3f8af312b 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -9,6 +9,7 @@ export enum Scene { Error404 = '404', ErrorNetwork = '4xx', ErrorProjectUnavailable = 'ProjectUnavailable', + ErrorTracking = 'ErrorTracking', Dashboards = 'Dashboards', Dashboard = 'Dashboard', Insight = 'Insight', diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 94983524158f6..f4ef644d8665c 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -53,6 +53,10 @@ export const sceneConfigurations: Record = { activityScope: ActivityScope.DASHBOARD, defaultDocsPath: '/docs/product-analytics/dashboards', }, + [Scene.ErrorTracking]: { + projectBased: true, + name: 'Error tracking', + }, [Scene.Insight]: { projectBased: true, name: 'Insights', @@ -408,7 +412,6 @@ export const sceneConfigurations: Record = { [Scene.Heatmaps]: { projectBased: true, name: 'Heatmaps', - hideProjectNotice: true, }, } @@ -529,7 +532,7 @@ export const routes: Record = { [urls.persons()]: Scene.PersonsManagement, [urls.pipelineNodeDataWarehouseNew()]: Scene.pipelineNodeDataWarehouseNew, [urls.pipelineNodeNew(':stage')]: Scene.PipelineNodeNew, - [urls.pipelineNodeNew(':stage', ':pluginIdOrBatchExportDestination')]: Scene.PipelineNodeNew, + [urls.pipelineNodeNew(':stage', ':id')]: Scene.PipelineNodeNew, [urls.pipeline(':tab')]: Scene.Pipeline, [urls.pipelineNode(':stage', ':id', ':nodeTab')]: Scene.PipelineNode, [urls.groups(':groupTypeIndex')]: Scene.PersonsManagement, @@ -541,6 +544,7 @@ export const routes: Record = { [urls.experiment(':id')]: Scene.Experiment, [urls.earlyAccessFeatures()]: Scene.EarlyAccessFeatures, [urls.earlyAccessFeature(':id')]: Scene.EarlyAccessFeature, + [urls.errorTracking()]: Scene.ErrorTracking, [urls.surveys()]: Scene.Surveys, [urls.survey(':id')]: Scene.Survey, [urls.surveyTemplates()]: Scene.SurveyTemplates, diff --git a/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss b/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss index 943f17aa977ba..8549d99d48dde 100644 --- a/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss +++ b/frontend/src/scenes/session-recordings/player/controller/Seekbar.scss @@ -47,7 +47,7 @@ .PlayerSeekbar__currentbar { z-index: 3; - background-color: var(--recording-seekbar-red); + background-color: var(--primary-3000); border-radius: var(--bar-height) 0 0 var(--bar-height); } @@ -76,7 +76,7 @@ width: var(--thumb-size); height: var(--thumb-size); margin-top: calc(var(--thumb-size) / 2 * -1); - background-color: var(--recording-seekbar-red); + background-color: var(--primary-3000); border: 2px solid var(--bg-light); border-radius: 50%; transition: top 150ms ease-in-out; diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx index e851684d58874..9131ba82271d2 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemEvent.tsx @@ -68,7 +68,7 @@ export function ItemEvent({ item, expanded, setExpanded }: ItemEventProps): JSX. {item.data.fullyLoaded ? ( item.data.event === '$exception' ? ( - + ) : ( ) diff --git a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts index 65829d1257afd..965e6f33382e3 100644 --- a/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts +++ b/frontend/src/scenes/session-recordings/player/playerSettingsLogic.ts @@ -1,5 +1,6 @@ -import { actions, kea, listeners, path, reducers, selectors } from 'kea' +import { actions, connect, kea, listeners, path, reducers, selectors } from 'kea' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +import { teamLogic } from 'scenes/teamLogic' import { AutoplayDirection, DurationType, SessionRecordingPlayerTab } from '~/types' @@ -191,7 +192,10 @@ export const playerSettingsLogic = kea([ setQuickFilterProperties: (properties: string[]) => ({ properties }), setTimestampFormat: (format: TimestampFormat) => ({ format }), }), - reducers(() => ({ + connect({ + values: [teamLogic, ['currentTeam']], + }), + reducers(({ values }) => ({ showFilters: [ true, { @@ -211,7 +215,7 @@ export const playerSettingsLogic = kea([ }, ], quickFilterProperties: [ - ['$geoip_country_name'] as string[], + ['$geoip_country_name', ...(values.currentTeam?.person_display_name_properties || [])] as string[], { persist: true, }, diff --git a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx index 40d3d356bd447..17ae678e31792 100644 --- a/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx +++ b/frontend/src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx @@ -23,6 +23,7 @@ import { urls } from 'scenes/urls' import { ReplayTabs, SessionRecordingType } from '~/types' +import { RecordingsUniversalFilters } from '../filters/RecordingsUniversalFilters' import { SessionRecordingsFilters } from '../filters/SessionRecordingsFilters' import { SessionRecordingPlayer } from '../player/SessionRecordingPlayer' import { SessionRecordingPreview, SessionRecordingPreviewSkeleton } from './SessionRecordingPreview' @@ -118,6 +119,7 @@ function RecordingsLists(): JSX.Element { recordingsCount, isRecordingsListCollapsed, sessionSummaryLoading, + useUniversalFiltering, } = useValues(sessionRecordingsPlaylistLogic) const { setSelectedRecordingId, @@ -205,25 +207,27 @@ function RecordingsLists(): JSX.Element { - - - - } - onClick={() => { - if (notebookNode) { - notebookNode.actions.toggleEditing() - } else { - setShowFilters(!showFilters) + {(!useUniversalFiltering || notebookNode) && ( + + + } - }} - > - Filter - + onClick={() => { + if (notebookNode) { + notebookNode.actions.toggleEditing() + } else { + setShowFilters(!showFilters) + } + }} + > + Filter + + )} - + +
+ {useUniversalFiltering && } +
- - +
+
) } diff --git a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts index 6f128876501c8..7d8b34203a403 100644 --- a/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts +++ b/frontend/src/scenes/session-recordings/playlist/sessionRecordingsPlaylistLogic.ts @@ -4,6 +4,10 @@ import { loaders } from 'kea-loaders' import { actionToUrl, router, urlToAction } from 'kea-router' import { subscriptions } from 'kea-subscriptions' import api from 'lib/api' +import { isAnyPropertyfilter } from 'lib/components/PropertyFilters/utils' +import { UniversalFiltersGroup, UniversalFilterValue } from 'lib/components/UniversalFilters/UniversalFilters' +import { DEFAULT_UNIVERSAL_GROUP_FILTER } from 'lib/components/UniversalFilters/universalFiltersLogic' +import { isActionFilter, isEventFilter } from 'lib/components/UniversalFilters/utils' import { FEATURE_FLAGS } from 'lib/constants' import { now } from 'lib/dayjs' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' @@ -12,11 +16,15 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import posthog from 'posthog-js' import { + AnyPropertyFilter, DurationType, + FilterableLogLevel, + FilterType, PropertyFilterType, PropertyOperator, RecordingDurationFilter, RecordingFilters, + RecordingUniversalFilters, ReplayTabs, SessionRecordingId, SessionRecordingsResponse, @@ -85,6 +93,14 @@ export const DEFAULT_RECORDING_FILTERS: RecordingFilters = { console_search_query: '', } +export const DEFAULT_RECORDING_UNIVERSAL_FILTERS: RecordingUniversalFilters = { + live_mode: false, + filter_test_accounts: false, + date_from: '-3d', + filter_group: { ...DEFAULT_UNIVERSAL_GROUP_FILTER }, + duration: [defaultRecordingDurationFilter], +} + const DEFAULT_PERSON_RECORDING_FILTERS: RecordingFilters = { ...DEFAULT_RECORDING_FILTERS, date_from: '-30d', @@ -106,6 +122,47 @@ const capturePartialFilters = (filters: Partial): void => { ...partialFilters, }) } +function convertUniversalFiltersToLegacyFilters(universalFilters: RecordingUniversalFilters): RecordingFilters { + const nestedFilters = universalFilters.filter_group.values[0] as UniversalFiltersGroup + const filters = nestedFilters.values as UniversalFilterValue[] + + const properties: AnyPropertyFilter[] = [] + const events: FilterType['events'] = [] + const actions: FilterType['actions'] = [] + let console_logs: FilterableLogLevel[] = [] + let console_search_query = '' + + filters.forEach((f) => { + if (isEventFilter(f)) { + events.push(f) + } else if (isActionFilter(f)) { + actions.push(f) + } else if (isAnyPropertyfilter(f)) { + if (f.type === PropertyFilterType.Recording) { + if (f.key === 'console_log_level') { + console_logs = f.value as FilterableLogLevel[] + } else if (f.key === 'console_log_query') { + console_search_query = (f.value || '') as string + } + } else { + properties.push(f) + } + } + }) + + const durationFilter = universalFilters.duration[0] + + return { + ...universalFilters, + properties, + events, + actions, + session_recording_duration: { ...durationFilter, key: 'duration' }, + duration_type_filter: durationFilter.key, + console_search_query, + console_logs, + } +} export interface SessionRecordingPlaylistLogicProps { logicKey?: string @@ -113,6 +170,7 @@ export interface SessionRecordingPlaylistLogicProps { updateSearchParams?: boolean autoPlay?: boolean hideSimpleFilters?: boolean + universalFilters?: RecordingUniversalFilters advancedFilters?: RecordingFilters simpleFilters?: RecordingFilters onFiltersChange?: (filters: RecordingFilters) => void @@ -148,6 +206,7 @@ export const sessionRecordingsPlaylistLogic = kea) => ({ filters }), setAdvancedFilters: (filters: Partial) => ({ filters }), setSimpleFilters: (filters: SimpleFiltersType) => ({ filters }), setShowFilters: (showFilters: boolean) => ({ showFilters }), @@ -355,6 +414,18 @@ export const sessionRecordingsPlaylistLogic = kea getDefaultFilters(props.personUUID), }, ], + universalFilters: [ + props.universalFilters ?? DEFAULT_RECORDING_UNIVERSAL_FILTERS, + { + setUniversalFilters: (state, { filters }) => { + return { + ...state, + ...filters, + } + }, + resetFilters: () => DEFAULT_RECORDING_UNIVERSAL_FILTERS, + }, + ], showFilters: [ true, { @@ -465,6 +536,12 @@ export const sessionRecordingsPlaylistLogic = kea { + actions.loadSessionRecordings() + props.onFiltersChange?.(values.filters) + capturePartialFilters(filters) + actions.loadEventsHaveSessionId() + }, setOrderBy: () => { actions.loadSessionRecordings() @@ -512,12 +589,20 @@ export const sessionRecordingsPlaylistLogic = kea [s.featureFlags], (featureFlags) => !!featureFlags[FEATURE_FLAGS.SESSION_REPLAY_HOG_QL_FILTERING], ], + useUniversalFiltering: [ + (s) => [s.featureFlags], + (featureFlags) => !!featureFlags[FEATURE_FLAGS.SESSION_REPLAY_UNIVERSAL_FILTERS], + ], logicProps: [() => [(_, props) => props], (props): SessionRecordingPlaylistLogicProps => props], filters: [ - (s) => [s.simpleFilters, s.advancedFilters], - (simpleFilters, advancedFilters): RecordingFilters => { + (s) => [s.simpleFilters, s.advancedFilters, s.universalFilters, s.featureFlags], + (simpleFilters, advancedFilters, universalFilters, featureFlags): RecordingFilters => { + if (featureFlags[FEATURE_FLAGS.SESSION_REPLAY_UNIVERSAL_FILTERS]) { + return convertUniversalFiltersToLegacyFilters(universalFilters) + } + return { ...advancedFilters, events: [...(simpleFilters?.events || []), ...(advancedFilters?.events || [])], diff --git a/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx b/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx index d0da4e1b4f7ee..caf5e06889346 100644 --- a/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx +++ b/frontend/src/scenes/settings/user/personalAPIKeysLogic.tsx @@ -306,7 +306,9 @@ export const personalAPIKeysLogic = kea([ <>

You can now use key "{key.label}" for authentication:

- {value} + + {value} + For security reasons the value above will never be shown again. diff --git a/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx index 4b43be07bcf46..ec3b03d54b41d 100644 --- a/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx +++ b/frontend/src/scenes/surveys/SurveyEditQuestionRow.tsx @@ -7,12 +7,15 @@ import { IconPlusSmall, IconTrash } from '@posthog/icons' import { LemonButton, LemonCheckbox, LemonInput, LemonSelect } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { Group } from 'kea-forms' +import { FEATURE_FLAGS } from 'lib/constants' import { SortableDragIcon } from 'lib/lemon-ui/icons' import { LemonField } from 'lib/lemon-ui/LemonField' +import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagLogic' import { Survey, SurveyQuestionType } from '~/types' import { defaultSurveyFieldValues, NewSurvey, SurveyQuestionLabel } from './constants' +import { QuestionBranchingInput } from './QuestionBranchingInput' import { HTMLEditor } from './SurveyAppearanceUtils' import { surveyLogic } from './surveyLogic' @@ -85,6 +88,10 @@ export function SurveyEditQuestionHeader({ export function SurveyEditQuestionGroup({ index, question }: { index: number; question: any }): JSX.Element { const { survey, descriptionContentType } = useValues(surveyLogic) const { setDefaultForQuestionType, setSurveyValue } = useActions(surveyLogic) + const { featureFlags } = useValues(enabledFeaturesLogic) + const hasBranching = + featureFlags[FEATURE_FLAGS.SURVEYS_BRANCHING_LOGIC] && + (question.type === SurveyQuestionType.Rating || question.type === SurveyQuestionType.SingleChoice) const initialDescriptionContentType = descriptionContentType(index) ?? 'text' @@ -332,6 +339,7 @@ export function SurveyEditQuestionGroup({ index, question }: { index: number; qu } /> + {hasBranching && }
) diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index c976e7f5dfb53..9b3964b64fdf3 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -16,10 +16,13 @@ import { hogql } from '~/queries/utils' import { Breadcrumb, FeatureFlagFilters, + MultipleSurveyQuestion, PropertyFilterType, PropertyOperator, + RatingSurveyQuestion, Survey, SurveyQuestionBase, + SurveyQuestionBranchingType, SurveyQuestionType, SurveyUrlMatchType, } from '~/types' @@ -154,6 +157,7 @@ export const surveyLogic = kea([ isEditingDescription, isEditingThankYouMessage, }), + setQuestionBranching: (questionIndex, value) => ({ questionIndex, value }), archiveSurvey: true, setWritingHTMLDescription: (writingHTML: boolean) => ({ writingHTML }), setSurveyTemplateValues: (template: any) => ({ template }), @@ -657,6 +661,44 @@ export const surveyLogic = kea([ const newTemplateSurvey = { ...NEW_SURVEY, ...template } return newTemplateSurvey }, + setQuestionBranching: (state, { questionIndex, value }) => { + const newQuestions = [...state.questions] + const question = newQuestions[questionIndex] + + if ( + question.type !== SurveyQuestionType.Rating && + question.type !== SurveyQuestionType.SingleChoice + ) { + throw new Error( + `Survey question type must be ${SurveyQuestionType.Rating} or ${SurveyQuestionType.SingleChoice}` + ) + } + + if (value === SurveyQuestionBranchingType.NextQuestion) { + delete question.branching + } else if (value === SurveyQuestionBranchingType.ConfirmationMessage) { + question.branching = { + type: SurveyQuestionBranchingType.ConfirmationMessage, + } + } else if (value === SurveyQuestionBranchingType.ResponseBased) { + question.branching = { + type: SurveyQuestionBranchingType.ResponseBased, + responseValue: {}, + } + } else if (value.startsWith(SurveyQuestionBranchingType.SpecificQuestion)) { + const nextQuestionIndex = parseInt(value.split(':')[1]) + question.branching = { + type: SurveyQuestionBranchingType.SpecificQuestion, + index: nextQuestionIndex, + } + } + + newQuestions[questionIndex] = question + return { + ...state, + questions: newQuestions, + } + }, }, ], selectedPageIndex: [ @@ -882,6 +924,28 @@ export const surveyLogic = kea([ } }, ], + getBranchingDropdownValue: [ + (s) => [s.survey], + (survey) => (questionIndex: number, question: RatingSurveyQuestion | MultipleSurveyQuestion) => { + if (question.branching?.type) { + const { type } = question.branching + + if (type === SurveyQuestionBranchingType.SpecificQuestion) { + const nextQuestionIndex = question.branching.index + return `${SurveyQuestionBranchingType.SpecificQuestion}:${nextQuestionIndex}` + } + + return type + } + + // No branching specified, default to Next question / Confirmation message + if (questionIndex < survey.questions.length - 1) { + return SurveyQuestionBranchingType.NextQuestion + } + + return SurveyQuestionBranchingType.ConfirmationMessage + }, + ], }), forms(({ actions, props, values }) => ({ survey: { diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index cc70cd9fc7f43..c5c68db5b337c 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -120,13 +120,13 @@ export const urls = { encode ? `/persons/${encodeURIComponent(uuid)}` : `/persons/${uuid}`, persons: (): string => '/persons', pipelineNodeDataWarehouseNew: (): string => `/pipeline/new/data-warehouse`, - pipelineNodeNew: (stage: PipelineStage | ':stage', pluginIdOrBatchExportDestination?: string | number): string => { + pipelineNodeNew: (stage: PipelineStage | ':stage', id?: string | number): string => { if (stage === PipelineStage.DataImport) { // should match 'pipelineNodeDataWarehouseNew' return `/pipeline/new/data-warehouse` } - return `/pipeline/new/${stage}${pluginIdOrBatchExportDestination ? `/${pluginIdOrBatchExportDestination}` : ''}` + return `/pipeline/new/${stage}${id ? `/${id}` : ''}` }, pipeline: (tab?: PipelineTab | ':tab'): string => `/pipeline/${tab ? tab : PipelineTab.Overview}`, /** @param id 'new' for new, uuid for batch exports and numbers for plugins */ @@ -149,6 +149,8 @@ export const urls = { earlyAccessFeatures: (): string => '/early_access_features', /** @param id A UUID or 'new'. ':id' for routing. */ earlyAccessFeature: (id: string): string => `/early_access_features/${id}`, + errorTracking: (): string => '/error_tracking', + errorTrackingGroup: (id: string): string => `/error_tracking/${id}`, surveys: (): string => '/surveys', /** @param id A UUID or 'new'. ':id' for routing. */ survey: (id: string): string => `/surveys/${id}`, diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx index 2e8727a6f5048..fbe32a7e3b359 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsNotice.tsx @@ -23,14 +23,14 @@ export const WebAnalyticsNotice = (): JSX.Element => { } - onClick={() => openSupportForm({ kind: 'bug' })} + onClick={() => openSupportForm({ kind: 'bug', isEmailFormOpen: true })} > Report a bug } - onClick={() => openSupportForm({ kind: 'feedback' })} + onClick={() => openSupportForm({ kind: 'feedback', isEmailFormOpen: true })} > Give feedback diff --git a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx index 62e3cce87f7fb..6dec7396bfc17 100644 --- a/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx +++ b/frontend/src/scenes/web-analytics/WebAnalyticsTile.tsx @@ -330,7 +330,7 @@ export const WebStatsTrendTile = ({ }, [onWorldMapClick, insightProps]) return ( -
+
{showIntervalTile && (
diff --git a/frontend/src/styles/vars.scss b/frontend/src/styles/vars.scss index 783611a4d6a56..8758149290917 100644 --- a/frontend/src/styles/vars.scss +++ b/frontend/src/styles/vars.scss @@ -156,13 +156,11 @@ $colors: ( // These vars are modified via SCSS for legacy reasons (e.g. darken/lighten), so keeping as SCSS vars for now. $_primary: map.get($colors, 'primary'); $_success: map.get($colors, 'success'); -$_danger: map.get($colors, 'danger'); -$_primary_bg_hover: rgba($_primary, 0.1); $_primary_bg_active: rgba($_primary, 0.2); $_lifecycle_new: $_primary; $_lifecycle_returning: $_success; $_lifecycle_resurrecting: #a56eff; // --data-lilac -$_lifecycle_dormant: $_danger; +$_lifecycle_dormant: map.get($colors, 'danger'); // root variables are defined as a mixin here because // the toolbar needs them attached to :host not :root @@ -193,9 +191,6 @@ $_lifecycle_dormant: $_danger; --green: var(--success); --black: var(--default); - // Tag colors - --purple-light: #dcb1e3; - //// Data colors (e.g. insight series). Note: colors.ts relies on these values being hexadecimal --data-color-1: #1d4aff; --data-color-2: #621da6; @@ -227,22 +222,6 @@ $_lifecycle_dormant: $_danger; // TODO: unify with lib/colors.ts, getGraphColors() --funnel-axis: var(--border); --funnel-grid: #ddd; - --antd-table-background-dark: #fafafa; - - // Session Recording - --recording-spacing: calc(2rem / 3); - --recording-player-container-bg: #797973; - --recording-buffer-bg: #faaf8c; - --recording-seekbar-red: var(--brand-red); - --recording-hover-event: var(--primary-bg-hover); - --recording-hover-event-mid: var(--primary-bg-active); - --recording-hover-event-dark: var(--primary-3000); - --recording-current-event: #eef2ff; - --recording-current-event-dark: var(--primary-alt); - --recording-failure-event: #fee9e2; - --recording-failure-event-dark: #cd3000; - --recording-highlight-event: var(--mark); - --recording-highlight-event-dark: #946508; // Z-indexes --z-bottom-notice: 5100; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index ab2fb248d8fd7..160a46df07749 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -5,6 +5,7 @@ import { ChartDataset, ChartType, InteractionItem } from 'chart.js' import { LogicWrapper } from 'kea' import { DashboardCompatibleScenes } from 'lib/components/SceneDashboardChoice/sceneDashboardChoiceModalLogic' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { UniversalFiltersGroup } from 'lib/components/UniversalFilters/UniversalFilters' import { BIN_COUNT_AUTO, DashboardPrivilegeLevel, @@ -777,7 +778,7 @@ export type AnyPropertyFilter = | ElementPropertyFilter | SessionPropertyFilter | CohortPropertyFilter - | RecordingDurationFilter + | RecordingPropertyFilter | GroupPropertyFilter | FeaturePropertyFilter | HogQLPropertyFilter @@ -946,13 +947,17 @@ export type ActionStepProperties = | ElementPropertyFilter | CohortPropertyFilter -export interface RecordingDurationFilter extends BasePropertyFilter { +export interface RecordingPropertyFilter extends BasePropertyFilter { type: PropertyFilterType.Recording - key: 'duration' - value: number + key: DurationType | 'console_log_level' | 'console_log_query' operator: PropertyOperator } +export interface RecordingDurationFilter extends RecordingPropertyFilter { + key: DurationType + value: number +} + export type DurationType = 'duration' | 'active_seconds' | 'inactive_seconds' export type FilterableLogLevel = 'info' | 'warn' | 'error' @@ -973,6 +978,18 @@ export interface RecordingFilters { filter_test_accounts?: boolean } +export interface RecordingUniversalFilters { + /** + * live mode is front end only, sets date_from and date_to to the last hour + */ + live_mode?: boolean + date_from?: string | null + date_to?: string | null + duration: RecordingDurationFilter[] + filter_test_accounts?: boolean + filter_group: UniversalFiltersGroup +} + export interface SessionRecordingsResponse { results: SessionRecordingType[] has_next: boolean @@ -989,6 +1006,13 @@ export type ErrorCluster = { } export type ErrorClusterResponse = ErrorCluster[] | null +export type ErrorTrackingGroup = { + title: string + sampleEventProperties: EventType['properties'] + occurrences: number + uniqueSessions: number +} + export type EntityType = 'actions' | 'events' | 'data_warehouse' | 'new_entity' export interface Entity { @@ -2649,6 +2673,11 @@ export interface RatingSurveyQuestion extends SurveyQuestionBase { scale: number lowerBoundLabel: string upperBoundLabel: string + branching?: + | NextQuestionBranching + | ConfirmationMessageBranching + | ResponseBasedBranching + | SpecificQuestionBranching } export interface MultipleSurveyQuestion extends SurveyQuestionBase { @@ -2656,6 +2685,11 @@ export interface MultipleSurveyQuestion extends SurveyQuestionBase { choices: string[] shuffleOptions?: boolean hasOpenChoice?: boolean + branching?: + | NextQuestionBranching + | ConfirmationMessageBranching + | ResponseBasedBranching + | SpecificQuestionBranching } export type SurveyQuestion = BasicSurveyQuestion | LinkSurveyQuestion | RatingSurveyQuestion | MultipleSurveyQuestion @@ -2668,6 +2702,31 @@ export enum SurveyQuestionType { Link = 'link', } +export enum SurveyQuestionBranchingType { + NextQuestion = 'next_question', + ConfirmationMessage = 'confirmation_message', + ResponseBased = 'response_based', + SpecificQuestion = 'specific_question', +} + +interface NextQuestionBranching { + type: SurveyQuestionBranchingType.NextQuestion +} + +interface ConfirmationMessageBranching { + type: SurveyQuestionBranchingType.ConfirmationMessage +} + +interface ResponseBasedBranching { + type: SurveyQuestionBranchingType.ResponseBased + responseValue: Record +} + +interface SpecificQuestionBranching { + type: SurveyQuestionBranchingType.SpecificQuestion + index: number +} + export interface FeatureFlagGroupType { properties?: AnyPropertyFilter[] rollout_percentage?: number | null @@ -3746,6 +3805,7 @@ export interface ExternalDataStripeSource { prefix: string last_run_at?: Dayjs schemas: ExternalDataSourceSchema[] + sync_frequency: DataWarehouseSyncInterval } export interface SimpleExternalDataSourceSchema { id: string @@ -3879,6 +3939,8 @@ export type BatchExportService = export type PipelineInterval = 'hour' | 'day' | 'every 5 minutes' +export type DataWarehouseSyncInterval = 'day' | 'week' | 'month' + export type BatchExportConfiguration = { // User provided data for the export. This is the data that the user // provides when creating the export. @@ -4088,6 +4150,44 @@ export type OnboardingProduct = { scene: Scene } +export type HogFunctionInputSchemaType = { + type: 'string' | 'boolean' | 'dictionary' | 'choice' | 'json' + key: string + label: string + choices?: { value: string; label: string }[] + required?: boolean + default?: any + secret?: boolean + description?: string +} + +export type HogFunctionType = { + id: string + name: string + description: string + created_by: UserBasicType | null + created_at: string + updated_at: string + enabled: boolean + hog: string + + inputs_schema: HogFunctionInputSchemaType[] + inputs: Record< + string, + { + value: any + bytecode?: any + } + > + filters?: PluginConfigFilters | null + template?: HogFunctionTemplateType +} + +export type HogFunctionTemplateType = Pick< + HogFunctionType, + 'id' | 'name' | 'description' | 'hog' | 'inputs_schema' | 'filters' +> + export interface AnomalyCondition { absoluteThreshold: { lower?: number diff --git a/hogql_parser/HogQLParser.cpp b/hogql_parser/HogQLParser.cpp index ff90358df9bd2..112fd7cb48ae0 100644 --- a/hogql_parser/HogQLParser.cpp +++ b/hogql_parser/HogQLParser.cpp @@ -113,7 +113,7 @@ void hogqlparserParserInitialize() { } ); static const int32_t serializedATNSegment[] = { - 4,1,154,1158,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6, + 4,1,154,1178,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6, 2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14, 7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21, 7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28, @@ -171,52 +171,54 @@ void hogqlparserParserInitialize() { 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, 1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,724,8,53,1,53,1,53,1,53,1,53, 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,741,8,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,753,8,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,763,8,53,1,53,3,53,766,8,53,1, - 53,1,53,3,53,770,8,53,1,53,3,53,773,8,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,3,53,787,8,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,804,8,53,1,53, - 1,53,1,53,3,53,809,8,53,1,53,1,53,3,53,813,8,53,1,53,1,53,1,53,1,53,3, - 53,819,8,53,1,53,1,53,1,53,1,53,1,53,3,53,826,8,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,3,53,838,8,53,1,53,1,53,3,53,842,8,53,1, - 53,3,53,845,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,854,8,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,868,8,53, + 1,53,1,53,1,53,1,53,3,53,747,8,53,1,53,3,53,750,8,53,1,53,3,53,753,8, + 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,763,8,53,1,53,1,53,1, + 53,1,53,3,53,769,8,53,1,53,3,53,772,8,53,1,53,3,53,775,8,53,1,53,1,53, + 1,53,1,53,1,53,1,53,3,53,783,8,53,1,53,3,53,786,8,53,1,53,1,53,3,53,790, + 8,53,1,53,3,53,793,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,3,53,807,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,824,8,53,1,53,1,53,1,53,3,53, + 829,8,53,1,53,1,53,3,53,833,8,53,1,53,1,53,1,53,1,53,3,53,839,8,53,1, + 53,1,53,1,53,1,53,1,53,3,53,846,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,3,53,858,8,53,1,53,1,53,3,53,862,8,53,1,53,3,53,865, + 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,874,8,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,888,8,53,1,53,1,53, 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,895,8,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,904,8,53,5,53,906,8,53,10,53, - 12,53,909,9,53,1,54,1,54,1,54,5,54,914,8,54,10,54,12,54,917,9,54,1,55, - 1,55,3,55,921,8,55,1,56,1,56,1,56,1,56,5,56,927,8,56,10,56,12,56,930, - 9,56,1,56,1,56,1,56,1,56,1,56,5,56,937,8,56,10,56,12,56,940,9,56,3,56, - 942,8,56,1,56,1,56,1,56,1,57,1,57,1,57,5,57,950,8,57,10,57,12,57,953, - 9,57,1,57,1,57,1,57,1,57,1,57,1,57,5,57,961,8,57,10,57,12,57,964,9,57, - 1,57,1,57,3,57,968,8,57,1,57,1,57,1,57,1,57,1,57,3,57,975,8,57,1,58,1, - 58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,3,58,988,8,58,1,59,1, - 59,1,59,5,59,993,8,59,10,59,12,59,996,9,59,1,60,1,60,1,60,1,60,1,60,1, - 60,1,60,1,60,1,60,1,60,3,60,1008,8,60,1,61,1,61,1,61,1,61,3,61,1014,8, - 61,1,61,3,61,1017,8,61,1,62,1,62,1,62,5,62,1022,8,62,10,62,12,62,1025, - 9,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,3,63,1036,8,63,1,63, - 1,63,1,63,1,63,3,63,1042,8,63,5,63,1044,8,63,10,63,12,63,1047,9,63,1, - 64,1,64,1,64,3,64,1052,8,64,1,64,1,64,1,65,1,65,1,65,3,65,1059,8,65,1, - 65,1,65,1,66,1,66,1,66,5,66,1066,8,66,10,66,12,66,1069,9,66,1,67,1,67, - 1,68,1,68,1,68,1,68,1,68,1,68,3,68,1079,8,68,3,68,1081,8,68,1,69,3,69, - 1084,8,69,1,69,1,69,1,69,1,69,1,69,1,69,3,69,1092,8,69,1,70,1,70,1,70, - 3,70,1097,8,70,1,71,1,71,1,72,1,72,1,73,1,73,1,74,1,74,3,74,1107,8,74, - 1,75,1,75,1,75,3,75,1112,8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77, - 1,78,1,78,3,78,1124,8,78,1,79,1,79,5,79,1128,8,79,10,79,12,79,1131,9, - 79,1,79,1,79,1,80,1,80,1,80,1,80,1,80,3,80,1140,8,80,1,81,1,81,5,81,1144, - 8,81,10,81,12,81,1147,9,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,3,82,1156, - 8,82,1,82,0,3,68,106,126,83,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30, - 32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76, - 78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116, - 118,120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152, - 154,156,158,160,162,164,0,16,2,0,17,17,72,72,2,0,42,42,49,49,3,0,1,1, - 4,4,8,8,4,0,1,1,3,4,8,8,78,78,2,0,49,49,71,71,2,0,1,1,4,4,2,0,7,7,21, - 22,2,0,28,28,47,47,2,0,69,69,74,74,3,0,10,10,48,48,87,87,2,0,39,39,51, - 51,1,0,103,104,2,0,114,114,134,134,7,0,20,20,36,36,53,54,68,68,76,76, - 93,93,99,99,12,0,1,19,21,28,30,35,37,40,42,49,51,52,56,56,58,67,69,75, - 77,92,94,95,97,98,4,0,19,19,28,28,37,37,46,46,1288,0,169,1,0,0,0,2,176, - 1,0,0,0,4,178,1,0,0,0,6,180,1,0,0,0,8,189,1,0,0,0,10,195,1,0,0,0,12,212, - 1,0,0,0,14,214,1,0,0,0,16,217,1,0,0,0,18,226,1,0,0,0,20,232,1,0,0,0,22, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,915,8,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,3,53,924,8,53,5,53,926,8,53,10,53,12,53,929, + 9,53,1,54,1,54,1,54,5,54,934,8,54,10,54,12,54,937,9,54,1,55,1,55,3,55, + 941,8,55,1,56,1,56,1,56,1,56,5,56,947,8,56,10,56,12,56,950,9,56,1,56, + 1,56,1,56,1,56,1,56,5,56,957,8,56,10,56,12,56,960,9,56,3,56,962,8,56, + 1,56,1,56,1,56,1,57,1,57,1,57,5,57,970,8,57,10,57,12,57,973,9,57,1,57, + 1,57,1,57,1,57,1,57,1,57,5,57,981,8,57,10,57,12,57,984,9,57,1,57,1,57, + 3,57,988,8,57,1,57,1,57,1,57,1,57,1,57,3,57,995,8,57,1,58,1,58,1,58,1, + 58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,3,58,1008,8,58,1,59,1,59,1,59,5, + 59,1013,8,59,10,59,12,59,1016,9,59,1,60,1,60,1,60,1,60,1,60,1,60,1,60, + 1,60,1,60,1,60,3,60,1028,8,60,1,61,1,61,1,61,1,61,3,61,1034,8,61,1,61, + 3,61,1037,8,61,1,62,1,62,1,62,5,62,1042,8,62,10,62,12,62,1045,9,62,1, + 63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,3,63,1056,8,63,1,63,1,63,1, + 63,1,63,3,63,1062,8,63,5,63,1064,8,63,10,63,12,63,1067,9,63,1,64,1,64, + 1,64,3,64,1072,8,64,1,64,1,64,1,65,1,65,1,65,3,65,1079,8,65,1,65,1,65, + 1,66,1,66,1,66,5,66,1086,8,66,10,66,12,66,1089,9,66,1,67,1,67,1,68,1, + 68,1,68,1,68,1,68,1,68,3,68,1099,8,68,3,68,1101,8,68,1,69,3,69,1104,8, + 69,1,69,1,69,1,69,1,69,1,69,1,69,3,69,1112,8,69,1,70,1,70,1,70,3,70,1117, + 8,70,1,71,1,71,1,72,1,72,1,73,1,73,1,74,1,74,3,74,1127,8,74,1,75,1,75, + 1,75,3,75,1132,8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,78,1,78, + 3,78,1144,8,78,1,79,1,79,5,79,1148,8,79,10,79,12,79,1151,9,79,1,79,1, + 79,1,80,1,80,1,80,1,80,1,80,3,80,1160,8,80,1,81,1,81,5,81,1164,8,81,10, + 81,12,81,1167,9,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,3,82,1176,8,82, + 1,82,0,3,68,106,126,83,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32, + 34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78, + 80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118, + 120,122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152,154, + 156,158,160,162,164,0,16,2,0,17,17,72,72,2,0,42,42,49,49,3,0,1,1,4,4, + 8,8,4,0,1,1,3,4,8,8,78,78,2,0,49,49,71,71,2,0,1,1,4,4,2,0,7,7,21,22,2, + 0,28,28,47,47,2,0,69,69,74,74,3,0,10,10,48,48,87,87,2,0,39,39,51,51,1, + 0,103,104,2,0,114,114,134,134,7,0,20,20,36,36,53,54,68,68,76,76,93,93, + 99,99,12,0,1,19,21,28,30,35,37,40,42,49,51,52,56,56,58,67,69,75,77,92, + 94,95,97,98,4,0,19,19,28,28,37,37,46,46,1314,0,169,1,0,0,0,2,176,1,0, + 0,0,4,178,1,0,0,0,6,180,1,0,0,0,8,189,1,0,0,0,10,195,1,0,0,0,12,212,1, + 0,0,0,14,214,1,0,0,0,16,217,1,0,0,0,18,226,1,0,0,0,20,232,1,0,0,0,22, 236,1,0,0,0,24,245,1,0,0,0,26,247,1,0,0,0,28,256,1,0,0,0,30,260,1,0,0, 0,32,271,1,0,0,0,34,275,1,0,0,0,36,290,1,0,0,0,38,293,1,0,0,0,40,342, 1,0,0,0,42,345,1,0,0,0,44,351,1,0,0,0,46,355,1,0,0,0,48,361,1,0,0,0,50, @@ -226,14 +228,14 @@ void hogqlparserParserInitialize() { 541,1,0,0,0,80,549,1,0,0,0,82,567,1,0,0,0,84,569,1,0,0,0,86,577,1,0,0, 0,88,582,1,0,0,0,90,590,1,0,0,0,92,594,1,0,0,0,94,598,1,0,0,0,96,607, 1,0,0,0,98,621,1,0,0,0,100,623,1,0,0,0,102,673,1,0,0,0,104,675,1,0,0, - 0,106,812,1,0,0,0,108,910,1,0,0,0,110,920,1,0,0,0,112,941,1,0,0,0,114, - 974,1,0,0,0,116,987,1,0,0,0,118,989,1,0,0,0,120,1007,1,0,0,0,122,1016, - 1,0,0,0,124,1018,1,0,0,0,126,1035,1,0,0,0,128,1048,1,0,0,0,130,1058,1, - 0,0,0,132,1062,1,0,0,0,134,1070,1,0,0,0,136,1080,1,0,0,0,138,1083,1,0, - 0,0,140,1096,1,0,0,0,142,1098,1,0,0,0,144,1100,1,0,0,0,146,1102,1,0,0, - 0,148,1106,1,0,0,0,150,1111,1,0,0,0,152,1113,1,0,0,0,154,1117,1,0,0,0, - 156,1123,1,0,0,0,158,1125,1,0,0,0,160,1139,1,0,0,0,162,1141,1,0,0,0,164, - 1155,1,0,0,0,166,168,3,2,1,0,167,166,1,0,0,0,168,171,1,0,0,0,169,167, + 0,106,832,1,0,0,0,108,930,1,0,0,0,110,940,1,0,0,0,112,961,1,0,0,0,114, + 994,1,0,0,0,116,1007,1,0,0,0,118,1009,1,0,0,0,120,1027,1,0,0,0,122,1036, + 1,0,0,0,124,1038,1,0,0,0,126,1055,1,0,0,0,128,1068,1,0,0,0,130,1078,1, + 0,0,0,132,1082,1,0,0,0,134,1090,1,0,0,0,136,1100,1,0,0,0,138,1103,1,0, + 0,0,140,1116,1,0,0,0,142,1118,1,0,0,0,144,1120,1,0,0,0,146,1122,1,0,0, + 0,148,1126,1,0,0,0,150,1131,1,0,0,0,152,1133,1,0,0,0,154,1137,1,0,0,0, + 156,1143,1,0,0,0,158,1145,1,0,0,0,160,1159,1,0,0,0,162,1161,1,0,0,0,164, + 1175,1,0,0,0,166,168,3,2,1,0,167,166,1,0,0,0,168,171,1,0,0,0,169,167, 1,0,0,0,169,170,1,0,0,0,170,172,1,0,0,0,171,169,1,0,0,0,172,173,5,0,0, 1,173,1,1,0,0,0,174,177,3,6,3,0,175,177,3,12,6,0,176,174,1,0,0,0,176, 175,1,0,0,0,177,3,1,0,0,0,178,179,3,106,53,0,179,5,1,0,0,0,180,181,5, @@ -384,158 +386,165 @@ void hogqlparserParserInitialize() { 688,689,5,94,0,0,689,690,3,106,53,0,690,691,5,81,0,0,691,692,3,106,53, 0,692,694,1,0,0,0,693,688,1,0,0,0,694,695,1,0,0,0,695,693,1,0,0,0,695, 696,1,0,0,0,696,699,1,0,0,0,697,698,5,24,0,0,698,700,3,106,53,0,699,697, - 1,0,0,0,699,700,1,0,0,0,700,701,1,0,0,0,701,702,5,25,0,0,702,813,1,0, + 1,0,0,0,699,700,1,0,0,0,700,701,1,0,0,0,701,702,5,25,0,0,702,833,1,0, 0,0,703,704,5,13,0,0,704,705,5,126,0,0,705,706,3,106,53,0,706,707,5,6, - 0,0,707,708,3,102,51,0,708,709,5,144,0,0,709,813,1,0,0,0,710,711,5,19, - 0,0,711,813,5,106,0,0,712,713,5,43,0,0,713,714,3,106,53,0,714,715,3,142, - 71,0,715,813,1,0,0,0,716,717,5,80,0,0,717,718,5,126,0,0,718,719,3,106, + 0,0,707,708,3,102,51,0,708,709,5,144,0,0,709,833,1,0,0,0,710,711,5,19, + 0,0,711,833,5,106,0,0,712,713,5,43,0,0,713,714,3,106,53,0,714,715,3,142, + 71,0,715,833,1,0,0,0,716,717,5,80,0,0,717,718,5,126,0,0,718,719,3,106, 53,0,719,720,5,32,0,0,720,723,3,106,53,0,721,722,5,31,0,0,722,724,3,106, 53,0,723,721,1,0,0,0,723,724,1,0,0,0,724,725,1,0,0,0,725,726,5,144,0, - 0,726,813,1,0,0,0,727,728,5,83,0,0,728,813,5,106,0,0,729,730,5,88,0,0, + 0,726,833,1,0,0,0,727,728,5,83,0,0,728,833,5,106,0,0,729,730,5,88,0,0, 730,731,5,126,0,0,731,732,7,9,0,0,732,733,3,156,78,0,733,734,5,32,0,0, - 734,735,3,106,53,0,735,736,5,144,0,0,736,813,1,0,0,0,737,738,3,150,75, + 734,735,3,106,53,0,735,736,5,144,0,0,736,833,1,0,0,0,737,738,3,150,75, 0,738,740,5,126,0,0,739,741,3,104,52,0,740,739,1,0,0,0,740,741,1,0,0, - 0,741,742,1,0,0,0,742,743,5,144,0,0,743,744,1,0,0,0,744,745,5,64,0,0, - 745,746,5,126,0,0,746,747,3,88,44,0,747,748,5,144,0,0,748,813,1,0,0,0, - 749,750,3,150,75,0,750,752,5,126,0,0,751,753,3,104,52,0,752,751,1,0,0, - 0,752,753,1,0,0,0,753,754,1,0,0,0,754,755,5,144,0,0,755,756,1,0,0,0,756, - 757,5,64,0,0,757,758,3,150,75,0,758,813,1,0,0,0,759,765,3,150,75,0,760, - 762,5,126,0,0,761,763,3,104,52,0,762,761,1,0,0,0,762,763,1,0,0,0,763, - 764,1,0,0,0,764,766,5,144,0,0,765,760,1,0,0,0,765,766,1,0,0,0,766,767, - 1,0,0,0,767,769,5,126,0,0,768,770,5,23,0,0,769,768,1,0,0,0,769,770,1, - 0,0,0,770,772,1,0,0,0,771,773,3,108,54,0,772,771,1,0,0,0,772,773,1,0, - 0,0,773,774,1,0,0,0,774,775,5,144,0,0,775,813,1,0,0,0,776,813,3,114,57, - 0,777,813,3,158,79,0,778,813,3,140,70,0,779,780,5,114,0,0,780,813,3,106, - 53,19,781,782,5,56,0,0,782,813,3,106,53,13,783,784,3,130,65,0,784,785, - 5,116,0,0,785,787,1,0,0,0,786,783,1,0,0,0,786,787,1,0,0,0,787,788,1,0, - 0,0,788,813,5,108,0,0,789,790,5,126,0,0,790,791,3,34,17,0,791,792,5,144, - 0,0,792,813,1,0,0,0,793,794,5,126,0,0,794,795,3,106,53,0,795,796,5,144, - 0,0,796,813,1,0,0,0,797,798,5,126,0,0,798,799,3,104,52,0,799,800,5,144, - 0,0,800,813,1,0,0,0,801,803,5,125,0,0,802,804,3,104,52,0,803,802,1,0, - 0,0,803,804,1,0,0,0,804,805,1,0,0,0,805,813,5,143,0,0,806,808,5,124,0, - 0,807,809,3,30,15,0,808,807,1,0,0,0,808,809,1,0,0,0,809,810,1,0,0,0,810, - 813,5,142,0,0,811,813,3,122,61,0,812,683,1,0,0,0,812,703,1,0,0,0,812, - 710,1,0,0,0,812,712,1,0,0,0,812,716,1,0,0,0,812,727,1,0,0,0,812,729,1, - 0,0,0,812,737,1,0,0,0,812,749,1,0,0,0,812,759,1,0,0,0,812,776,1,0,0,0, - 812,777,1,0,0,0,812,778,1,0,0,0,812,779,1,0,0,0,812,781,1,0,0,0,812,786, - 1,0,0,0,812,789,1,0,0,0,812,793,1,0,0,0,812,797,1,0,0,0,812,801,1,0,0, - 0,812,806,1,0,0,0,812,811,1,0,0,0,813,907,1,0,0,0,814,818,10,18,0,0,815, - 819,5,108,0,0,816,819,5,146,0,0,817,819,5,133,0,0,818,815,1,0,0,0,818, - 816,1,0,0,0,818,817,1,0,0,0,819,820,1,0,0,0,820,906,3,106,53,19,821,825, - 10,17,0,0,822,826,5,134,0,0,823,826,5,114,0,0,824,826,5,113,0,0,825,822, - 1,0,0,0,825,823,1,0,0,0,825,824,1,0,0,0,826,827,1,0,0,0,827,906,3,106, - 53,18,828,853,10,16,0,0,829,854,5,117,0,0,830,854,5,118,0,0,831,854,5, - 129,0,0,832,854,5,127,0,0,833,854,5,128,0,0,834,854,5,119,0,0,835,854, - 5,120,0,0,836,838,5,56,0,0,837,836,1,0,0,0,837,838,1,0,0,0,838,839,1, - 0,0,0,839,841,5,40,0,0,840,842,5,14,0,0,841,840,1,0,0,0,841,842,1,0,0, - 0,842,854,1,0,0,0,843,845,5,56,0,0,844,843,1,0,0,0,844,845,1,0,0,0,845, - 846,1,0,0,0,846,854,7,10,0,0,847,854,5,140,0,0,848,854,5,141,0,0,849, - 854,5,131,0,0,850,854,5,122,0,0,851,854,5,123,0,0,852,854,5,130,0,0,853, - 829,1,0,0,0,853,830,1,0,0,0,853,831,1,0,0,0,853,832,1,0,0,0,853,833,1, - 0,0,0,853,834,1,0,0,0,853,835,1,0,0,0,853,837,1,0,0,0,853,844,1,0,0,0, - 853,847,1,0,0,0,853,848,1,0,0,0,853,849,1,0,0,0,853,850,1,0,0,0,853,851, - 1,0,0,0,853,852,1,0,0,0,854,855,1,0,0,0,855,906,3,106,53,17,856,857,10, - 14,0,0,857,858,5,132,0,0,858,906,3,106,53,15,859,860,10,12,0,0,860,861, - 5,2,0,0,861,906,3,106,53,13,862,863,10,11,0,0,863,864,5,61,0,0,864,906, - 3,106,53,12,865,867,10,10,0,0,866,868,5,56,0,0,867,866,1,0,0,0,867,868, - 1,0,0,0,868,869,1,0,0,0,869,870,5,9,0,0,870,871,3,106,53,0,871,872,5, - 2,0,0,872,873,3,106,53,11,873,906,1,0,0,0,874,875,10,9,0,0,875,876,5, - 135,0,0,876,877,3,106,53,0,877,878,5,111,0,0,878,879,3,106,53,9,879,906, - 1,0,0,0,880,881,10,22,0,0,881,882,5,125,0,0,882,883,3,106,53,0,883,884, - 5,143,0,0,884,906,1,0,0,0,885,886,10,21,0,0,886,887,5,116,0,0,887,906, - 5,104,0,0,888,889,10,20,0,0,889,890,5,116,0,0,890,906,3,150,75,0,891, - 892,10,15,0,0,892,894,5,44,0,0,893,895,5,56,0,0,894,893,1,0,0,0,894,895, - 1,0,0,0,895,896,1,0,0,0,896,906,5,57,0,0,897,903,10,8,0,0,898,904,3,148, - 74,0,899,900,5,6,0,0,900,904,3,150,75,0,901,902,5,6,0,0,902,904,5,106, - 0,0,903,898,1,0,0,0,903,899,1,0,0,0,903,901,1,0,0,0,904,906,1,0,0,0,905, - 814,1,0,0,0,905,821,1,0,0,0,905,828,1,0,0,0,905,856,1,0,0,0,905,859,1, - 0,0,0,905,862,1,0,0,0,905,865,1,0,0,0,905,874,1,0,0,0,905,880,1,0,0,0, - 905,885,1,0,0,0,905,888,1,0,0,0,905,891,1,0,0,0,905,897,1,0,0,0,906,909, - 1,0,0,0,907,905,1,0,0,0,907,908,1,0,0,0,908,107,1,0,0,0,909,907,1,0,0, - 0,910,915,3,110,55,0,911,912,5,112,0,0,912,914,3,110,55,0,913,911,1,0, - 0,0,914,917,1,0,0,0,915,913,1,0,0,0,915,916,1,0,0,0,916,109,1,0,0,0,917, - 915,1,0,0,0,918,921,3,112,56,0,919,921,3,106,53,0,920,918,1,0,0,0,920, - 919,1,0,0,0,921,111,1,0,0,0,922,923,5,126,0,0,923,928,3,150,75,0,924, - 925,5,112,0,0,925,927,3,150,75,0,926,924,1,0,0,0,927,930,1,0,0,0,928, - 926,1,0,0,0,928,929,1,0,0,0,929,931,1,0,0,0,930,928,1,0,0,0,931,932,5, - 144,0,0,932,942,1,0,0,0,933,938,3,150,75,0,934,935,5,112,0,0,935,937, - 3,150,75,0,936,934,1,0,0,0,937,940,1,0,0,0,938,936,1,0,0,0,938,939,1, - 0,0,0,939,942,1,0,0,0,940,938,1,0,0,0,941,922,1,0,0,0,941,933,1,0,0,0, - 942,943,1,0,0,0,943,944,5,107,0,0,944,945,3,106,53,0,945,113,1,0,0,0, - 946,947,5,128,0,0,947,951,3,150,75,0,948,950,3,116,58,0,949,948,1,0,0, - 0,950,953,1,0,0,0,951,949,1,0,0,0,951,952,1,0,0,0,952,954,1,0,0,0,953, - 951,1,0,0,0,954,955,5,146,0,0,955,956,5,120,0,0,956,975,1,0,0,0,957,958, - 5,128,0,0,958,962,3,150,75,0,959,961,3,116,58,0,960,959,1,0,0,0,961,964, - 1,0,0,0,962,960,1,0,0,0,962,963,1,0,0,0,963,965,1,0,0,0,964,962,1,0,0, - 0,965,967,5,120,0,0,966,968,3,114,57,0,967,966,1,0,0,0,967,968,1,0,0, - 0,968,969,1,0,0,0,969,970,5,128,0,0,970,971,5,146,0,0,971,972,3,150,75, - 0,972,973,5,120,0,0,973,975,1,0,0,0,974,946,1,0,0,0,974,957,1,0,0,0,975, - 115,1,0,0,0,976,977,3,150,75,0,977,978,5,118,0,0,978,979,3,156,78,0,979, - 988,1,0,0,0,980,981,3,150,75,0,981,982,5,118,0,0,982,983,5,124,0,0,983, - 984,3,106,53,0,984,985,5,142,0,0,985,988,1,0,0,0,986,988,3,150,75,0,987, - 976,1,0,0,0,987,980,1,0,0,0,987,986,1,0,0,0,988,117,1,0,0,0,989,994,3, - 120,60,0,990,991,5,112,0,0,991,993,3,120,60,0,992,990,1,0,0,0,993,996, - 1,0,0,0,994,992,1,0,0,0,994,995,1,0,0,0,995,119,1,0,0,0,996,994,1,0,0, - 0,997,998,3,150,75,0,998,999,5,6,0,0,999,1000,5,126,0,0,1000,1001,3,34, - 17,0,1001,1002,5,144,0,0,1002,1008,1,0,0,0,1003,1004,3,106,53,0,1004, - 1005,5,6,0,0,1005,1006,3,150,75,0,1006,1008,1,0,0,0,1007,997,1,0,0,0, - 1007,1003,1,0,0,0,1008,121,1,0,0,0,1009,1017,3,154,77,0,1010,1011,3,130, - 65,0,1011,1012,5,116,0,0,1012,1014,1,0,0,0,1013,1010,1,0,0,0,1013,1014, - 1,0,0,0,1014,1015,1,0,0,0,1015,1017,3,124,62,0,1016,1009,1,0,0,0,1016, - 1013,1,0,0,0,1017,123,1,0,0,0,1018,1023,3,150,75,0,1019,1020,5,116,0, - 0,1020,1022,3,150,75,0,1021,1019,1,0,0,0,1022,1025,1,0,0,0,1023,1021, - 1,0,0,0,1023,1024,1,0,0,0,1024,125,1,0,0,0,1025,1023,1,0,0,0,1026,1027, - 6,63,-1,0,1027,1036,3,130,65,0,1028,1036,3,128,64,0,1029,1030,5,126,0, - 0,1030,1031,3,34,17,0,1031,1032,5,144,0,0,1032,1036,1,0,0,0,1033,1036, - 3,114,57,0,1034,1036,3,154,77,0,1035,1026,1,0,0,0,1035,1028,1,0,0,0,1035, - 1029,1,0,0,0,1035,1033,1,0,0,0,1035,1034,1,0,0,0,1036,1045,1,0,0,0,1037, - 1041,10,3,0,0,1038,1042,3,148,74,0,1039,1040,5,6,0,0,1040,1042,3,150, - 75,0,1041,1038,1,0,0,0,1041,1039,1,0,0,0,1042,1044,1,0,0,0,1043,1037, - 1,0,0,0,1044,1047,1,0,0,0,1045,1043,1,0,0,0,1045,1046,1,0,0,0,1046,127, - 1,0,0,0,1047,1045,1,0,0,0,1048,1049,3,150,75,0,1049,1051,5,126,0,0,1050, - 1052,3,132,66,0,1051,1050,1,0,0,0,1051,1052,1,0,0,0,1052,1053,1,0,0,0, - 1053,1054,5,144,0,0,1054,129,1,0,0,0,1055,1056,3,134,67,0,1056,1057,5, - 116,0,0,1057,1059,1,0,0,0,1058,1055,1,0,0,0,1058,1059,1,0,0,0,1059,1060, - 1,0,0,0,1060,1061,3,150,75,0,1061,131,1,0,0,0,1062,1067,3,106,53,0,1063, - 1064,5,112,0,0,1064,1066,3,106,53,0,1065,1063,1,0,0,0,1066,1069,1,0,0, - 0,1067,1065,1,0,0,0,1067,1068,1,0,0,0,1068,133,1,0,0,0,1069,1067,1,0, - 0,0,1070,1071,3,150,75,0,1071,135,1,0,0,0,1072,1081,5,102,0,0,1073,1074, - 5,116,0,0,1074,1081,7,11,0,0,1075,1076,5,104,0,0,1076,1078,5,116,0,0, - 1077,1079,7,11,0,0,1078,1077,1,0,0,0,1078,1079,1,0,0,0,1079,1081,1,0, - 0,0,1080,1072,1,0,0,0,1080,1073,1,0,0,0,1080,1075,1,0,0,0,1081,137,1, - 0,0,0,1082,1084,7,12,0,0,1083,1082,1,0,0,0,1083,1084,1,0,0,0,1084,1091, - 1,0,0,0,1085,1092,3,136,68,0,1086,1092,5,103,0,0,1087,1092,5,104,0,0, - 1088,1092,5,105,0,0,1089,1092,5,41,0,0,1090,1092,5,55,0,0,1091,1085,1, - 0,0,0,1091,1086,1,0,0,0,1091,1087,1,0,0,0,1091,1088,1,0,0,0,1091,1089, - 1,0,0,0,1091,1090,1,0,0,0,1092,139,1,0,0,0,1093,1097,3,138,69,0,1094, - 1097,5,106,0,0,1095,1097,5,57,0,0,1096,1093,1,0,0,0,1096,1094,1,0,0,0, - 1096,1095,1,0,0,0,1097,141,1,0,0,0,1098,1099,7,13,0,0,1099,143,1,0,0, - 0,1100,1101,7,14,0,0,1101,145,1,0,0,0,1102,1103,7,15,0,0,1103,147,1,0, - 0,0,1104,1107,5,101,0,0,1105,1107,3,146,73,0,1106,1104,1,0,0,0,1106,1105, - 1,0,0,0,1107,149,1,0,0,0,1108,1112,5,101,0,0,1109,1112,3,142,71,0,1110, - 1112,3,144,72,0,1111,1108,1,0,0,0,1111,1109,1,0,0,0,1111,1110,1,0,0,0, - 1112,151,1,0,0,0,1113,1114,3,156,78,0,1114,1115,5,118,0,0,1115,1116,3, - 138,69,0,1116,153,1,0,0,0,1117,1118,5,124,0,0,1118,1119,3,150,75,0,1119, - 1120,5,142,0,0,1120,155,1,0,0,0,1121,1124,5,106,0,0,1122,1124,3,158,79, - 0,1123,1121,1,0,0,0,1123,1122,1,0,0,0,1124,157,1,0,0,0,1125,1129,5,137, - 0,0,1126,1128,3,160,80,0,1127,1126,1,0,0,0,1128,1131,1,0,0,0,1129,1127, - 1,0,0,0,1129,1130,1,0,0,0,1130,1132,1,0,0,0,1131,1129,1,0,0,0,1132,1133, - 5,139,0,0,1133,159,1,0,0,0,1134,1135,5,152,0,0,1135,1136,3,106,53,0,1136, - 1137,5,142,0,0,1137,1140,1,0,0,0,1138,1140,5,151,0,0,1139,1134,1,0,0, - 0,1139,1138,1,0,0,0,1140,161,1,0,0,0,1141,1145,5,138,0,0,1142,1144,3, - 164,82,0,1143,1142,1,0,0,0,1144,1147,1,0,0,0,1145,1143,1,0,0,0,1145,1146, - 1,0,0,0,1146,1148,1,0,0,0,1147,1145,1,0,0,0,1148,1149,5,0,0,1,1149,163, - 1,0,0,0,1150,1151,5,154,0,0,1151,1152,3,106,53,0,1152,1153,5,142,0,0, - 1153,1156,1,0,0,0,1154,1156,5,153,0,0,1155,1150,1,0,0,0,1155,1154,1,0, - 0,0,1156,165,1,0,0,0,135,169,176,185,200,212,224,240,251,265,271,281, + 0,741,742,1,0,0,0,742,743,5,144,0,0,743,752,1,0,0,0,744,746,5,126,0,0, + 745,747,5,23,0,0,746,745,1,0,0,0,746,747,1,0,0,0,747,749,1,0,0,0,748, + 750,3,108,54,0,749,748,1,0,0,0,749,750,1,0,0,0,750,751,1,0,0,0,751,753, + 5,144,0,0,752,744,1,0,0,0,752,753,1,0,0,0,753,754,1,0,0,0,754,755,5,64, + 0,0,755,756,5,126,0,0,756,757,3,88,44,0,757,758,5,144,0,0,758,833,1,0, + 0,0,759,760,3,150,75,0,760,762,5,126,0,0,761,763,3,104,52,0,762,761,1, + 0,0,0,762,763,1,0,0,0,763,764,1,0,0,0,764,765,5,144,0,0,765,774,1,0,0, + 0,766,768,5,126,0,0,767,769,5,23,0,0,768,767,1,0,0,0,768,769,1,0,0,0, + 769,771,1,0,0,0,770,772,3,108,54,0,771,770,1,0,0,0,771,772,1,0,0,0,772, + 773,1,0,0,0,773,775,5,144,0,0,774,766,1,0,0,0,774,775,1,0,0,0,775,776, + 1,0,0,0,776,777,5,64,0,0,777,778,3,150,75,0,778,833,1,0,0,0,779,785,3, + 150,75,0,780,782,5,126,0,0,781,783,3,104,52,0,782,781,1,0,0,0,782,783, + 1,0,0,0,783,784,1,0,0,0,784,786,5,144,0,0,785,780,1,0,0,0,785,786,1,0, + 0,0,786,787,1,0,0,0,787,789,5,126,0,0,788,790,5,23,0,0,789,788,1,0,0, + 0,789,790,1,0,0,0,790,792,1,0,0,0,791,793,3,108,54,0,792,791,1,0,0,0, + 792,793,1,0,0,0,793,794,1,0,0,0,794,795,5,144,0,0,795,833,1,0,0,0,796, + 833,3,114,57,0,797,833,3,158,79,0,798,833,3,140,70,0,799,800,5,114,0, + 0,800,833,3,106,53,19,801,802,5,56,0,0,802,833,3,106,53,13,803,804,3, + 130,65,0,804,805,5,116,0,0,805,807,1,0,0,0,806,803,1,0,0,0,806,807,1, + 0,0,0,807,808,1,0,0,0,808,833,5,108,0,0,809,810,5,126,0,0,810,811,3,34, + 17,0,811,812,5,144,0,0,812,833,1,0,0,0,813,814,5,126,0,0,814,815,3,106, + 53,0,815,816,5,144,0,0,816,833,1,0,0,0,817,818,5,126,0,0,818,819,3,104, + 52,0,819,820,5,144,0,0,820,833,1,0,0,0,821,823,5,125,0,0,822,824,3,104, + 52,0,823,822,1,0,0,0,823,824,1,0,0,0,824,825,1,0,0,0,825,833,5,143,0, + 0,826,828,5,124,0,0,827,829,3,30,15,0,828,827,1,0,0,0,828,829,1,0,0,0, + 829,830,1,0,0,0,830,833,5,142,0,0,831,833,3,122,61,0,832,683,1,0,0,0, + 832,703,1,0,0,0,832,710,1,0,0,0,832,712,1,0,0,0,832,716,1,0,0,0,832,727, + 1,0,0,0,832,729,1,0,0,0,832,737,1,0,0,0,832,759,1,0,0,0,832,779,1,0,0, + 0,832,796,1,0,0,0,832,797,1,0,0,0,832,798,1,0,0,0,832,799,1,0,0,0,832, + 801,1,0,0,0,832,806,1,0,0,0,832,809,1,0,0,0,832,813,1,0,0,0,832,817,1, + 0,0,0,832,821,1,0,0,0,832,826,1,0,0,0,832,831,1,0,0,0,833,927,1,0,0,0, + 834,838,10,18,0,0,835,839,5,108,0,0,836,839,5,146,0,0,837,839,5,133,0, + 0,838,835,1,0,0,0,838,836,1,0,0,0,838,837,1,0,0,0,839,840,1,0,0,0,840, + 926,3,106,53,19,841,845,10,17,0,0,842,846,5,134,0,0,843,846,5,114,0,0, + 844,846,5,113,0,0,845,842,1,0,0,0,845,843,1,0,0,0,845,844,1,0,0,0,846, + 847,1,0,0,0,847,926,3,106,53,18,848,873,10,16,0,0,849,874,5,117,0,0,850, + 874,5,118,0,0,851,874,5,129,0,0,852,874,5,127,0,0,853,874,5,128,0,0,854, + 874,5,119,0,0,855,874,5,120,0,0,856,858,5,56,0,0,857,856,1,0,0,0,857, + 858,1,0,0,0,858,859,1,0,0,0,859,861,5,40,0,0,860,862,5,14,0,0,861,860, + 1,0,0,0,861,862,1,0,0,0,862,874,1,0,0,0,863,865,5,56,0,0,864,863,1,0, + 0,0,864,865,1,0,0,0,865,866,1,0,0,0,866,874,7,10,0,0,867,874,5,140,0, + 0,868,874,5,141,0,0,869,874,5,131,0,0,870,874,5,122,0,0,871,874,5,123, + 0,0,872,874,5,130,0,0,873,849,1,0,0,0,873,850,1,0,0,0,873,851,1,0,0,0, + 873,852,1,0,0,0,873,853,1,0,0,0,873,854,1,0,0,0,873,855,1,0,0,0,873,857, + 1,0,0,0,873,864,1,0,0,0,873,867,1,0,0,0,873,868,1,0,0,0,873,869,1,0,0, + 0,873,870,1,0,0,0,873,871,1,0,0,0,873,872,1,0,0,0,874,875,1,0,0,0,875, + 926,3,106,53,17,876,877,10,14,0,0,877,878,5,132,0,0,878,926,3,106,53, + 15,879,880,10,12,0,0,880,881,5,2,0,0,881,926,3,106,53,13,882,883,10,11, + 0,0,883,884,5,61,0,0,884,926,3,106,53,12,885,887,10,10,0,0,886,888,5, + 56,0,0,887,886,1,0,0,0,887,888,1,0,0,0,888,889,1,0,0,0,889,890,5,9,0, + 0,890,891,3,106,53,0,891,892,5,2,0,0,892,893,3,106,53,11,893,926,1,0, + 0,0,894,895,10,9,0,0,895,896,5,135,0,0,896,897,3,106,53,0,897,898,5,111, + 0,0,898,899,3,106,53,9,899,926,1,0,0,0,900,901,10,22,0,0,901,902,5,125, + 0,0,902,903,3,106,53,0,903,904,5,143,0,0,904,926,1,0,0,0,905,906,10,21, + 0,0,906,907,5,116,0,0,907,926,5,104,0,0,908,909,10,20,0,0,909,910,5,116, + 0,0,910,926,3,150,75,0,911,912,10,15,0,0,912,914,5,44,0,0,913,915,5,56, + 0,0,914,913,1,0,0,0,914,915,1,0,0,0,915,916,1,0,0,0,916,926,5,57,0,0, + 917,923,10,8,0,0,918,924,3,148,74,0,919,920,5,6,0,0,920,924,3,150,75, + 0,921,922,5,6,0,0,922,924,5,106,0,0,923,918,1,0,0,0,923,919,1,0,0,0,923, + 921,1,0,0,0,924,926,1,0,0,0,925,834,1,0,0,0,925,841,1,0,0,0,925,848,1, + 0,0,0,925,876,1,0,0,0,925,879,1,0,0,0,925,882,1,0,0,0,925,885,1,0,0,0, + 925,894,1,0,0,0,925,900,1,0,0,0,925,905,1,0,0,0,925,908,1,0,0,0,925,911, + 1,0,0,0,925,917,1,0,0,0,926,929,1,0,0,0,927,925,1,0,0,0,927,928,1,0,0, + 0,928,107,1,0,0,0,929,927,1,0,0,0,930,935,3,110,55,0,931,932,5,112,0, + 0,932,934,3,110,55,0,933,931,1,0,0,0,934,937,1,0,0,0,935,933,1,0,0,0, + 935,936,1,0,0,0,936,109,1,0,0,0,937,935,1,0,0,0,938,941,3,112,56,0,939, + 941,3,106,53,0,940,938,1,0,0,0,940,939,1,0,0,0,941,111,1,0,0,0,942,943, + 5,126,0,0,943,948,3,150,75,0,944,945,5,112,0,0,945,947,3,150,75,0,946, + 944,1,0,0,0,947,950,1,0,0,0,948,946,1,0,0,0,948,949,1,0,0,0,949,951,1, + 0,0,0,950,948,1,0,0,0,951,952,5,144,0,0,952,962,1,0,0,0,953,958,3,150, + 75,0,954,955,5,112,0,0,955,957,3,150,75,0,956,954,1,0,0,0,957,960,1,0, + 0,0,958,956,1,0,0,0,958,959,1,0,0,0,959,962,1,0,0,0,960,958,1,0,0,0,961, + 942,1,0,0,0,961,953,1,0,0,0,962,963,1,0,0,0,963,964,5,107,0,0,964,965, + 3,106,53,0,965,113,1,0,0,0,966,967,5,128,0,0,967,971,3,150,75,0,968,970, + 3,116,58,0,969,968,1,0,0,0,970,973,1,0,0,0,971,969,1,0,0,0,971,972,1, + 0,0,0,972,974,1,0,0,0,973,971,1,0,0,0,974,975,5,146,0,0,975,976,5,120, + 0,0,976,995,1,0,0,0,977,978,5,128,0,0,978,982,3,150,75,0,979,981,3,116, + 58,0,980,979,1,0,0,0,981,984,1,0,0,0,982,980,1,0,0,0,982,983,1,0,0,0, + 983,985,1,0,0,0,984,982,1,0,0,0,985,987,5,120,0,0,986,988,3,114,57,0, + 987,986,1,0,0,0,987,988,1,0,0,0,988,989,1,0,0,0,989,990,5,128,0,0,990, + 991,5,146,0,0,991,992,3,150,75,0,992,993,5,120,0,0,993,995,1,0,0,0,994, + 966,1,0,0,0,994,977,1,0,0,0,995,115,1,0,0,0,996,997,3,150,75,0,997,998, + 5,118,0,0,998,999,3,156,78,0,999,1008,1,0,0,0,1000,1001,3,150,75,0,1001, + 1002,5,118,0,0,1002,1003,5,124,0,0,1003,1004,3,106,53,0,1004,1005,5,142, + 0,0,1005,1008,1,0,0,0,1006,1008,3,150,75,0,1007,996,1,0,0,0,1007,1000, + 1,0,0,0,1007,1006,1,0,0,0,1008,117,1,0,0,0,1009,1014,3,120,60,0,1010, + 1011,5,112,0,0,1011,1013,3,120,60,0,1012,1010,1,0,0,0,1013,1016,1,0,0, + 0,1014,1012,1,0,0,0,1014,1015,1,0,0,0,1015,119,1,0,0,0,1016,1014,1,0, + 0,0,1017,1018,3,150,75,0,1018,1019,5,6,0,0,1019,1020,5,126,0,0,1020,1021, + 3,34,17,0,1021,1022,5,144,0,0,1022,1028,1,0,0,0,1023,1024,3,106,53,0, + 1024,1025,5,6,0,0,1025,1026,3,150,75,0,1026,1028,1,0,0,0,1027,1017,1, + 0,0,0,1027,1023,1,0,0,0,1028,121,1,0,0,0,1029,1037,3,154,77,0,1030,1031, + 3,130,65,0,1031,1032,5,116,0,0,1032,1034,1,0,0,0,1033,1030,1,0,0,0,1033, + 1034,1,0,0,0,1034,1035,1,0,0,0,1035,1037,3,124,62,0,1036,1029,1,0,0,0, + 1036,1033,1,0,0,0,1037,123,1,0,0,0,1038,1043,3,150,75,0,1039,1040,5,116, + 0,0,1040,1042,3,150,75,0,1041,1039,1,0,0,0,1042,1045,1,0,0,0,1043,1041, + 1,0,0,0,1043,1044,1,0,0,0,1044,125,1,0,0,0,1045,1043,1,0,0,0,1046,1047, + 6,63,-1,0,1047,1056,3,130,65,0,1048,1056,3,128,64,0,1049,1050,5,126,0, + 0,1050,1051,3,34,17,0,1051,1052,5,144,0,0,1052,1056,1,0,0,0,1053,1056, + 3,114,57,0,1054,1056,3,154,77,0,1055,1046,1,0,0,0,1055,1048,1,0,0,0,1055, + 1049,1,0,0,0,1055,1053,1,0,0,0,1055,1054,1,0,0,0,1056,1065,1,0,0,0,1057, + 1061,10,3,0,0,1058,1062,3,148,74,0,1059,1060,5,6,0,0,1060,1062,3,150, + 75,0,1061,1058,1,0,0,0,1061,1059,1,0,0,0,1062,1064,1,0,0,0,1063,1057, + 1,0,0,0,1064,1067,1,0,0,0,1065,1063,1,0,0,0,1065,1066,1,0,0,0,1066,127, + 1,0,0,0,1067,1065,1,0,0,0,1068,1069,3,150,75,0,1069,1071,5,126,0,0,1070, + 1072,3,132,66,0,1071,1070,1,0,0,0,1071,1072,1,0,0,0,1072,1073,1,0,0,0, + 1073,1074,5,144,0,0,1074,129,1,0,0,0,1075,1076,3,134,67,0,1076,1077,5, + 116,0,0,1077,1079,1,0,0,0,1078,1075,1,0,0,0,1078,1079,1,0,0,0,1079,1080, + 1,0,0,0,1080,1081,3,150,75,0,1081,131,1,0,0,0,1082,1087,3,106,53,0,1083, + 1084,5,112,0,0,1084,1086,3,106,53,0,1085,1083,1,0,0,0,1086,1089,1,0,0, + 0,1087,1085,1,0,0,0,1087,1088,1,0,0,0,1088,133,1,0,0,0,1089,1087,1,0, + 0,0,1090,1091,3,150,75,0,1091,135,1,0,0,0,1092,1101,5,102,0,0,1093,1094, + 5,116,0,0,1094,1101,7,11,0,0,1095,1096,5,104,0,0,1096,1098,5,116,0,0, + 1097,1099,7,11,0,0,1098,1097,1,0,0,0,1098,1099,1,0,0,0,1099,1101,1,0, + 0,0,1100,1092,1,0,0,0,1100,1093,1,0,0,0,1100,1095,1,0,0,0,1101,137,1, + 0,0,0,1102,1104,7,12,0,0,1103,1102,1,0,0,0,1103,1104,1,0,0,0,1104,1111, + 1,0,0,0,1105,1112,3,136,68,0,1106,1112,5,103,0,0,1107,1112,5,104,0,0, + 1108,1112,5,105,0,0,1109,1112,5,41,0,0,1110,1112,5,55,0,0,1111,1105,1, + 0,0,0,1111,1106,1,0,0,0,1111,1107,1,0,0,0,1111,1108,1,0,0,0,1111,1109, + 1,0,0,0,1111,1110,1,0,0,0,1112,139,1,0,0,0,1113,1117,3,138,69,0,1114, + 1117,5,106,0,0,1115,1117,5,57,0,0,1116,1113,1,0,0,0,1116,1114,1,0,0,0, + 1116,1115,1,0,0,0,1117,141,1,0,0,0,1118,1119,7,13,0,0,1119,143,1,0,0, + 0,1120,1121,7,14,0,0,1121,145,1,0,0,0,1122,1123,7,15,0,0,1123,147,1,0, + 0,0,1124,1127,5,101,0,0,1125,1127,3,146,73,0,1126,1124,1,0,0,0,1126,1125, + 1,0,0,0,1127,149,1,0,0,0,1128,1132,5,101,0,0,1129,1132,3,142,71,0,1130, + 1132,3,144,72,0,1131,1128,1,0,0,0,1131,1129,1,0,0,0,1131,1130,1,0,0,0, + 1132,151,1,0,0,0,1133,1134,3,156,78,0,1134,1135,5,118,0,0,1135,1136,3, + 138,69,0,1136,153,1,0,0,0,1137,1138,5,124,0,0,1138,1139,3,150,75,0,1139, + 1140,5,142,0,0,1140,155,1,0,0,0,1141,1144,5,106,0,0,1142,1144,3,158,79, + 0,1143,1141,1,0,0,0,1143,1142,1,0,0,0,1144,157,1,0,0,0,1145,1149,5,137, + 0,0,1146,1148,3,160,80,0,1147,1146,1,0,0,0,1148,1151,1,0,0,0,1149,1147, + 1,0,0,0,1149,1150,1,0,0,0,1150,1152,1,0,0,0,1151,1149,1,0,0,0,1152,1153, + 5,139,0,0,1153,159,1,0,0,0,1154,1155,5,152,0,0,1155,1156,3,106,53,0,1156, + 1157,5,142,0,0,1157,1160,1,0,0,0,1158,1160,5,151,0,0,1159,1154,1,0,0, + 0,1159,1158,1,0,0,0,1160,161,1,0,0,0,1161,1165,5,138,0,0,1162,1164,3, + 164,82,0,1163,1162,1,0,0,0,1164,1167,1,0,0,0,1165,1163,1,0,0,0,1165,1166, + 1,0,0,0,1166,1168,1,0,0,0,1167,1165,1,0,0,0,1168,1169,5,0,0,1,1169,163, + 1,0,0,0,1170,1171,5,154,0,0,1171,1172,3,106,53,0,1172,1173,5,142,0,0, + 1173,1176,1,0,0,0,1174,1176,5,153,0,0,1175,1170,1,0,0,0,1175,1174,1,0, + 0,0,1176,165,1,0,0,0,141,169,176,185,200,212,224,240,251,265,271,281, 290,293,297,300,304,307,310,313,316,320,324,327,330,333,337,340,349,355, 376,393,410,416,422,433,435,446,449,455,463,469,471,475,480,483,486,490, 494,497,499,502,506,510,513,515,517,522,533,539,546,551,555,559,565,567, - 574,582,585,588,607,621,637,649,661,669,673,680,686,695,699,723,740,752, - 762,765,769,772,786,803,808,812,818,825,837,841,844,853,867,894,903,905, - 907,915,920,928,938,941,951,962,967,974,987,994,1007,1013,1016,1023,1035, - 1041,1045,1051,1058,1067,1078,1080,1083,1091,1096,1106,1111,1123,1129, - 1139,1145,1155 + 574,582,585,588,607,621,637,649,661,669,673,680,686,695,699,723,740,746, + 749,752,762,768,771,774,782,785,789,792,806,823,828,832,838,845,857,861, + 864,873,887,914,923,925,927,935,940,948,958,961,971,982,987,994,1007, + 1014,1027,1033,1036,1043,1055,1061,1065,1071,1078,1087,1098,1100,1103, + 1111,1116,1126,1131,1143,1149,1159,1165,1175 }; staticData->serializedATN = antlr4::atn::SerializedATNView(serializedATNSegment, sizeof(serializedATNSegment) / sizeof(serializedATNSegment[0])); @@ -6335,18 +6344,34 @@ tree::TerminalNode* HogQLParser::ColumnExprWinFunctionTargetContext::OVER() { return getToken(HogQLParser::OVER, 0); } -tree::TerminalNode* HogQLParser::ColumnExprWinFunctionTargetContext::LPAREN() { - return getToken(HogQLParser::LPAREN, 0); +std::vector HogQLParser::ColumnExprWinFunctionTargetContext::LPAREN() { + return getTokens(HogQLParser::LPAREN); } -tree::TerminalNode* HogQLParser::ColumnExprWinFunctionTargetContext::RPAREN() { - return getToken(HogQLParser::RPAREN, 0); +tree::TerminalNode* HogQLParser::ColumnExprWinFunctionTargetContext::LPAREN(size_t i) { + return getToken(HogQLParser::LPAREN, i); +} + +std::vector HogQLParser::ColumnExprWinFunctionTargetContext::RPAREN() { + return getTokens(HogQLParser::RPAREN); +} + +tree::TerminalNode* HogQLParser::ColumnExprWinFunctionTargetContext::RPAREN(size_t i) { + return getToken(HogQLParser::RPAREN, i); } HogQLParser::ColumnExprListContext* HogQLParser::ColumnExprWinFunctionTargetContext::columnExprList() { return getRuleContext(0); } +tree::TerminalNode* HogQLParser::ColumnExprWinFunctionTargetContext::DISTINCT() { + return getToken(HogQLParser::DISTINCT, 0); +} + +HogQLParser::ColumnArgListContext* HogQLParser::ColumnExprWinFunctionTargetContext::columnArgList() { + return getRuleContext(0); +} + HogQLParser::ColumnExprWinFunctionTargetContext::ColumnExprWinFunctionTargetContext(ColumnExprContext *ctx) { copyFrom(ctx); } @@ -6767,6 +6792,14 @@ HogQLParser::ColumnExprListContext* HogQLParser::ColumnExprWinFunctionContext::c return getRuleContext(0); } +tree::TerminalNode* HogQLParser::ColumnExprWinFunctionContext::DISTINCT() { + return getToken(HogQLParser::DISTINCT, 0); +} + +HogQLParser::ColumnArgListContext* HogQLParser::ColumnExprWinFunctionContext::columnArgList() { + return getRuleContext(0); +} + HogQLParser::ColumnExprWinFunctionContext::ColumnExprWinFunctionContext(ColumnExprContext *ctx) { copyFrom(ctx); } @@ -6883,9 +6916,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(812); + setState(832); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 90, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 96, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; @@ -7072,13 +7105,47 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { } setState(742); match(HogQLParser::RPAREN); - setState(744); + setState(752); + _errHandler->sync(this); + + _la = _input->LA(1); + if (_la == HogQLParser::LPAREN) { + setState(744); + match(HogQLParser::LPAREN); + setState(746); + _errHandler->sync(this); + + switch (getInterpreter()->adaptivePredict(_input, 82, _ctx)) { + case 1: { + setState(745); + match(HogQLParser::DISTINCT); + break; + } + + default: + break; + } + setState(749); + _errHandler->sync(this); + + _la = _input->LA(1); + if ((((_la & ~ 0x3fULL) == 0) && + ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && + ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && + ((1ULL << (_la - 128)) & 577) != 0)) { + setState(748); + columnArgList(); + } + setState(751); + match(HogQLParser::RPAREN); + } + setState(754); match(HogQLParser::OVER); - setState(745); + setState(755); match(HogQLParser::LPAREN); - setState(746); + setState(756); windowExpr(); - setState(747); + setState(757); match(HogQLParser::RPAREN); break; } @@ -7087,12 +7154,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(749); + setState(759); identifier(); - setState(750); + setState(760); match(HogQLParser::LPAREN); - setState(752); + setState(762); _errHandler->sync(this); _la = _input->LA(1); @@ -7100,14 +7167,48 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(751); + setState(761); columnExprList(); } - setState(754); + setState(764); match(HogQLParser::RPAREN); - setState(756); + setState(774); + _errHandler->sync(this); + + _la = _input->LA(1); + if (_la == HogQLParser::LPAREN) { + setState(766); + match(HogQLParser::LPAREN); + setState(768); + _errHandler->sync(this); + + switch (getInterpreter()->adaptivePredict(_input, 86, _ctx)) { + case 1: { + setState(767); + match(HogQLParser::DISTINCT); + break; + } + + default: + break; + } + setState(771); + _errHandler->sync(this); + + _la = _input->LA(1); + if ((((_la & ~ 0x3fULL) == 0) && + ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && + ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && + ((1ULL << (_la - 128)) & 577) != 0)) { + setState(770); + columnArgList(); + } + setState(773); + match(HogQLParser::RPAREN); + } + setState(776); match(HogQLParser::OVER); - setState(757); + setState(777); identifier(); break; } @@ -7116,16 +7217,16 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(759); + setState(779); identifier(); - setState(765); + setState(785); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 84, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 90, _ctx)) { case 1: { - setState(760); + setState(780); match(HogQLParser::LPAREN); - setState(762); + setState(782); _errHandler->sync(this); _la = _input->LA(1); @@ -7133,10 +7234,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(761); + setState(781); columnExprList(); } - setState(764); + setState(784); match(HogQLParser::RPAREN); break; } @@ -7144,14 +7245,14 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(767); + setState(787); match(HogQLParser::LPAREN); - setState(769); + setState(789); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 85, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 91, _ctx)) { case 1: { - setState(768); + setState(788); match(HogQLParser::DISTINCT); break; } @@ -7159,7 +7260,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(772); + setState(792); _errHandler->sync(this); _la = _input->LA(1); @@ -7167,10 +7268,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(771); + setState(791); columnArgList(); } - setState(774); + setState(794); match(HogQLParser::RPAREN); break; } @@ -7179,7 +7280,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(776); + setState(796); hogqlxTagElement(); break; } @@ -7188,7 +7289,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(777); + setState(797); templateString(); break; } @@ -7197,7 +7298,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(778); + setState(798); literal(); break; } @@ -7206,9 +7307,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(779); + setState(799); match(HogQLParser::DASH); - setState(780); + setState(800); columnExpr(19); break; } @@ -7217,9 +7318,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(781); + setState(801); match(HogQLParser::NOT); - setState(782); + setState(802); columnExpr(13); break; } @@ -7228,19 +7329,19 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(786); + setState(806); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 201863462911) != 0)) { - setState(783); + setState(803); tableIdentifier(); - setState(784); + setState(804); match(HogQLParser::DOT); } - setState(788); + setState(808); match(HogQLParser::ASTERISK); break; } @@ -7249,11 +7350,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(789); + setState(809); match(HogQLParser::LPAREN); - setState(790); + setState(810); selectUnionStmt(); - setState(791); + setState(811); match(HogQLParser::RPAREN); break; } @@ -7262,11 +7363,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(793); + setState(813); match(HogQLParser::LPAREN); - setState(794); + setState(814); columnExpr(0); - setState(795); + setState(815); match(HogQLParser::RPAREN); break; } @@ -7275,11 +7376,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(797); + setState(817); match(HogQLParser::LPAREN); - setState(798); + setState(818); columnExprList(); - setState(799); + setState(819); match(HogQLParser::RPAREN); break; } @@ -7288,9 +7389,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(801); + setState(821); match(HogQLParser::LBRACKET); - setState(803); + setState(823); _errHandler->sync(this); _la = _input->LA(1); @@ -7298,10 +7399,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(802); + setState(822); columnExprList(); } - setState(805); + setState(825); match(HogQLParser::RBRACKET); break; } @@ -7310,9 +7411,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(806); + setState(826); match(HogQLParser::LBRACE); - setState(808); + setState(828); _errHandler->sync(this); _la = _input->LA(1); @@ -7320,10 +7421,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(807); + setState(827); kvPairList(); } - setState(810); + setState(830); match(HogQLParser::RBRACE); break; } @@ -7332,7 +7433,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(811); + setState(831); columnIdentifier(); break; } @@ -7341,42 +7442,42 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { break; } _ctx->stop = _input->LT(-1); - setState(907); + setState(927); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 101, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 107, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { if (!_parseListeners.empty()) triggerExitRuleEvent(); previousContext = _localctx; - setState(905); + setState(925); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 100, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 106, _ctx)) { case 1: { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; newContext->left = previousContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(814); + setState(834); if (!(precpred(_ctx, 18))) throw FailedPredicateException(this, "precpred(_ctx, 18)"); - setState(818); + setState(838); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::ASTERISK: { - setState(815); + setState(835); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::ASTERISK); break; } case HogQLParser::SLASH: { - setState(816); + setState(836); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::SLASH); break; } case HogQLParser::PERCENT: { - setState(817); + setState(837); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::PERCENT); break; } @@ -7384,7 +7485,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: throw NoViableAltException(this); } - setState(820); + setState(840); antlrcpp::downCast(_localctx)->right = columnExpr(19); break; } @@ -7394,26 +7495,26 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = newContext; newContext->left = previousContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(821); + setState(841); if (!(precpred(_ctx, 17))) throw FailedPredicateException(this, "precpred(_ctx, 17)"); - setState(825); + setState(845); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::PLUS: { - setState(822); + setState(842); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::PLUS); break; } case HogQLParser::DASH: { - setState(823); + setState(843); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::DASH); break; } case HogQLParser::CONCAT: { - setState(824); + setState(844); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::CONCAT); break; } @@ -7421,7 +7522,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: throw NoViableAltException(this); } - setState(827); + setState(847); antlrcpp::downCast(_localctx)->right = columnExpr(18); break; } @@ -7431,71 +7532,71 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = newContext; newContext->left = previousContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(828); + setState(848); if (!(precpred(_ctx, 16))) throw FailedPredicateException(this, "precpred(_ctx, 16)"); - setState(853); + setState(873); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 96, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 102, _ctx)) { case 1: { - setState(829); + setState(849); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::EQ_DOUBLE); break; } case 2: { - setState(830); + setState(850); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::EQ_SINGLE); break; } case 3: { - setState(831); + setState(851); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT_EQ); break; } case 4: { - setState(832); + setState(852); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::LT_EQ); break; } case 5: { - setState(833); + setState(853); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::LT); break; } case 6: { - setState(834); + setState(854); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::GT_EQ); break; } case 7: { - setState(835); + setState(855); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::GT); break; } case 8: { - setState(837); + setState(857); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(836); + setState(856); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT); } - setState(839); + setState(859); match(HogQLParser::IN); - setState(841); + setState(861); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 94, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 100, _ctx)) { case 1: { - setState(840); + setState(860); match(HogQLParser::COHORT); break; } @@ -7507,15 +7608,15 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { } case 9: { - setState(844); + setState(864); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(843); + setState(863); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT); } - setState(846); + setState(866); _la = _input->LA(1); if (!(_la == HogQLParser::ILIKE @@ -7530,37 +7631,37 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { } case 10: { - setState(847); + setState(867); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::REGEX_SINGLE); break; } case 11: { - setState(848); + setState(868); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::REGEX_DOUBLE); break; } case 12: { - setState(849); + setState(869); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT_REGEX); break; } case 13: { - setState(850); + setState(870); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::IREGEX_SINGLE); break; } case 14: { - setState(851); + setState(871); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::IREGEX_DOUBLE); break; } case 15: { - setState(852); + setState(872); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT_IREGEX); break; } @@ -7568,7 +7669,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(855); + setState(875); antlrcpp::downCast(_localctx)->right = columnExpr(17); break; } @@ -7577,12 +7678,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(856); + setState(876); if (!(precpred(_ctx, 14))) throw FailedPredicateException(this, "precpred(_ctx, 14)"); - setState(857); + setState(877); match(HogQLParser::NULLISH); - setState(858); + setState(878); columnExpr(15); break; } @@ -7591,12 +7692,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(859); + setState(879); if (!(precpred(_ctx, 12))) throw FailedPredicateException(this, "precpred(_ctx, 12)"); - setState(860); + setState(880); match(HogQLParser::AND); - setState(861); + setState(881); columnExpr(13); break; } @@ -7605,12 +7706,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(862); + setState(882); if (!(precpred(_ctx, 11))) throw FailedPredicateException(this, "precpred(_ctx, 11)"); - setState(863); + setState(883); match(HogQLParser::OR); - setState(864); + setState(884); columnExpr(12); break; } @@ -7619,24 +7720,24 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(865); + setState(885); if (!(precpred(_ctx, 10))) throw FailedPredicateException(this, "precpred(_ctx, 10)"); - setState(867); + setState(887); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(866); + setState(886); match(HogQLParser::NOT); } - setState(869); + setState(889); match(HogQLParser::BETWEEN); - setState(870); + setState(890); columnExpr(0); - setState(871); + setState(891); match(HogQLParser::AND); - setState(872); + setState(892); columnExpr(11); break; } @@ -7645,16 +7746,16 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(874); + setState(894); if (!(precpred(_ctx, 9))) throw FailedPredicateException(this, "precpred(_ctx, 9)"); - setState(875); + setState(895); match(HogQLParser::QUERY); - setState(876); + setState(896); columnExpr(0); - setState(877); + setState(897); match(HogQLParser::COLON); - setState(878); + setState(898); columnExpr(9); break; } @@ -7663,14 +7764,14 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(880); + setState(900); if (!(precpred(_ctx, 22))) throw FailedPredicateException(this, "precpred(_ctx, 22)"); - setState(881); + setState(901); match(HogQLParser::LBRACKET); - setState(882); + setState(902); columnExpr(0); - setState(883); + setState(903); match(HogQLParser::RBRACKET); break; } @@ -7679,12 +7780,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(885); + setState(905); if (!(precpred(_ctx, 21))) throw FailedPredicateException(this, "precpred(_ctx, 21)"); - setState(886); + setState(906); match(HogQLParser::DOT); - setState(887); + setState(907); match(HogQLParser::DECIMAL_LITERAL); break; } @@ -7693,12 +7794,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(888); + setState(908); if (!(precpred(_ctx, 20))) throw FailedPredicateException(this, "precpred(_ctx, 20)"); - setState(889); + setState(909); match(HogQLParser::DOT); - setState(890); + setState(910); identifier(); break; } @@ -7707,20 +7808,20 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(891); + setState(911); if (!(precpred(_ctx, 15))) throw FailedPredicateException(this, "precpred(_ctx, 15)"); - setState(892); + setState(912); match(HogQLParser::IS); - setState(894); + setState(914); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(893); + setState(913); match(HogQLParser::NOT); } - setState(896); + setState(916); match(HogQLParser::NULL_SQL); break; } @@ -7729,30 +7830,30 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(897); + setState(917); if (!(precpred(_ctx, 8))) throw FailedPredicateException(this, "precpred(_ctx, 8)"); - setState(903); + setState(923); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 99, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 105, _ctx)) { case 1: { - setState(898); + setState(918); alias(); break; } case 2: { - setState(899); + setState(919); match(HogQLParser::AS); - setState(900); + setState(920); identifier(); break; } case 3: { - setState(901); + setState(921); match(HogQLParser::AS); - setState(902); + setState(922); match(HogQLParser::STRING_LITERAL); break; } @@ -7767,9 +7868,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { break; } } - setState(909); + setState(929); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 101, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 107, _ctx); } } catch (RecognitionException &e) { @@ -7829,17 +7930,17 @@ HogQLParser::ColumnArgListContext* HogQLParser::columnArgList() { }); try { enterOuterAlt(_localctx, 1); - setState(910); + setState(930); columnArgExpr(); - setState(915); + setState(935); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(911); + setState(931); match(HogQLParser::COMMA); - setState(912); + setState(932); columnArgExpr(); - setState(917); + setState(937); _errHandler->sync(this); _la = _input->LA(1); } @@ -7893,19 +7994,19 @@ HogQLParser::ColumnArgExprContext* HogQLParser::columnArgExpr() { exitRule(); }); try { - setState(920); + setState(940); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 103, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 109, _ctx)) { case 1: { enterOuterAlt(_localctx, 1); - setState(918); + setState(938); columnLambdaExpr(); break; } case 2: { enterOuterAlt(_localctx, 2); - setState(919); + setState(939); columnExpr(0); break; } @@ -7989,27 +8090,27 @@ HogQLParser::ColumnLambdaExprContext* HogQLParser::columnLambdaExpr() { }); try { enterOuterAlt(_localctx, 1); - setState(941); + setState(961); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::LPAREN: { - setState(922); + setState(942); match(HogQLParser::LPAREN); - setState(923); + setState(943); identifier(); - setState(928); + setState(948); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(924); + setState(944); match(HogQLParser::COMMA); - setState(925); + setState(945); identifier(); - setState(930); + setState(950); _errHandler->sync(this); _la = _input->LA(1); } - setState(931); + setState(951); match(HogQLParser::RPAREN); break; } @@ -8108,17 +8209,17 @@ HogQLParser::ColumnLambdaExprContext* HogQLParser::columnLambdaExpr() { case HogQLParser::WITH: case HogQLParser::YEAR: case HogQLParser::IDENTIFIER: { - setState(933); + setState(953); identifier(); - setState(938); + setState(958); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(934); + setState(954); match(HogQLParser::COMMA); - setState(935); + setState(955); identifier(); - setState(940); + setState(960); _errHandler->sync(this); _la = _input->LA(1); } @@ -8128,9 +8229,9 @@ HogQLParser::ColumnLambdaExprContext* HogQLParser::columnLambdaExpr() { default: throw NoViableAltException(this); } - setState(943); + setState(963); match(HogQLParser::ARROW); - setState(944); + setState(964); columnExpr(0); } @@ -8257,31 +8358,31 @@ HogQLParser::HogqlxTagElementContext* HogQLParser::hogqlxTagElement() { exitRule(); }); try { - setState(974); + setState(994); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 110, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 116, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 1); - setState(946); + setState(966); match(HogQLParser::LT); - setState(947); + setState(967); identifier(); - setState(951); + setState(971); _errHandler->sync(this); _la = _input->LA(1); while ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 201863462911) != 0)) { - setState(948); + setState(968); hogqlxTagAttribute(); - setState(953); + setState(973); _errHandler->sync(this); _la = _input->LA(1); } - setState(954); + setState(974); match(HogQLParser::SLASH); - setState(955); + setState(975); match(HogQLParser::GT); break; } @@ -8289,30 +8390,30 @@ HogQLParser::HogqlxTagElementContext* HogQLParser::hogqlxTagElement() { case 2: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 2); - setState(957); + setState(977); match(HogQLParser::LT); - setState(958); + setState(978); identifier(); - setState(962); + setState(982); _errHandler->sync(this); _la = _input->LA(1); while ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 201863462911) != 0)) { - setState(959); + setState(979); hogqlxTagAttribute(); - setState(964); + setState(984); _errHandler->sync(this); _la = _input->LA(1); } - setState(965); + setState(985); match(HogQLParser::GT); - setState(967); + setState(987); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 109, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 115, _ctx)) { case 1: { - setState(966); + setState(986); hogqlxTagElement(); break; } @@ -8320,13 +8421,13 @@ HogQLParser::HogqlxTagElementContext* HogQLParser::hogqlxTagElement() { default: break; } - setState(969); + setState(989); match(HogQLParser::LT); - setState(970); + setState(990); match(HogQLParser::SLASH); - setState(971); + setState(991); identifier(); - setState(972); + setState(992); match(HogQLParser::GT); break; } @@ -8400,38 +8501,38 @@ HogQLParser::HogqlxTagAttributeContext* HogQLParser::hogqlxTagAttribute() { exitRule(); }); try { - setState(987); + setState(1007); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 111, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 117, _ctx)) { case 1: { enterOuterAlt(_localctx, 1); - setState(976); + setState(996); identifier(); - setState(977); + setState(997); match(HogQLParser::EQ_SINGLE); - setState(978); + setState(998); string(); break; } case 2: { enterOuterAlt(_localctx, 2); - setState(980); + setState(1000); identifier(); - setState(981); + setState(1001); match(HogQLParser::EQ_SINGLE); - setState(982); + setState(1002); match(HogQLParser::LBRACE); - setState(983); + setState(1003); columnExpr(0); - setState(984); + setState(1004); match(HogQLParser::RBRACE); break; } case 3: { enterOuterAlt(_localctx, 3); - setState(986); + setState(1006); identifier(); break; } @@ -8499,17 +8600,17 @@ HogQLParser::WithExprListContext* HogQLParser::withExprList() { }); try { enterOuterAlt(_localctx, 1); - setState(989); + setState(1009); withExpr(); - setState(994); + setState(1014); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(990); + setState(1010); match(HogQLParser::COMMA); - setState(991); + setState(1011); withExpr(); - setState(996); + setState(1016); _errHandler->sync(this); _la = _input->LA(1); } @@ -8605,21 +8706,21 @@ HogQLParser::WithExprContext* HogQLParser::withExpr() { exitRule(); }); try { - setState(1007); + setState(1027); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 113, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 119, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 1); - setState(997); + setState(1017); identifier(); - setState(998); + setState(1018); match(HogQLParser::AS); - setState(999); + setState(1019); match(HogQLParser::LPAREN); - setState(1000); + setState(1020); selectUnionStmt(); - setState(1001); + setState(1021); match(HogQLParser::RPAREN); break; } @@ -8627,11 +8728,11 @@ HogQLParser::WithExprContext* HogQLParser::withExpr() { case 2: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 2); - setState(1003); + setState(1023); columnExpr(0); - setState(1004); + setState(1024); match(HogQLParser::AS); - setState(1005); + setState(1025); identifier(); break; } @@ -8697,12 +8798,12 @@ HogQLParser::ColumnIdentifierContext* HogQLParser::columnIdentifier() { exitRule(); }); try { - setState(1016); + setState(1036); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::LBRACE: { enterOuterAlt(_localctx, 1); - setState(1009); + setState(1029); placeholder(); break; } @@ -8802,14 +8903,14 @@ HogQLParser::ColumnIdentifierContext* HogQLParser::columnIdentifier() { case HogQLParser::YEAR: case HogQLParser::IDENTIFIER: { enterOuterAlt(_localctx, 2); - setState(1013); + setState(1033); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 114, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 120, _ctx)) { case 1: { - setState(1010); + setState(1030); tableIdentifier(); - setState(1011); + setState(1031); match(HogQLParser::DOT); break; } @@ -8817,7 +8918,7 @@ HogQLParser::ColumnIdentifierContext* HogQLParser::columnIdentifier() { default: break; } - setState(1015); + setState(1035); nestedIdentifier(); break; } @@ -8885,21 +8986,21 @@ HogQLParser::NestedIdentifierContext* HogQLParser::nestedIdentifier() { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(1018); + setState(1038); identifier(); - setState(1023); + setState(1043); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 116, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 122, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { - setState(1019); + setState(1039); match(HogQLParser::DOT); - setState(1020); + setState(1040); identifier(); } - setState(1025); + setState(1045); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 116, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 122, _ctx); } } @@ -9063,15 +9164,15 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(1035); + setState(1055); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 117, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 123, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1027); + setState(1047); tableIdentifier(); break; } @@ -9080,7 +9181,7 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1028); + setState(1048); tableFunctionExpr(); break; } @@ -9089,11 +9190,11 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1029); + setState(1049); match(HogQLParser::LPAREN); - setState(1030); + setState(1050); selectUnionStmt(); - setState(1031); + setState(1051); match(HogQLParser::RPAREN); break; } @@ -9102,7 +9203,7 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1033); + setState(1053); hogqlxTagElement(); break; } @@ -9111,7 +9212,7 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1034); + setState(1054); placeholder(); break; } @@ -9120,9 +9221,9 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { break; } _ctx->stop = _input->LT(-1); - setState(1045); + setState(1065); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 119, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 125, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { if (!_parseListeners.empty()) @@ -9131,10 +9232,10 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleTableExpr); - setState(1037); + setState(1057); if (!(precpred(_ctx, 3))) throw FailedPredicateException(this, "precpred(_ctx, 3)"); - setState(1041); + setState(1061); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::DATE: @@ -9142,15 +9243,15 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { case HogQLParser::ID: case HogQLParser::KEY: case HogQLParser::IDENTIFIER: { - setState(1038); + setState(1058); alias(); break; } case HogQLParser::AS: { - setState(1039); + setState(1059); match(HogQLParser::AS); - setState(1040); + setState(1060); identifier(); break; } @@ -9159,9 +9260,9 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { throw NoViableAltException(this); } } - setState(1047); + setState(1067); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 119, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 125, _ctx); } } catch (RecognitionException &e) { @@ -9221,11 +9322,11 @@ HogQLParser::TableFunctionExprContext* HogQLParser::tableFunctionExpr() { }); try { enterOuterAlt(_localctx, 1); - setState(1048); + setState(1068); identifier(); - setState(1049); + setState(1069); match(HogQLParser::LPAREN); - setState(1051); + setState(1071); _errHandler->sync(this); _la = _input->LA(1); @@ -9233,10 +9334,10 @@ HogQLParser::TableFunctionExprContext* HogQLParser::tableFunctionExpr() { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(1050); + setState(1070); tableArgList(); } - setState(1053); + setState(1073); match(HogQLParser::RPAREN); } @@ -9293,14 +9394,14 @@ HogQLParser::TableIdentifierContext* HogQLParser::tableIdentifier() { }); try { enterOuterAlt(_localctx, 1); - setState(1058); + setState(1078); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 121, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 127, _ctx)) { case 1: { - setState(1055); + setState(1075); databaseIdentifier(); - setState(1056); + setState(1076); match(HogQLParser::DOT); break; } @@ -9308,7 +9409,7 @@ HogQLParser::TableIdentifierContext* HogQLParser::tableIdentifier() { default: break; } - setState(1060); + setState(1080); identifier(); } @@ -9370,17 +9471,17 @@ HogQLParser::TableArgListContext* HogQLParser::tableArgList() { }); try { enterOuterAlt(_localctx, 1); - setState(1062); + setState(1082); columnExpr(0); - setState(1067); + setState(1087); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(1063); + setState(1083); match(HogQLParser::COMMA); - setState(1064); + setState(1084); columnExpr(0); - setState(1069); + setState(1089); _errHandler->sync(this); _la = _input->LA(1); } @@ -9431,7 +9532,7 @@ HogQLParser::DatabaseIdentifierContext* HogQLParser::databaseIdentifier() { }); try { enterOuterAlt(_localctx, 1); - setState(1070); + setState(1090); identifier(); } @@ -9496,21 +9597,21 @@ HogQLParser::FloatingLiteralContext* HogQLParser::floatingLiteral() { exitRule(); }); try { - setState(1080); + setState(1100); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::FLOATING_LITERAL: { enterOuterAlt(_localctx, 1); - setState(1072); + setState(1092); match(HogQLParser::FLOATING_LITERAL); break; } case HogQLParser::DOT: { enterOuterAlt(_localctx, 2); - setState(1073); + setState(1093); match(HogQLParser::DOT); - setState(1074); + setState(1094); _la = _input->LA(1); if (!(_la == HogQLParser::OCTAL_LITERAL @@ -9526,16 +9627,16 @@ HogQLParser::FloatingLiteralContext* HogQLParser::floatingLiteral() { case HogQLParser::DECIMAL_LITERAL: { enterOuterAlt(_localctx, 3); - setState(1075); + setState(1095); match(HogQLParser::DECIMAL_LITERAL); - setState(1076); + setState(1096); match(HogQLParser::DOT); - setState(1078); + setState(1098); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 123, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 129, _ctx)) { case 1: { - setState(1077); + setState(1097); _la = _input->LA(1); if (!(_la == HogQLParser::OCTAL_LITERAL @@ -9634,14 +9735,14 @@ HogQLParser::NumberLiteralContext* HogQLParser::numberLiteral() { }); try { enterOuterAlt(_localctx, 1); - setState(1083); + setState(1103); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::DASH || _la == HogQLParser::PLUS) { - setState(1082); + setState(1102); _la = _input->LA(1); if (!(_la == HogQLParser::DASH @@ -9653,41 +9754,41 @@ HogQLParser::NumberLiteralContext* HogQLParser::numberLiteral() { consume(); } } - setState(1091); + setState(1111); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 126, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 132, _ctx)) { case 1: { - setState(1085); + setState(1105); floatingLiteral(); break; } case 2: { - setState(1086); + setState(1106); match(HogQLParser::OCTAL_LITERAL); break; } case 3: { - setState(1087); + setState(1107); match(HogQLParser::DECIMAL_LITERAL); break; } case 4: { - setState(1088); + setState(1108); match(HogQLParser::HEXADECIMAL_LITERAL); break; } case 5: { - setState(1089); + setState(1109); match(HogQLParser::INF); break; } case 6: { - setState(1090); + setState(1110); match(HogQLParser::NAN_SQL); break; } @@ -9749,7 +9850,7 @@ HogQLParser::LiteralContext* HogQLParser::literal() { exitRule(); }); try { - setState(1096); + setState(1116); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::INF: @@ -9762,21 +9863,21 @@ HogQLParser::LiteralContext* HogQLParser::literal() { case HogQLParser::DOT: case HogQLParser::PLUS: { enterOuterAlt(_localctx, 1); - setState(1093); + setState(1113); numberLiteral(); break; } case HogQLParser::STRING_LITERAL: { enterOuterAlt(_localctx, 2); - setState(1094); + setState(1114); match(HogQLParser::STRING_LITERAL); break; } case HogQLParser::NULL_SQL: { enterOuterAlt(_localctx, 3); - setState(1095); + setState(1115); match(HogQLParser::NULL_SQL); break; } @@ -9860,7 +9961,7 @@ HogQLParser::IntervalContext* HogQLParser::interval() { }); try { enterOuterAlt(_localctx, 1); - setState(1098); + setState(1118); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 27021666484748288) != 0) || ((((_la - 68) & ~ 0x3fULL) == 0) && @@ -10255,7 +10356,7 @@ HogQLParser::KeywordContext* HogQLParser::keyword() { }); try { enterOuterAlt(_localctx, 1); - setState(1100); + setState(1120); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -208293751046537218) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && @@ -10326,7 +10427,7 @@ HogQLParser::KeywordForAliasContext* HogQLParser::keywordForAlias() { }); try { enterOuterAlt(_localctx, 1); - setState(1102); + setState(1122); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 70506452090880) != 0))) { @@ -10386,12 +10487,12 @@ HogQLParser::AliasContext* HogQLParser::alias() { exitRule(); }); try { - setState(1106); + setState(1126); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::IDENTIFIER: { enterOuterAlt(_localctx, 1); - setState(1104); + setState(1124); match(HogQLParser::IDENTIFIER); break; } @@ -10401,7 +10502,7 @@ HogQLParser::AliasContext* HogQLParser::alias() { case HogQLParser::ID: case HogQLParser::KEY: { enterOuterAlt(_localctx, 2); - setState(1105); + setState(1125); keywordForAlias(); break; } @@ -10463,12 +10564,12 @@ HogQLParser::IdentifierContext* HogQLParser::identifier() { exitRule(); }); try { - setState(1111); + setState(1131); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::IDENTIFIER: { enterOuterAlt(_localctx, 1); - setState(1108); + setState(1128); match(HogQLParser::IDENTIFIER); break; } @@ -10482,7 +10583,7 @@ HogQLParser::IdentifierContext* HogQLParser::identifier() { case HogQLParser::WEEK: case HogQLParser::YEAR: { enterOuterAlt(_localctx, 2); - setState(1109); + setState(1129); interval(); break; } @@ -10573,7 +10674,7 @@ HogQLParser::IdentifierContext* HogQLParser::identifier() { case HogQLParser::WINDOW: case HogQLParser::WITH: { enterOuterAlt(_localctx, 3); - setState(1110); + setState(1130); keyword(); break; } @@ -10636,11 +10737,11 @@ HogQLParser::EnumValueContext* HogQLParser::enumValue() { }); try { enterOuterAlt(_localctx, 1); - setState(1113); + setState(1133); string(); - setState(1114); + setState(1134); match(HogQLParser::EQ_SINGLE); - setState(1115); + setState(1135); numberLiteral(); } @@ -10697,11 +10798,11 @@ HogQLParser::PlaceholderContext* HogQLParser::placeholder() { }); try { enterOuterAlt(_localctx, 1); - setState(1117); + setState(1137); match(HogQLParser::LBRACE); - setState(1118); + setState(1138); identifier(); - setState(1119); + setState(1139); match(HogQLParser::RBRACE); } @@ -10753,19 +10854,19 @@ HogQLParser::StringContext* HogQLParser::string() { exitRule(); }); try { - setState(1123); + setState(1143); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::STRING_LITERAL: { enterOuterAlt(_localctx, 1); - setState(1121); + setState(1141); match(HogQLParser::STRING_LITERAL); break; } case HogQLParser::QUOTE_SINGLE_TEMPLATE: { enterOuterAlt(_localctx, 2); - setState(1122); + setState(1142); templateString(); break; } @@ -10833,21 +10934,21 @@ HogQLParser::TemplateStringContext* HogQLParser::templateString() { }); try { enterOuterAlt(_localctx, 1); - setState(1125); + setState(1145); match(HogQLParser::QUOTE_SINGLE_TEMPLATE); - setState(1129); + setState(1149); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::STRING_TEXT || _la == HogQLParser::STRING_ESCAPE_TRIGGER) { - setState(1126); + setState(1146); stringContents(); - setState(1131); + setState(1151); _errHandler->sync(this); _la = _input->LA(1); } - setState(1132); + setState(1152); match(HogQLParser::QUOTE_SINGLE); } @@ -10907,23 +11008,23 @@ HogQLParser::StringContentsContext* HogQLParser::stringContents() { exitRule(); }); try { - setState(1139); + setState(1159); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::STRING_ESCAPE_TRIGGER: { enterOuterAlt(_localctx, 1); - setState(1134); + setState(1154); match(HogQLParser::STRING_ESCAPE_TRIGGER); - setState(1135); + setState(1155); columnExpr(0); - setState(1136); + setState(1156); match(HogQLParser::RBRACE); break; } case HogQLParser::STRING_TEXT: { enterOuterAlt(_localctx, 2); - setState(1138); + setState(1158); match(HogQLParser::STRING_TEXT); break; } @@ -10991,21 +11092,21 @@ HogQLParser::FullTemplateStringContext* HogQLParser::fullTemplateString() { }); try { enterOuterAlt(_localctx, 1); - setState(1141); + setState(1161); match(HogQLParser::QUOTE_SINGLE_TEMPLATE_FULL); - setState(1145); + setState(1165); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::FULL_STRING_TEXT || _la == HogQLParser::FULL_STRING_ESCAPE_TRIGGER) { - setState(1142); + setState(1162); stringContentsFull(); - setState(1147); + setState(1167); _errHandler->sync(this); _la = _input->LA(1); } - setState(1148); + setState(1168); match(HogQLParser::EOF); } @@ -11065,23 +11166,23 @@ HogQLParser::StringContentsFullContext* HogQLParser::stringContentsFull() { exitRule(); }); try { - setState(1155); + setState(1175); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::FULL_STRING_ESCAPE_TRIGGER: { enterOuterAlt(_localctx, 1); - setState(1150); + setState(1170); match(HogQLParser::FULL_STRING_ESCAPE_TRIGGER); - setState(1151); + setState(1171); columnExpr(0); - setState(1152); + setState(1172); match(HogQLParser::RBRACE); break; } case HogQLParser::FULL_STRING_TEXT: { enterOuterAlt(_localctx, 2); - setState(1154); + setState(1174); match(HogQLParser::FULL_STRING_TEXT); break; } diff --git a/hogql_parser/HogQLParser.h b/hogql_parser/HogQLParser.h index 174d2572e5736..94f46d07b4562 100644 --- a/hogql_parser/HogQLParser.h +++ b/hogql_parser/HogQLParser.h @@ -1439,9 +1439,13 @@ class HogQLParser : public antlr4::Parser { std::vector identifier(); IdentifierContext* identifier(size_t i); antlr4::tree::TerminalNode *OVER(); - antlr4::tree::TerminalNode *LPAREN(); - antlr4::tree::TerminalNode *RPAREN(); + std::vector LPAREN(); + antlr4::tree::TerminalNode* LPAREN(size_t i); + std::vector RPAREN(); + antlr4::tree::TerminalNode* RPAREN(size_t i); ColumnExprListContext *columnExprList(); + antlr4::tree::TerminalNode *DISTINCT(); + ColumnArgListContext *columnArgList(); virtual std::any accept(antlr4::tree::ParseTreeVisitor *visitor) override; }; @@ -1635,6 +1639,8 @@ class HogQLParser : public antlr4::Parser { std::vector RPAREN(); antlr4::tree::TerminalNode* RPAREN(size_t i); ColumnExprListContext *columnExprList(); + antlr4::tree::TerminalNode *DISTINCT(); + ColumnArgListContext *columnArgList(); virtual std::any accept(antlr4::tree::ParseTreeVisitor *visitor) override; }; diff --git a/hogql_parser/HogQLParser.interp b/hogql_parser/HogQLParser.interp index a2f030a7eb8fa..086eca220c32f 100644 --- a/hogql_parser/HogQLParser.interp +++ b/hogql_parser/HogQLParser.interp @@ -399,4 +399,4 @@ stringContentsFull atn: -[4, 1, 154, 1158, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 199, 8, 5, 10, 5, 12, 5, 202, 9, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 213, 8, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 225, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 241, 8, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 250, 8, 13, 10, 13, 12, 13, 253, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 264, 8, 15, 10, 15, 12, 15, 267, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 272, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 280, 8, 17, 10, 17, 12, 17, 283, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 291, 8, 18, 1, 19, 3, 19, 294, 8, 19, 1, 19, 1, 19, 3, 19, 298, 8, 19, 1, 19, 3, 19, 301, 8, 19, 1, 19, 1, 19, 3, 19, 305, 8, 19, 1, 19, 3, 19, 308, 8, 19, 1, 19, 3, 19, 311, 8, 19, 1, 19, 3, 19, 314, 8, 19, 1, 19, 3, 19, 317, 8, 19, 1, 19, 1, 19, 3, 19, 321, 8, 19, 1, 19, 1, 19, 3, 19, 325, 8, 19, 1, 19, 3, 19, 328, 8, 19, 1, 19, 3, 19, 331, 8, 19, 1, 19, 3, 19, 334, 8, 19, 1, 19, 1, 19, 3, 19, 338, 8, 19, 1, 19, 3, 19, 341, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 350, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 356, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 375, 8, 24, 10, 24, 12, 24, 378, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 394, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 411, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 417, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 423, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 434, 8, 31, 3, 31, 436, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 447, 8, 34, 1, 34, 3, 34, 450, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 456, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 464, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 470, 8, 34, 10, 34, 12, 34, 473, 9, 34, 1, 35, 3, 35, 476, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 481, 8, 35, 1, 35, 3, 35, 484, 8, 35, 1, 35, 3, 35, 487, 8, 35, 1, 35, 1, 35, 3, 35, 491, 8, 35, 1, 35, 1, 35, 3, 35, 495, 8, 35, 1, 35, 3, 35, 498, 8, 35, 3, 35, 500, 8, 35, 1, 35, 3, 35, 503, 8, 35, 1, 35, 1, 35, 3, 35, 507, 8, 35, 1, 35, 1, 35, 3, 35, 511, 8, 35, 1, 35, 3, 35, 514, 8, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 523, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 534, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 540, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 545, 8, 39, 10, 39, 12, 39, 548, 9, 39, 1, 40, 1, 40, 3, 40, 552, 8, 40, 1, 40, 1, 40, 3, 40, 556, 8, 40, 1, 40, 1, 40, 3, 40, 560, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 566, 8, 41, 3, 41, 568, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 573, 8, 42, 10, 42, 12, 42, 576, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 583, 8, 44, 1, 44, 3, 44, 586, 8, 44, 1, 44, 3, 44, 589, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 608, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 622, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 636, 8, 51, 10, 51, 12, 51, 639, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 648, 8, 51, 10, 51, 12, 51, 651, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 660, 8, 51, 10, 51, 12, 51, 663, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 670, 8, 51, 1, 51, 1, 51, 3, 51, 674, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 679, 8, 52, 10, 52, 12, 52, 682, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 687, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 694, 8, 53, 11, 53, 12, 53, 695, 1, 53, 1, 53, 3, 53, 700, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 724, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 741, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 753, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 763, 8, 53, 1, 53, 3, 53, 766, 8, 53, 1, 53, 1, 53, 3, 53, 770, 8, 53, 1, 53, 3, 53, 773, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 787, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 804, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 809, 8, 53, 1, 53, 1, 53, 3, 53, 813, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 819, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 826, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 838, 8, 53, 1, 53, 1, 53, 3, 53, 842, 8, 53, 1, 53, 3, 53, 845, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 854, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 868, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 895, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 904, 8, 53, 5, 53, 906, 8, 53, 10, 53, 12, 53, 909, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 914, 8, 54, 10, 54, 12, 54, 917, 9, 54, 1, 55, 1, 55, 3, 55, 921, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 927, 8, 56, 10, 56, 12, 56, 930, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 937, 8, 56, 10, 56, 12, 56, 940, 9, 56, 3, 56, 942, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 950, 8, 57, 10, 57, 12, 57, 953, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 961, 8, 57, 10, 57, 12, 57, 964, 9, 57, 1, 57, 1, 57, 3, 57, 968, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 975, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 988, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 993, 8, 59, 10, 59, 12, 59, 996, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1008, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1014, 8, 61, 1, 61, 3, 61, 1017, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1022, 8, 62, 10, 62, 12, 62, 1025, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1036, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1042, 8, 63, 5, 63, 1044, 8, 63, 10, 63, 12, 63, 1047, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1052, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1059, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1066, 8, 66, 10, 66, 12, 66, 1069, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1079, 8, 68, 3, 68, 1081, 8, 68, 1, 69, 3, 69, 1084, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1092, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1097, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1107, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1112, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1124, 8, 78, 1, 79, 1, 79, 5, 79, 1128, 8, 79, 10, 79, 12, 79, 1131, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1140, 8, 80, 1, 81, 1, 81, 5, 81, 1144, 8, 81, 10, 81, 12, 81, 1147, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1156, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1288, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 189, 1, 0, 0, 0, 10, 195, 1, 0, 0, 0, 12, 212, 1, 0, 0, 0, 14, 214, 1, 0, 0, 0, 16, 217, 1, 0, 0, 0, 18, 226, 1, 0, 0, 0, 20, 232, 1, 0, 0, 0, 22, 236, 1, 0, 0, 0, 24, 245, 1, 0, 0, 0, 26, 247, 1, 0, 0, 0, 28, 256, 1, 0, 0, 0, 30, 260, 1, 0, 0, 0, 32, 271, 1, 0, 0, 0, 34, 275, 1, 0, 0, 0, 36, 290, 1, 0, 0, 0, 38, 293, 1, 0, 0, 0, 40, 342, 1, 0, 0, 0, 42, 345, 1, 0, 0, 0, 44, 351, 1, 0, 0, 0, 46, 355, 1, 0, 0, 0, 48, 361, 1, 0, 0, 0, 50, 379, 1, 0, 0, 0, 52, 382, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 395, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 402, 1, 0, 0, 0, 62, 435, 1, 0, 0, 0, 64, 437, 1, 0, 0, 0, 66, 440, 1, 0, 0, 0, 68, 455, 1, 0, 0, 0, 70, 517, 1, 0, 0, 0, 72, 522, 1, 0, 0, 0, 74, 533, 1, 0, 0, 0, 76, 535, 1, 0, 0, 0, 78, 541, 1, 0, 0, 0, 80, 549, 1, 0, 0, 0, 82, 567, 1, 0, 0, 0, 84, 569, 1, 0, 0, 0, 86, 577, 1, 0, 0, 0, 88, 582, 1, 0, 0, 0, 90, 590, 1, 0, 0, 0, 92, 594, 1, 0, 0, 0, 94, 598, 1, 0, 0, 0, 96, 607, 1, 0, 0, 0, 98, 621, 1, 0, 0, 0, 100, 623, 1, 0, 0, 0, 102, 673, 1, 0, 0, 0, 104, 675, 1, 0, 0, 0, 106, 812, 1, 0, 0, 0, 108, 910, 1, 0, 0, 0, 110, 920, 1, 0, 0, 0, 112, 941, 1, 0, 0, 0, 114, 974, 1, 0, 0, 0, 116, 987, 1, 0, 0, 0, 118, 989, 1, 0, 0, 0, 120, 1007, 1, 0, 0, 0, 122, 1016, 1, 0, 0, 0, 124, 1018, 1, 0, 0, 0, 126, 1035, 1, 0, 0, 0, 128, 1048, 1, 0, 0, 0, 130, 1058, 1, 0, 0, 0, 132, 1062, 1, 0, 0, 0, 134, 1070, 1, 0, 0, 0, 136, 1080, 1, 0, 0, 0, 138, 1083, 1, 0, 0, 0, 140, 1096, 1, 0, 0, 0, 142, 1098, 1, 0, 0, 0, 144, 1100, 1, 0, 0, 0, 146, 1102, 1, 0, 0, 0, 148, 1106, 1, 0, 0, 0, 150, 1111, 1, 0, 0, 0, 152, 1113, 1, 0, 0, 0, 154, 1117, 1, 0, 0, 0, 156, 1123, 1, 0, 0, 0, 158, 1125, 1, 0, 0, 0, 160, 1139, 1, 0, 0, 0, 162, 1141, 1, 0, 0, 0, 164, 1155, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 12, 6, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 188, 5, 145, 0, 0, 188, 7, 1, 0, 0, 0, 189, 190, 3, 4, 2, 0, 190, 191, 5, 111, 0, 0, 191, 192, 5, 118, 0, 0, 192, 193, 3, 4, 2, 0, 193, 194, 5, 145, 0, 0, 194, 9, 1, 0, 0, 0, 195, 200, 3, 150, 75, 0, 196, 197, 5, 112, 0, 0, 197, 199, 3, 150, 75, 0, 198, 196, 1, 0, 0, 0, 199, 202, 1, 0, 0, 0, 200, 198, 1, 0, 0, 0, 200, 201, 1, 0, 0, 0, 201, 11, 1, 0, 0, 0, 202, 200, 1, 0, 0, 0, 203, 213, 3, 20, 10, 0, 204, 213, 3, 24, 12, 0, 205, 213, 3, 14, 7, 0, 206, 213, 3, 16, 8, 0, 207, 213, 3, 18, 9, 0, 208, 213, 3, 22, 11, 0, 209, 213, 3, 8, 4, 0, 210, 213, 3, 20, 10, 0, 211, 213, 3, 26, 13, 0, 212, 203, 1, 0, 0, 0, 212, 204, 1, 0, 0, 0, 212, 205, 1, 0, 0, 0, 212, 206, 1, 0, 0, 0, 212, 207, 1, 0, 0, 0, 212, 208, 1, 0, 0, 0, 212, 209, 1, 0, 0, 0, 212, 210, 1, 0, 0, 0, 212, 211, 1, 0, 0, 0, 213, 13, 1, 0, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 145, 0, 0, 216, 15, 1, 0, 0, 0, 217, 218, 5, 38, 0, 0, 218, 219, 5, 126, 0, 0, 219, 220, 3, 4, 2, 0, 220, 221, 5, 144, 0, 0, 221, 224, 3, 12, 6, 0, 222, 223, 5, 24, 0, 0, 223, 225, 3, 12, 6, 0, 224, 222, 1, 0, 0, 0, 224, 225, 1, 0, 0, 0, 225, 17, 1, 0, 0, 0, 226, 227, 5, 96, 0, 0, 227, 228, 5, 126, 0, 0, 228, 229, 3, 4, 2, 0, 229, 230, 5, 144, 0, 0, 230, 231, 3, 12, 6, 0, 231, 19, 1, 0, 0, 0, 232, 233, 5, 70, 0, 0, 233, 234, 3, 4, 2, 0, 234, 235, 5, 145, 0, 0, 235, 21, 1, 0, 0, 0, 236, 237, 5, 29, 0, 0, 237, 238, 3, 150, 75, 0, 238, 240, 5, 126, 0, 0, 239, 241, 3, 10, 5, 0, 240, 239, 1, 0, 0, 0, 240, 241, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 5, 144, 0, 0, 243, 244, 3, 26, 13, 0, 244, 23, 1, 0, 0, 0, 245, 246, 5, 145, 0, 0, 246, 25, 1, 0, 0, 0, 247, 251, 5, 124, 0, 0, 248, 250, 3, 2, 1, 0, 249, 248, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 254, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 255, 5, 142, 0, 0, 255, 27, 1, 0, 0, 0, 256, 257, 3, 4, 2, 0, 257, 258, 5, 111, 0, 0, 258, 259, 3, 4, 2, 0, 259, 29, 1, 0, 0, 0, 260, 265, 3, 28, 14, 0, 261, 262, 5, 112, 0, 0, 262, 264, 3, 28, 14, 0, 263, 261, 1, 0, 0, 0, 264, 267, 1, 0, 0, 0, 265, 263, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 31, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 272, 3, 34, 17, 0, 269, 272, 3, 38, 19, 0, 270, 272, 3, 114, 57, 0, 271, 268, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 271, 270, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 274, 5, 0, 0, 1, 274, 33, 1, 0, 0, 0, 275, 281, 3, 36, 18, 0, 276, 277, 5, 91, 0, 0, 277, 278, 5, 1, 0, 0, 278, 280, 3, 36, 18, 0, 279, 276, 1, 0, 0, 0, 280, 283, 1, 0, 0, 0, 281, 279, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 35, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 284, 291, 3, 38, 19, 0, 285, 286, 5, 126, 0, 0, 286, 287, 3, 34, 17, 0, 287, 288, 5, 144, 0, 0, 288, 291, 1, 0, 0, 0, 289, 291, 3, 154, 77, 0, 290, 284, 1, 0, 0, 0, 290, 285, 1, 0, 0, 0, 290, 289, 1, 0, 0, 0, 291, 37, 1, 0, 0, 0, 292, 294, 3, 40, 20, 0, 293, 292, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 297, 5, 77, 0, 0, 296, 298, 5, 23, 0, 0, 297, 296, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 300, 1, 0, 0, 0, 299, 301, 3, 42, 21, 0, 300, 299, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 304, 3, 104, 52, 0, 303, 305, 3, 44, 22, 0, 304, 303, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 307, 1, 0, 0, 0, 306, 308, 3, 46, 23, 0, 307, 306, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308, 310, 1, 0, 0, 0, 309, 311, 3, 50, 25, 0, 310, 309, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 313, 1, 0, 0, 0, 312, 314, 3, 52, 26, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 316, 1, 0, 0, 0, 315, 317, 3, 54, 27, 0, 316, 315, 1, 0, 0, 0, 316, 317, 1, 0, 0, 0, 317, 320, 1, 0, 0, 0, 318, 319, 5, 98, 0, 0, 319, 321, 7, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 324, 1, 0, 0, 0, 322, 323, 5, 98, 0, 0, 323, 325, 5, 86, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 327, 1, 0, 0, 0, 326, 328, 3, 56, 28, 0, 327, 326, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328, 330, 1, 0, 0, 0, 329, 331, 3, 48, 24, 0, 330, 329, 1, 0, 0, 0, 330, 331, 1, 0, 0, 0, 331, 333, 1, 0, 0, 0, 332, 334, 3, 58, 29, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 337, 1, 0, 0, 0, 335, 338, 3, 62, 31, 0, 336, 338, 3, 64, 32, 0, 337, 335, 1, 0, 0, 0, 337, 336, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 340, 1, 0, 0, 0, 339, 341, 3, 66, 33, 0, 340, 339, 1, 0, 0, 0, 340, 341, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 343, 5, 98, 0, 0, 343, 344, 3, 118, 59, 0, 344, 41, 1, 0, 0, 0, 345, 346, 5, 85, 0, 0, 346, 349, 5, 104, 0, 0, 347, 348, 5, 98, 0, 0, 348, 350, 5, 82, 0, 0, 349, 347, 1, 0, 0, 0, 349, 350, 1, 0, 0, 0, 350, 43, 1, 0, 0, 0, 351, 352, 5, 32, 0, 0, 352, 353, 3, 68, 34, 0, 353, 45, 1, 0, 0, 0, 354, 356, 7, 1, 0, 0, 355, 354, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 357, 1, 0, 0, 0, 357, 358, 5, 5, 0, 0, 358, 359, 5, 45, 0, 0, 359, 360, 3, 104, 52, 0, 360, 47, 1, 0, 0, 0, 361, 362, 5, 97, 0, 0, 362, 363, 3, 150, 75, 0, 363, 364, 5, 6, 0, 0, 364, 365, 5, 126, 0, 0, 365, 366, 3, 88, 44, 0, 366, 376, 5, 144, 0, 0, 367, 368, 5, 112, 0, 0, 368, 369, 3, 150, 75, 0, 369, 370, 5, 6, 0, 0, 370, 371, 5, 126, 0, 0, 371, 372, 3, 88, 44, 0, 372, 373, 5, 144, 0, 0, 373, 375, 1, 0, 0, 0, 374, 367, 1, 0, 0, 0, 375, 378, 1, 0, 0, 0, 376, 374, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 49, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 379, 380, 5, 67, 0, 0, 380, 381, 3, 106, 53, 0, 381, 51, 1, 0, 0, 0, 382, 383, 5, 95, 0, 0, 383, 384, 3, 106, 53, 0, 384, 53, 1, 0, 0, 0, 385, 386, 5, 34, 0, 0, 386, 393, 5, 11, 0, 0, 387, 388, 7, 0, 0, 0, 388, 389, 5, 126, 0, 0, 389, 390, 3, 104, 52, 0, 390, 391, 5, 144, 0, 0, 391, 394, 1, 0, 0, 0, 392, 394, 3, 104, 52, 0, 393, 387, 1, 0, 0, 0, 393, 392, 1, 0, 0, 0, 394, 55, 1, 0, 0, 0, 395, 396, 5, 35, 0, 0, 396, 397, 3, 106, 53, 0, 397, 57, 1, 0, 0, 0, 398, 399, 5, 62, 0, 0, 399, 400, 5, 11, 0, 0, 400, 401, 3, 78, 39, 0, 401, 59, 1, 0, 0, 0, 402, 403, 5, 62, 0, 0, 403, 404, 5, 11, 0, 0, 404, 405, 3, 104, 52, 0, 405, 61, 1, 0, 0, 0, 406, 407, 5, 52, 0, 0, 407, 410, 3, 106, 53, 0, 408, 409, 5, 112, 0, 0, 409, 411, 3, 106, 53, 0, 410, 408, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 416, 1, 0, 0, 0, 412, 413, 5, 98, 0, 0, 413, 417, 5, 82, 0, 0, 414, 415, 5, 11, 0, 0, 415, 417, 3, 104, 52, 0, 416, 412, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 416, 417, 1, 0, 0, 0, 417, 436, 1, 0, 0, 0, 418, 419, 5, 52, 0, 0, 419, 422, 3, 106, 53, 0, 420, 421, 5, 98, 0, 0, 421, 423, 5, 82, 0, 0, 422, 420, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 425, 5, 59, 0, 0, 425, 426, 3, 106, 53, 0, 426, 436, 1, 0, 0, 0, 427, 428, 5, 52, 0, 0, 428, 429, 3, 106, 53, 0, 429, 430, 5, 59, 0, 0, 430, 433, 3, 106, 53, 0, 431, 432, 5, 11, 0, 0, 432, 434, 3, 104, 52, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 436, 1, 0, 0, 0, 435, 406, 1, 0, 0, 0, 435, 418, 1, 0, 0, 0, 435, 427, 1, 0, 0, 0, 436, 63, 1, 0, 0, 0, 437, 438, 5, 59, 0, 0, 438, 439, 3, 106, 53, 0, 439, 65, 1, 0, 0, 0, 440, 441, 5, 79, 0, 0, 441, 442, 3, 84, 42, 0, 442, 67, 1, 0, 0, 0, 443, 444, 6, 34, -1, 0, 444, 446, 3, 126, 63, 0, 445, 447, 5, 27, 0, 0, 446, 445, 1, 0, 0, 0, 446, 447, 1, 0, 0, 0, 447, 449, 1, 0, 0, 0, 448, 450, 3, 76, 38, 0, 449, 448, 1, 0, 0, 0, 449, 450, 1, 0, 0, 0, 450, 456, 1, 0, 0, 0, 451, 452, 5, 126, 0, 0, 452, 453, 3, 68, 34, 0, 453, 454, 5, 144, 0, 0, 454, 456, 1, 0, 0, 0, 455, 443, 1, 0, 0, 0, 455, 451, 1, 0, 0, 0, 456, 471, 1, 0, 0, 0, 457, 458, 10, 3, 0, 0, 458, 459, 3, 72, 36, 0, 459, 460, 3, 68, 34, 4, 460, 470, 1, 0, 0, 0, 461, 463, 10, 4, 0, 0, 462, 464, 3, 70, 35, 0, 463, 462, 1, 0, 0, 0, 463, 464, 1, 0, 0, 0, 464, 465, 1, 0, 0, 0, 465, 466, 5, 45, 0, 0, 466, 467, 3, 68, 34, 0, 467, 468, 3, 74, 37, 0, 468, 470, 1, 0, 0, 0, 469, 457, 1, 0, 0, 0, 469, 461, 1, 0, 0, 0, 470, 473, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 471, 472, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 474, 476, 7, 2, 0, 0, 475, 474, 1, 0, 0, 0, 475, 476, 1, 0, 0, 0, 476, 477, 1, 0, 0, 0, 477, 484, 5, 42, 0, 0, 478, 480, 5, 42, 0, 0, 479, 481, 7, 2, 0, 0, 480, 479, 1, 0, 0, 0, 480, 481, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 484, 7, 2, 0, 0, 483, 475, 1, 0, 0, 0, 483, 478, 1, 0, 0, 0, 483, 482, 1, 0, 0, 0, 484, 518, 1, 0, 0, 0, 485, 487, 7, 3, 0, 0, 486, 485, 1, 0, 0, 0, 486, 487, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 490, 7, 4, 0, 0, 489, 491, 5, 63, 0, 0, 490, 489, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 500, 1, 0, 0, 0, 492, 494, 7, 4, 0, 0, 493, 495, 5, 63, 0, 0, 494, 493, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 497, 1, 0, 0, 0, 496, 498, 7, 3, 0, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 500, 1, 0, 0, 0, 499, 486, 1, 0, 0, 0, 499, 492, 1, 0, 0, 0, 500, 518, 1, 0, 0, 0, 501, 503, 7, 5, 0, 0, 502, 501, 1, 0, 0, 0, 502, 503, 1, 0, 0, 0, 503, 504, 1, 0, 0, 0, 504, 506, 5, 33, 0, 0, 505, 507, 5, 63, 0, 0, 506, 505, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 516, 1, 0, 0, 0, 508, 510, 5, 33, 0, 0, 509, 511, 5, 63, 0, 0, 510, 509, 1, 0, 0, 0, 510, 511, 1, 0, 0, 0, 511, 513, 1, 0, 0, 0, 512, 514, 7, 5, 0, 0, 513, 512, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 516, 1, 0, 0, 0, 515, 502, 1, 0, 0, 0, 515, 508, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 483, 1, 0, 0, 0, 517, 499, 1, 0, 0, 0, 517, 515, 1, 0, 0, 0, 518, 71, 1, 0, 0, 0, 519, 520, 5, 16, 0, 0, 520, 523, 5, 45, 0, 0, 521, 523, 5, 112, 0, 0, 522, 519, 1, 0, 0, 0, 522, 521, 1, 0, 0, 0, 523, 73, 1, 0, 0, 0, 524, 525, 5, 60, 0, 0, 525, 534, 3, 104, 52, 0, 526, 527, 5, 92, 0, 0, 527, 528, 5, 126, 0, 0, 528, 529, 3, 104, 52, 0, 529, 530, 5, 144, 0, 0, 530, 534, 1, 0, 0, 0, 531, 532, 5, 92, 0, 0, 532, 534, 3, 104, 52, 0, 533, 524, 1, 0, 0, 0, 533, 526, 1, 0, 0, 0, 533, 531, 1, 0, 0, 0, 534, 75, 1, 0, 0, 0, 535, 536, 5, 75, 0, 0, 536, 539, 3, 82, 41, 0, 537, 538, 5, 59, 0, 0, 538, 540, 3, 82, 41, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 77, 1, 0, 0, 0, 541, 546, 3, 80, 40, 0, 542, 543, 5, 112, 0, 0, 543, 545, 3, 80, 40, 0, 544, 542, 1, 0, 0, 0, 545, 548, 1, 0, 0, 0, 546, 544, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 79, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 549, 551, 3, 106, 53, 0, 550, 552, 7, 6, 0, 0, 551, 550, 1, 0, 0, 0, 551, 552, 1, 0, 0, 0, 552, 555, 1, 0, 0, 0, 553, 554, 5, 58, 0, 0, 554, 556, 7, 7, 0, 0, 555, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 559, 1, 0, 0, 0, 557, 558, 5, 15, 0, 0, 558, 560, 5, 106, 0, 0, 559, 557, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 81, 1, 0, 0, 0, 561, 568, 3, 154, 77, 0, 562, 565, 3, 138, 69, 0, 563, 564, 5, 146, 0, 0, 564, 566, 3, 138, 69, 0, 565, 563, 1, 0, 0, 0, 565, 566, 1, 0, 0, 0, 566, 568, 1, 0, 0, 0, 567, 561, 1, 0, 0, 0, 567, 562, 1, 0, 0, 0, 568, 83, 1, 0, 0, 0, 569, 574, 3, 86, 43, 0, 570, 571, 5, 112, 0, 0, 571, 573, 3, 86, 43, 0, 572, 570, 1, 0, 0, 0, 573, 576, 1, 0, 0, 0, 574, 572, 1, 0, 0, 0, 574, 575, 1, 0, 0, 0, 575, 85, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 577, 578, 3, 150, 75, 0, 578, 579, 5, 118, 0, 0, 579, 580, 3, 140, 70, 0, 580, 87, 1, 0, 0, 0, 581, 583, 3, 90, 45, 0, 582, 581, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 585, 1, 0, 0, 0, 584, 586, 3, 92, 46, 0, 585, 584, 1, 0, 0, 0, 585, 586, 1, 0, 0, 0, 586, 588, 1, 0, 0, 0, 587, 589, 3, 94, 47, 0, 588, 587, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 89, 1, 0, 0, 0, 590, 591, 5, 65, 0, 0, 591, 592, 5, 11, 0, 0, 592, 593, 3, 104, 52, 0, 593, 91, 1, 0, 0, 0, 594, 595, 5, 62, 0, 0, 595, 596, 5, 11, 0, 0, 596, 597, 3, 78, 39, 0, 597, 93, 1, 0, 0, 0, 598, 599, 7, 8, 0, 0, 599, 600, 3, 96, 48, 0, 600, 95, 1, 0, 0, 0, 601, 608, 3, 98, 49, 0, 602, 603, 5, 9, 0, 0, 603, 604, 3, 98, 49, 0, 604, 605, 5, 2, 0, 0, 605, 606, 3, 98, 49, 0, 606, 608, 1, 0, 0, 0, 607, 601, 1, 0, 0, 0, 607, 602, 1, 0, 0, 0, 608, 97, 1, 0, 0, 0, 609, 610, 5, 18, 0, 0, 610, 622, 5, 73, 0, 0, 611, 612, 5, 90, 0, 0, 612, 622, 5, 66, 0, 0, 613, 614, 5, 90, 0, 0, 614, 622, 5, 30, 0, 0, 615, 616, 3, 138, 69, 0, 616, 617, 5, 66, 0, 0, 617, 622, 1, 0, 0, 0, 618, 619, 3, 138, 69, 0, 619, 620, 5, 30, 0, 0, 620, 622, 1, 0, 0, 0, 621, 609, 1, 0, 0, 0, 621, 611, 1, 0, 0, 0, 621, 613, 1, 0, 0, 0, 621, 615, 1, 0, 0, 0, 621, 618, 1, 0, 0, 0, 622, 99, 1, 0, 0, 0, 623, 624, 3, 106, 53, 0, 624, 625, 5, 0, 0, 1, 625, 101, 1, 0, 0, 0, 626, 674, 3, 150, 75, 0, 627, 628, 3, 150, 75, 0, 628, 629, 5, 126, 0, 0, 629, 630, 3, 150, 75, 0, 630, 637, 3, 102, 51, 0, 631, 632, 5, 112, 0, 0, 632, 633, 3, 150, 75, 0, 633, 634, 3, 102, 51, 0, 634, 636, 1, 0, 0, 0, 635, 631, 1, 0, 0, 0, 636, 639, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 637, 638, 1, 0, 0, 0, 638, 640, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 640, 641, 5, 144, 0, 0, 641, 674, 1, 0, 0, 0, 642, 643, 3, 150, 75, 0, 643, 644, 5, 126, 0, 0, 644, 649, 3, 152, 76, 0, 645, 646, 5, 112, 0, 0, 646, 648, 3, 152, 76, 0, 647, 645, 1, 0, 0, 0, 648, 651, 1, 0, 0, 0, 649, 647, 1, 0, 0, 0, 649, 650, 1, 0, 0, 0, 650, 652, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 652, 653, 5, 144, 0, 0, 653, 674, 1, 0, 0, 0, 654, 655, 3, 150, 75, 0, 655, 656, 5, 126, 0, 0, 656, 661, 3, 102, 51, 0, 657, 658, 5, 112, 0, 0, 658, 660, 3, 102, 51, 0, 659, 657, 1, 0, 0, 0, 660, 663, 1, 0, 0, 0, 661, 659, 1, 0, 0, 0, 661, 662, 1, 0, 0, 0, 662, 664, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 664, 665, 5, 144, 0, 0, 665, 674, 1, 0, 0, 0, 666, 667, 3, 150, 75, 0, 667, 669, 5, 126, 0, 0, 668, 670, 3, 104, 52, 0, 669, 668, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 672, 5, 144, 0, 0, 672, 674, 1, 0, 0, 0, 673, 626, 1, 0, 0, 0, 673, 627, 1, 0, 0, 0, 673, 642, 1, 0, 0, 0, 673, 654, 1, 0, 0, 0, 673, 666, 1, 0, 0, 0, 674, 103, 1, 0, 0, 0, 675, 680, 3, 106, 53, 0, 676, 677, 5, 112, 0, 0, 677, 679, 3, 106, 53, 0, 678, 676, 1, 0, 0, 0, 679, 682, 1, 0, 0, 0, 680, 678, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 681, 105, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 683, 684, 6, 53, -1, 0, 684, 686, 5, 12, 0, 0, 685, 687, 3, 106, 53, 0, 686, 685, 1, 0, 0, 0, 686, 687, 1, 0, 0, 0, 687, 693, 1, 0, 0, 0, 688, 689, 5, 94, 0, 0, 689, 690, 3, 106, 53, 0, 690, 691, 5, 81, 0, 0, 691, 692, 3, 106, 53, 0, 692, 694, 1, 0, 0, 0, 693, 688, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 693, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 699, 1, 0, 0, 0, 697, 698, 5, 24, 0, 0, 698, 700, 3, 106, 53, 0, 699, 697, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 702, 5, 25, 0, 0, 702, 813, 1, 0, 0, 0, 703, 704, 5, 13, 0, 0, 704, 705, 5, 126, 0, 0, 705, 706, 3, 106, 53, 0, 706, 707, 5, 6, 0, 0, 707, 708, 3, 102, 51, 0, 708, 709, 5, 144, 0, 0, 709, 813, 1, 0, 0, 0, 710, 711, 5, 19, 0, 0, 711, 813, 5, 106, 0, 0, 712, 713, 5, 43, 0, 0, 713, 714, 3, 106, 53, 0, 714, 715, 3, 142, 71, 0, 715, 813, 1, 0, 0, 0, 716, 717, 5, 80, 0, 0, 717, 718, 5, 126, 0, 0, 718, 719, 3, 106, 53, 0, 719, 720, 5, 32, 0, 0, 720, 723, 3, 106, 53, 0, 721, 722, 5, 31, 0, 0, 722, 724, 3, 106, 53, 0, 723, 721, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 144, 0, 0, 726, 813, 1, 0, 0, 0, 727, 728, 5, 83, 0, 0, 728, 813, 5, 106, 0, 0, 729, 730, 5, 88, 0, 0, 730, 731, 5, 126, 0, 0, 731, 732, 7, 9, 0, 0, 732, 733, 3, 156, 78, 0, 733, 734, 5, 32, 0, 0, 734, 735, 3, 106, 53, 0, 735, 736, 5, 144, 0, 0, 736, 813, 1, 0, 0, 0, 737, 738, 3, 150, 75, 0, 738, 740, 5, 126, 0, 0, 739, 741, 3, 104, 52, 0, 740, 739, 1, 0, 0, 0, 740, 741, 1, 0, 0, 0, 741, 742, 1, 0, 0, 0, 742, 743, 5, 144, 0, 0, 743, 744, 1, 0, 0, 0, 744, 745, 5, 64, 0, 0, 745, 746, 5, 126, 0, 0, 746, 747, 3, 88, 44, 0, 747, 748, 5, 144, 0, 0, 748, 813, 1, 0, 0, 0, 749, 750, 3, 150, 75, 0, 750, 752, 5, 126, 0, 0, 751, 753, 3, 104, 52, 0, 752, 751, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 754, 1, 0, 0, 0, 754, 755, 5, 144, 0, 0, 755, 756, 1, 0, 0, 0, 756, 757, 5, 64, 0, 0, 757, 758, 3, 150, 75, 0, 758, 813, 1, 0, 0, 0, 759, 765, 3, 150, 75, 0, 760, 762, 5, 126, 0, 0, 761, 763, 3, 104, 52, 0, 762, 761, 1, 0, 0, 0, 762, 763, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 766, 5, 144, 0, 0, 765, 760, 1, 0, 0, 0, 765, 766, 1, 0, 0, 0, 766, 767, 1, 0, 0, 0, 767, 769, 5, 126, 0, 0, 768, 770, 5, 23, 0, 0, 769, 768, 1, 0, 0, 0, 769, 770, 1, 0, 0, 0, 770, 772, 1, 0, 0, 0, 771, 773, 3, 108, 54, 0, 772, 771, 1, 0, 0, 0, 772, 773, 1, 0, 0, 0, 773, 774, 1, 0, 0, 0, 774, 775, 5, 144, 0, 0, 775, 813, 1, 0, 0, 0, 776, 813, 3, 114, 57, 0, 777, 813, 3, 158, 79, 0, 778, 813, 3, 140, 70, 0, 779, 780, 5, 114, 0, 0, 780, 813, 3, 106, 53, 19, 781, 782, 5, 56, 0, 0, 782, 813, 3, 106, 53, 13, 783, 784, 3, 130, 65, 0, 784, 785, 5, 116, 0, 0, 785, 787, 1, 0, 0, 0, 786, 783, 1, 0, 0, 0, 786, 787, 1, 0, 0, 0, 787, 788, 1, 0, 0, 0, 788, 813, 5, 108, 0, 0, 789, 790, 5, 126, 0, 0, 790, 791, 3, 34, 17, 0, 791, 792, 5, 144, 0, 0, 792, 813, 1, 0, 0, 0, 793, 794, 5, 126, 0, 0, 794, 795, 3, 106, 53, 0, 795, 796, 5, 144, 0, 0, 796, 813, 1, 0, 0, 0, 797, 798, 5, 126, 0, 0, 798, 799, 3, 104, 52, 0, 799, 800, 5, 144, 0, 0, 800, 813, 1, 0, 0, 0, 801, 803, 5, 125, 0, 0, 802, 804, 3, 104, 52, 0, 803, 802, 1, 0, 0, 0, 803, 804, 1, 0, 0, 0, 804, 805, 1, 0, 0, 0, 805, 813, 5, 143, 0, 0, 806, 808, 5, 124, 0, 0, 807, 809, 3, 30, 15, 0, 808, 807, 1, 0, 0, 0, 808, 809, 1, 0, 0, 0, 809, 810, 1, 0, 0, 0, 810, 813, 5, 142, 0, 0, 811, 813, 3, 122, 61, 0, 812, 683, 1, 0, 0, 0, 812, 703, 1, 0, 0, 0, 812, 710, 1, 0, 0, 0, 812, 712, 1, 0, 0, 0, 812, 716, 1, 0, 0, 0, 812, 727, 1, 0, 0, 0, 812, 729, 1, 0, 0, 0, 812, 737, 1, 0, 0, 0, 812, 749, 1, 0, 0, 0, 812, 759, 1, 0, 0, 0, 812, 776, 1, 0, 0, 0, 812, 777, 1, 0, 0, 0, 812, 778, 1, 0, 0, 0, 812, 779, 1, 0, 0, 0, 812, 781, 1, 0, 0, 0, 812, 786, 1, 0, 0, 0, 812, 789, 1, 0, 0, 0, 812, 793, 1, 0, 0, 0, 812, 797, 1, 0, 0, 0, 812, 801, 1, 0, 0, 0, 812, 806, 1, 0, 0, 0, 812, 811, 1, 0, 0, 0, 813, 907, 1, 0, 0, 0, 814, 818, 10, 18, 0, 0, 815, 819, 5, 108, 0, 0, 816, 819, 5, 146, 0, 0, 817, 819, 5, 133, 0, 0, 818, 815, 1, 0, 0, 0, 818, 816, 1, 0, 0, 0, 818, 817, 1, 0, 0, 0, 819, 820, 1, 0, 0, 0, 820, 906, 3, 106, 53, 19, 821, 825, 10, 17, 0, 0, 822, 826, 5, 134, 0, 0, 823, 826, 5, 114, 0, 0, 824, 826, 5, 113, 0, 0, 825, 822, 1, 0, 0, 0, 825, 823, 1, 0, 0, 0, 825, 824, 1, 0, 0, 0, 826, 827, 1, 0, 0, 0, 827, 906, 3, 106, 53, 18, 828, 853, 10, 16, 0, 0, 829, 854, 5, 117, 0, 0, 830, 854, 5, 118, 0, 0, 831, 854, 5, 129, 0, 0, 832, 854, 5, 127, 0, 0, 833, 854, 5, 128, 0, 0, 834, 854, 5, 119, 0, 0, 835, 854, 5, 120, 0, 0, 836, 838, 5, 56, 0, 0, 837, 836, 1, 0, 0, 0, 837, 838, 1, 0, 0, 0, 838, 839, 1, 0, 0, 0, 839, 841, 5, 40, 0, 0, 840, 842, 5, 14, 0, 0, 841, 840, 1, 0, 0, 0, 841, 842, 1, 0, 0, 0, 842, 854, 1, 0, 0, 0, 843, 845, 5, 56, 0, 0, 844, 843, 1, 0, 0, 0, 844, 845, 1, 0, 0, 0, 845, 846, 1, 0, 0, 0, 846, 854, 7, 10, 0, 0, 847, 854, 5, 140, 0, 0, 848, 854, 5, 141, 0, 0, 849, 854, 5, 131, 0, 0, 850, 854, 5, 122, 0, 0, 851, 854, 5, 123, 0, 0, 852, 854, 5, 130, 0, 0, 853, 829, 1, 0, 0, 0, 853, 830, 1, 0, 0, 0, 853, 831, 1, 0, 0, 0, 853, 832, 1, 0, 0, 0, 853, 833, 1, 0, 0, 0, 853, 834, 1, 0, 0, 0, 853, 835, 1, 0, 0, 0, 853, 837, 1, 0, 0, 0, 853, 844, 1, 0, 0, 0, 853, 847, 1, 0, 0, 0, 853, 848, 1, 0, 0, 0, 853, 849, 1, 0, 0, 0, 853, 850, 1, 0, 0, 0, 853, 851, 1, 0, 0, 0, 853, 852, 1, 0, 0, 0, 854, 855, 1, 0, 0, 0, 855, 906, 3, 106, 53, 17, 856, 857, 10, 14, 0, 0, 857, 858, 5, 132, 0, 0, 858, 906, 3, 106, 53, 15, 859, 860, 10, 12, 0, 0, 860, 861, 5, 2, 0, 0, 861, 906, 3, 106, 53, 13, 862, 863, 10, 11, 0, 0, 863, 864, 5, 61, 0, 0, 864, 906, 3, 106, 53, 12, 865, 867, 10, 10, 0, 0, 866, 868, 5, 56, 0, 0, 867, 866, 1, 0, 0, 0, 867, 868, 1, 0, 0, 0, 868, 869, 1, 0, 0, 0, 869, 870, 5, 9, 0, 0, 870, 871, 3, 106, 53, 0, 871, 872, 5, 2, 0, 0, 872, 873, 3, 106, 53, 11, 873, 906, 1, 0, 0, 0, 874, 875, 10, 9, 0, 0, 875, 876, 5, 135, 0, 0, 876, 877, 3, 106, 53, 0, 877, 878, 5, 111, 0, 0, 878, 879, 3, 106, 53, 9, 879, 906, 1, 0, 0, 0, 880, 881, 10, 22, 0, 0, 881, 882, 5, 125, 0, 0, 882, 883, 3, 106, 53, 0, 883, 884, 5, 143, 0, 0, 884, 906, 1, 0, 0, 0, 885, 886, 10, 21, 0, 0, 886, 887, 5, 116, 0, 0, 887, 906, 5, 104, 0, 0, 888, 889, 10, 20, 0, 0, 889, 890, 5, 116, 0, 0, 890, 906, 3, 150, 75, 0, 891, 892, 10, 15, 0, 0, 892, 894, 5, 44, 0, 0, 893, 895, 5, 56, 0, 0, 894, 893, 1, 0, 0, 0, 894, 895, 1, 0, 0, 0, 895, 896, 1, 0, 0, 0, 896, 906, 5, 57, 0, 0, 897, 903, 10, 8, 0, 0, 898, 904, 3, 148, 74, 0, 899, 900, 5, 6, 0, 0, 900, 904, 3, 150, 75, 0, 901, 902, 5, 6, 0, 0, 902, 904, 5, 106, 0, 0, 903, 898, 1, 0, 0, 0, 903, 899, 1, 0, 0, 0, 903, 901, 1, 0, 0, 0, 904, 906, 1, 0, 0, 0, 905, 814, 1, 0, 0, 0, 905, 821, 1, 0, 0, 0, 905, 828, 1, 0, 0, 0, 905, 856, 1, 0, 0, 0, 905, 859, 1, 0, 0, 0, 905, 862, 1, 0, 0, 0, 905, 865, 1, 0, 0, 0, 905, 874, 1, 0, 0, 0, 905, 880, 1, 0, 0, 0, 905, 885, 1, 0, 0, 0, 905, 888, 1, 0, 0, 0, 905, 891, 1, 0, 0, 0, 905, 897, 1, 0, 0, 0, 906, 909, 1, 0, 0, 0, 907, 905, 1, 0, 0, 0, 907, 908, 1, 0, 0, 0, 908, 107, 1, 0, 0, 0, 909, 907, 1, 0, 0, 0, 910, 915, 3, 110, 55, 0, 911, 912, 5, 112, 0, 0, 912, 914, 3, 110, 55, 0, 913, 911, 1, 0, 0, 0, 914, 917, 1, 0, 0, 0, 915, 913, 1, 0, 0, 0, 915, 916, 1, 0, 0, 0, 916, 109, 1, 0, 0, 0, 917, 915, 1, 0, 0, 0, 918, 921, 3, 112, 56, 0, 919, 921, 3, 106, 53, 0, 920, 918, 1, 0, 0, 0, 920, 919, 1, 0, 0, 0, 921, 111, 1, 0, 0, 0, 922, 923, 5, 126, 0, 0, 923, 928, 3, 150, 75, 0, 924, 925, 5, 112, 0, 0, 925, 927, 3, 150, 75, 0, 926, 924, 1, 0, 0, 0, 927, 930, 1, 0, 0, 0, 928, 926, 1, 0, 0, 0, 928, 929, 1, 0, 0, 0, 929, 931, 1, 0, 0, 0, 930, 928, 1, 0, 0, 0, 931, 932, 5, 144, 0, 0, 932, 942, 1, 0, 0, 0, 933, 938, 3, 150, 75, 0, 934, 935, 5, 112, 0, 0, 935, 937, 3, 150, 75, 0, 936, 934, 1, 0, 0, 0, 937, 940, 1, 0, 0, 0, 938, 936, 1, 0, 0, 0, 938, 939, 1, 0, 0, 0, 939, 942, 1, 0, 0, 0, 940, 938, 1, 0, 0, 0, 941, 922, 1, 0, 0, 0, 941, 933, 1, 0, 0, 0, 942, 943, 1, 0, 0, 0, 943, 944, 5, 107, 0, 0, 944, 945, 3, 106, 53, 0, 945, 113, 1, 0, 0, 0, 946, 947, 5, 128, 0, 0, 947, 951, 3, 150, 75, 0, 948, 950, 3, 116, 58, 0, 949, 948, 1, 0, 0, 0, 950, 953, 1, 0, 0, 0, 951, 949, 1, 0, 0, 0, 951, 952, 1, 0, 0, 0, 952, 954, 1, 0, 0, 0, 953, 951, 1, 0, 0, 0, 954, 955, 5, 146, 0, 0, 955, 956, 5, 120, 0, 0, 956, 975, 1, 0, 0, 0, 957, 958, 5, 128, 0, 0, 958, 962, 3, 150, 75, 0, 959, 961, 3, 116, 58, 0, 960, 959, 1, 0, 0, 0, 961, 964, 1, 0, 0, 0, 962, 960, 1, 0, 0, 0, 962, 963, 1, 0, 0, 0, 963, 965, 1, 0, 0, 0, 964, 962, 1, 0, 0, 0, 965, 967, 5, 120, 0, 0, 966, 968, 3, 114, 57, 0, 967, 966, 1, 0, 0, 0, 967, 968, 1, 0, 0, 0, 968, 969, 1, 0, 0, 0, 969, 970, 5, 128, 0, 0, 970, 971, 5, 146, 0, 0, 971, 972, 3, 150, 75, 0, 972, 973, 5, 120, 0, 0, 973, 975, 1, 0, 0, 0, 974, 946, 1, 0, 0, 0, 974, 957, 1, 0, 0, 0, 975, 115, 1, 0, 0, 0, 976, 977, 3, 150, 75, 0, 977, 978, 5, 118, 0, 0, 978, 979, 3, 156, 78, 0, 979, 988, 1, 0, 0, 0, 980, 981, 3, 150, 75, 0, 981, 982, 5, 118, 0, 0, 982, 983, 5, 124, 0, 0, 983, 984, 3, 106, 53, 0, 984, 985, 5, 142, 0, 0, 985, 988, 1, 0, 0, 0, 986, 988, 3, 150, 75, 0, 987, 976, 1, 0, 0, 0, 987, 980, 1, 0, 0, 0, 987, 986, 1, 0, 0, 0, 988, 117, 1, 0, 0, 0, 989, 994, 3, 120, 60, 0, 990, 991, 5, 112, 0, 0, 991, 993, 3, 120, 60, 0, 992, 990, 1, 0, 0, 0, 993, 996, 1, 0, 0, 0, 994, 992, 1, 0, 0, 0, 994, 995, 1, 0, 0, 0, 995, 119, 1, 0, 0, 0, 996, 994, 1, 0, 0, 0, 997, 998, 3, 150, 75, 0, 998, 999, 5, 6, 0, 0, 999, 1000, 5, 126, 0, 0, 1000, 1001, 3, 34, 17, 0, 1001, 1002, 5, 144, 0, 0, 1002, 1008, 1, 0, 0, 0, 1003, 1004, 3, 106, 53, 0, 1004, 1005, 5, 6, 0, 0, 1005, 1006, 3, 150, 75, 0, 1006, 1008, 1, 0, 0, 0, 1007, 997, 1, 0, 0, 0, 1007, 1003, 1, 0, 0, 0, 1008, 121, 1, 0, 0, 0, 1009, 1017, 3, 154, 77, 0, 1010, 1011, 3, 130, 65, 0, 1011, 1012, 5, 116, 0, 0, 1012, 1014, 1, 0, 0, 0, 1013, 1010, 1, 0, 0, 0, 1013, 1014, 1, 0, 0, 0, 1014, 1015, 1, 0, 0, 0, 1015, 1017, 3, 124, 62, 0, 1016, 1009, 1, 0, 0, 0, 1016, 1013, 1, 0, 0, 0, 1017, 123, 1, 0, 0, 0, 1018, 1023, 3, 150, 75, 0, 1019, 1020, 5, 116, 0, 0, 1020, 1022, 3, 150, 75, 0, 1021, 1019, 1, 0, 0, 0, 1022, 1025, 1, 0, 0, 0, 1023, 1021, 1, 0, 0, 0, 1023, 1024, 1, 0, 0, 0, 1024, 125, 1, 0, 0, 0, 1025, 1023, 1, 0, 0, 0, 1026, 1027, 6, 63, -1, 0, 1027, 1036, 3, 130, 65, 0, 1028, 1036, 3, 128, 64, 0, 1029, 1030, 5, 126, 0, 0, 1030, 1031, 3, 34, 17, 0, 1031, 1032, 5, 144, 0, 0, 1032, 1036, 1, 0, 0, 0, 1033, 1036, 3, 114, 57, 0, 1034, 1036, 3, 154, 77, 0, 1035, 1026, 1, 0, 0, 0, 1035, 1028, 1, 0, 0, 0, 1035, 1029, 1, 0, 0, 0, 1035, 1033, 1, 0, 0, 0, 1035, 1034, 1, 0, 0, 0, 1036, 1045, 1, 0, 0, 0, 1037, 1041, 10, 3, 0, 0, 1038, 1042, 3, 148, 74, 0, 1039, 1040, 5, 6, 0, 0, 1040, 1042, 3, 150, 75, 0, 1041, 1038, 1, 0, 0, 0, 1041, 1039, 1, 0, 0, 0, 1042, 1044, 1, 0, 0, 0, 1043, 1037, 1, 0, 0, 0, 1044, 1047, 1, 0, 0, 0, 1045, 1043, 1, 0, 0, 0, 1045, 1046, 1, 0, 0, 0, 1046, 127, 1, 0, 0, 0, 1047, 1045, 1, 0, 0, 0, 1048, 1049, 3, 150, 75, 0, 1049, 1051, 5, 126, 0, 0, 1050, 1052, 3, 132, 66, 0, 1051, 1050, 1, 0, 0, 0, 1051, 1052, 1, 0, 0, 0, 1052, 1053, 1, 0, 0, 0, 1053, 1054, 5, 144, 0, 0, 1054, 129, 1, 0, 0, 0, 1055, 1056, 3, 134, 67, 0, 1056, 1057, 5, 116, 0, 0, 1057, 1059, 1, 0, 0, 0, 1058, 1055, 1, 0, 0, 0, 1058, 1059, 1, 0, 0, 0, 1059, 1060, 1, 0, 0, 0, 1060, 1061, 3, 150, 75, 0, 1061, 131, 1, 0, 0, 0, 1062, 1067, 3, 106, 53, 0, 1063, 1064, 5, 112, 0, 0, 1064, 1066, 3, 106, 53, 0, 1065, 1063, 1, 0, 0, 0, 1066, 1069, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1067, 1068, 1, 0, 0, 0, 1068, 133, 1, 0, 0, 0, 1069, 1067, 1, 0, 0, 0, 1070, 1071, 3, 150, 75, 0, 1071, 135, 1, 0, 0, 0, 1072, 1081, 5, 102, 0, 0, 1073, 1074, 5, 116, 0, 0, 1074, 1081, 7, 11, 0, 0, 1075, 1076, 5, 104, 0, 0, 1076, 1078, 5, 116, 0, 0, 1077, 1079, 7, 11, 0, 0, 1078, 1077, 1, 0, 0, 0, 1078, 1079, 1, 0, 0, 0, 1079, 1081, 1, 0, 0, 0, 1080, 1072, 1, 0, 0, 0, 1080, 1073, 1, 0, 0, 0, 1080, 1075, 1, 0, 0, 0, 1081, 137, 1, 0, 0, 0, 1082, 1084, 7, 12, 0, 0, 1083, 1082, 1, 0, 0, 0, 1083, 1084, 1, 0, 0, 0, 1084, 1091, 1, 0, 0, 0, 1085, 1092, 3, 136, 68, 0, 1086, 1092, 5, 103, 0, 0, 1087, 1092, 5, 104, 0, 0, 1088, 1092, 5, 105, 0, 0, 1089, 1092, 5, 41, 0, 0, 1090, 1092, 5, 55, 0, 0, 1091, 1085, 1, 0, 0, 0, 1091, 1086, 1, 0, 0, 0, 1091, 1087, 1, 0, 0, 0, 1091, 1088, 1, 0, 0, 0, 1091, 1089, 1, 0, 0, 0, 1091, 1090, 1, 0, 0, 0, 1092, 139, 1, 0, 0, 0, 1093, 1097, 3, 138, 69, 0, 1094, 1097, 5, 106, 0, 0, 1095, 1097, 5, 57, 0, 0, 1096, 1093, 1, 0, 0, 0, 1096, 1094, 1, 0, 0, 0, 1096, 1095, 1, 0, 0, 0, 1097, 141, 1, 0, 0, 0, 1098, 1099, 7, 13, 0, 0, 1099, 143, 1, 0, 0, 0, 1100, 1101, 7, 14, 0, 0, 1101, 145, 1, 0, 0, 0, 1102, 1103, 7, 15, 0, 0, 1103, 147, 1, 0, 0, 0, 1104, 1107, 5, 101, 0, 0, 1105, 1107, 3, 146, 73, 0, 1106, 1104, 1, 0, 0, 0, 1106, 1105, 1, 0, 0, 0, 1107, 149, 1, 0, 0, 0, 1108, 1112, 5, 101, 0, 0, 1109, 1112, 3, 142, 71, 0, 1110, 1112, 3, 144, 72, 0, 1111, 1108, 1, 0, 0, 0, 1111, 1109, 1, 0, 0, 0, 1111, 1110, 1, 0, 0, 0, 1112, 151, 1, 0, 0, 0, 1113, 1114, 3, 156, 78, 0, 1114, 1115, 5, 118, 0, 0, 1115, 1116, 3, 138, 69, 0, 1116, 153, 1, 0, 0, 0, 1117, 1118, 5, 124, 0, 0, 1118, 1119, 3, 150, 75, 0, 1119, 1120, 5, 142, 0, 0, 1120, 155, 1, 0, 0, 0, 1121, 1124, 5, 106, 0, 0, 1122, 1124, 3, 158, 79, 0, 1123, 1121, 1, 0, 0, 0, 1123, 1122, 1, 0, 0, 0, 1124, 157, 1, 0, 0, 0, 1125, 1129, 5, 137, 0, 0, 1126, 1128, 3, 160, 80, 0, 1127, 1126, 1, 0, 0, 0, 1128, 1131, 1, 0, 0, 0, 1129, 1127, 1, 0, 0, 0, 1129, 1130, 1, 0, 0, 0, 1130, 1132, 1, 0, 0, 0, 1131, 1129, 1, 0, 0, 0, 1132, 1133, 5, 139, 0, 0, 1133, 159, 1, 0, 0, 0, 1134, 1135, 5, 152, 0, 0, 1135, 1136, 3, 106, 53, 0, 1136, 1137, 5, 142, 0, 0, 1137, 1140, 1, 0, 0, 0, 1138, 1140, 5, 151, 0, 0, 1139, 1134, 1, 0, 0, 0, 1139, 1138, 1, 0, 0, 0, 1140, 161, 1, 0, 0, 0, 1141, 1145, 5, 138, 0, 0, 1142, 1144, 3, 164, 82, 0, 1143, 1142, 1, 0, 0, 0, 1144, 1147, 1, 0, 0, 0, 1145, 1143, 1, 0, 0, 0, 1145, 1146, 1, 0, 0, 0, 1146, 1148, 1, 0, 0, 0, 1147, 1145, 1, 0, 0, 0, 1148, 1149, 5, 0, 0, 1, 1149, 163, 1, 0, 0, 0, 1150, 1151, 5, 154, 0, 0, 1151, 1152, 3, 106, 53, 0, 1152, 1153, 5, 142, 0, 0, 1153, 1156, 1, 0, 0, 0, 1154, 1156, 5, 153, 0, 0, 1155, 1150, 1, 0, 0, 0, 1155, 1154, 1, 0, 0, 0, 1156, 165, 1, 0, 0, 0, 135, 169, 176, 185, 200, 212, 224, 240, 251, 265, 271, 281, 290, 293, 297, 300, 304, 307, 310, 313, 316, 320, 324, 327, 330, 333, 337, 340, 349, 355, 376, 393, 410, 416, 422, 433, 435, 446, 449, 455, 463, 469, 471, 475, 480, 483, 486, 490, 494, 497, 499, 502, 506, 510, 513, 515, 517, 522, 533, 539, 546, 551, 555, 559, 565, 567, 574, 582, 585, 588, 607, 621, 637, 649, 661, 669, 673, 680, 686, 695, 699, 723, 740, 752, 762, 765, 769, 772, 786, 803, 808, 812, 818, 825, 837, 841, 844, 853, 867, 894, 903, 905, 907, 915, 920, 928, 938, 941, 951, 962, 967, 974, 987, 994, 1007, 1013, 1016, 1023, 1035, 1041, 1045, 1051, 1058, 1067, 1078, 1080, 1083, 1091, 1096, 1106, 1111, 1123, 1129, 1139, 1145, 1155] \ No newline at end of file +[4, 1, 154, 1178, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 199, 8, 5, 10, 5, 12, 5, 202, 9, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 213, 8, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 225, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 241, 8, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 250, 8, 13, 10, 13, 12, 13, 253, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 264, 8, 15, 10, 15, 12, 15, 267, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 272, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 280, 8, 17, 10, 17, 12, 17, 283, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 291, 8, 18, 1, 19, 3, 19, 294, 8, 19, 1, 19, 1, 19, 3, 19, 298, 8, 19, 1, 19, 3, 19, 301, 8, 19, 1, 19, 1, 19, 3, 19, 305, 8, 19, 1, 19, 3, 19, 308, 8, 19, 1, 19, 3, 19, 311, 8, 19, 1, 19, 3, 19, 314, 8, 19, 1, 19, 3, 19, 317, 8, 19, 1, 19, 1, 19, 3, 19, 321, 8, 19, 1, 19, 1, 19, 3, 19, 325, 8, 19, 1, 19, 3, 19, 328, 8, 19, 1, 19, 3, 19, 331, 8, 19, 1, 19, 3, 19, 334, 8, 19, 1, 19, 1, 19, 3, 19, 338, 8, 19, 1, 19, 3, 19, 341, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 350, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 356, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 375, 8, 24, 10, 24, 12, 24, 378, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 394, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 411, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 417, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 423, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 434, 8, 31, 3, 31, 436, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 447, 8, 34, 1, 34, 3, 34, 450, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 456, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 464, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 470, 8, 34, 10, 34, 12, 34, 473, 9, 34, 1, 35, 3, 35, 476, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 481, 8, 35, 1, 35, 3, 35, 484, 8, 35, 1, 35, 3, 35, 487, 8, 35, 1, 35, 1, 35, 3, 35, 491, 8, 35, 1, 35, 1, 35, 3, 35, 495, 8, 35, 1, 35, 3, 35, 498, 8, 35, 3, 35, 500, 8, 35, 1, 35, 3, 35, 503, 8, 35, 1, 35, 1, 35, 3, 35, 507, 8, 35, 1, 35, 1, 35, 3, 35, 511, 8, 35, 1, 35, 3, 35, 514, 8, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 523, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 534, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 540, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 545, 8, 39, 10, 39, 12, 39, 548, 9, 39, 1, 40, 1, 40, 3, 40, 552, 8, 40, 1, 40, 1, 40, 3, 40, 556, 8, 40, 1, 40, 1, 40, 3, 40, 560, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 566, 8, 41, 3, 41, 568, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 573, 8, 42, 10, 42, 12, 42, 576, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 583, 8, 44, 1, 44, 3, 44, 586, 8, 44, 1, 44, 3, 44, 589, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 608, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 622, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 636, 8, 51, 10, 51, 12, 51, 639, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 648, 8, 51, 10, 51, 12, 51, 651, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 660, 8, 51, 10, 51, 12, 51, 663, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 670, 8, 51, 1, 51, 1, 51, 3, 51, 674, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 679, 8, 52, 10, 52, 12, 52, 682, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 687, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 694, 8, 53, 11, 53, 12, 53, 695, 1, 53, 1, 53, 3, 53, 700, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 724, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 741, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 747, 8, 53, 1, 53, 3, 53, 750, 8, 53, 1, 53, 3, 53, 753, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 763, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 769, 8, 53, 1, 53, 3, 53, 772, 8, 53, 1, 53, 3, 53, 775, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 783, 8, 53, 1, 53, 3, 53, 786, 8, 53, 1, 53, 1, 53, 3, 53, 790, 8, 53, 1, 53, 3, 53, 793, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 807, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 824, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 829, 8, 53, 1, 53, 1, 53, 3, 53, 833, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 839, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 846, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 858, 8, 53, 1, 53, 1, 53, 3, 53, 862, 8, 53, 1, 53, 3, 53, 865, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 874, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 888, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 915, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 924, 8, 53, 5, 53, 926, 8, 53, 10, 53, 12, 53, 929, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 934, 8, 54, 10, 54, 12, 54, 937, 9, 54, 1, 55, 1, 55, 3, 55, 941, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 947, 8, 56, 10, 56, 12, 56, 950, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 957, 8, 56, 10, 56, 12, 56, 960, 9, 56, 3, 56, 962, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 970, 8, 57, 10, 57, 12, 57, 973, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 981, 8, 57, 10, 57, 12, 57, 984, 9, 57, 1, 57, 1, 57, 3, 57, 988, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 995, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 1008, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 1013, 8, 59, 10, 59, 12, 59, 1016, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1028, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1034, 8, 61, 1, 61, 3, 61, 1037, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1042, 8, 62, 10, 62, 12, 62, 1045, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1056, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1062, 8, 63, 5, 63, 1064, 8, 63, 10, 63, 12, 63, 1067, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1072, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1079, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1086, 8, 66, 10, 66, 12, 66, 1089, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1099, 8, 68, 3, 68, 1101, 8, 68, 1, 69, 3, 69, 1104, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1112, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1117, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1127, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1132, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1144, 8, 78, 1, 79, 1, 79, 5, 79, 1148, 8, 79, 10, 79, 12, 79, 1151, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1160, 8, 80, 1, 81, 1, 81, 5, 81, 1164, 8, 81, 10, 81, 12, 81, 1167, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1176, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1314, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 189, 1, 0, 0, 0, 10, 195, 1, 0, 0, 0, 12, 212, 1, 0, 0, 0, 14, 214, 1, 0, 0, 0, 16, 217, 1, 0, 0, 0, 18, 226, 1, 0, 0, 0, 20, 232, 1, 0, 0, 0, 22, 236, 1, 0, 0, 0, 24, 245, 1, 0, 0, 0, 26, 247, 1, 0, 0, 0, 28, 256, 1, 0, 0, 0, 30, 260, 1, 0, 0, 0, 32, 271, 1, 0, 0, 0, 34, 275, 1, 0, 0, 0, 36, 290, 1, 0, 0, 0, 38, 293, 1, 0, 0, 0, 40, 342, 1, 0, 0, 0, 42, 345, 1, 0, 0, 0, 44, 351, 1, 0, 0, 0, 46, 355, 1, 0, 0, 0, 48, 361, 1, 0, 0, 0, 50, 379, 1, 0, 0, 0, 52, 382, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 395, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 402, 1, 0, 0, 0, 62, 435, 1, 0, 0, 0, 64, 437, 1, 0, 0, 0, 66, 440, 1, 0, 0, 0, 68, 455, 1, 0, 0, 0, 70, 517, 1, 0, 0, 0, 72, 522, 1, 0, 0, 0, 74, 533, 1, 0, 0, 0, 76, 535, 1, 0, 0, 0, 78, 541, 1, 0, 0, 0, 80, 549, 1, 0, 0, 0, 82, 567, 1, 0, 0, 0, 84, 569, 1, 0, 0, 0, 86, 577, 1, 0, 0, 0, 88, 582, 1, 0, 0, 0, 90, 590, 1, 0, 0, 0, 92, 594, 1, 0, 0, 0, 94, 598, 1, 0, 0, 0, 96, 607, 1, 0, 0, 0, 98, 621, 1, 0, 0, 0, 100, 623, 1, 0, 0, 0, 102, 673, 1, 0, 0, 0, 104, 675, 1, 0, 0, 0, 106, 832, 1, 0, 0, 0, 108, 930, 1, 0, 0, 0, 110, 940, 1, 0, 0, 0, 112, 961, 1, 0, 0, 0, 114, 994, 1, 0, 0, 0, 116, 1007, 1, 0, 0, 0, 118, 1009, 1, 0, 0, 0, 120, 1027, 1, 0, 0, 0, 122, 1036, 1, 0, 0, 0, 124, 1038, 1, 0, 0, 0, 126, 1055, 1, 0, 0, 0, 128, 1068, 1, 0, 0, 0, 130, 1078, 1, 0, 0, 0, 132, 1082, 1, 0, 0, 0, 134, 1090, 1, 0, 0, 0, 136, 1100, 1, 0, 0, 0, 138, 1103, 1, 0, 0, 0, 140, 1116, 1, 0, 0, 0, 142, 1118, 1, 0, 0, 0, 144, 1120, 1, 0, 0, 0, 146, 1122, 1, 0, 0, 0, 148, 1126, 1, 0, 0, 0, 150, 1131, 1, 0, 0, 0, 152, 1133, 1, 0, 0, 0, 154, 1137, 1, 0, 0, 0, 156, 1143, 1, 0, 0, 0, 158, 1145, 1, 0, 0, 0, 160, 1159, 1, 0, 0, 0, 162, 1161, 1, 0, 0, 0, 164, 1175, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 12, 6, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 188, 5, 145, 0, 0, 188, 7, 1, 0, 0, 0, 189, 190, 3, 4, 2, 0, 190, 191, 5, 111, 0, 0, 191, 192, 5, 118, 0, 0, 192, 193, 3, 4, 2, 0, 193, 194, 5, 145, 0, 0, 194, 9, 1, 0, 0, 0, 195, 200, 3, 150, 75, 0, 196, 197, 5, 112, 0, 0, 197, 199, 3, 150, 75, 0, 198, 196, 1, 0, 0, 0, 199, 202, 1, 0, 0, 0, 200, 198, 1, 0, 0, 0, 200, 201, 1, 0, 0, 0, 201, 11, 1, 0, 0, 0, 202, 200, 1, 0, 0, 0, 203, 213, 3, 20, 10, 0, 204, 213, 3, 24, 12, 0, 205, 213, 3, 14, 7, 0, 206, 213, 3, 16, 8, 0, 207, 213, 3, 18, 9, 0, 208, 213, 3, 22, 11, 0, 209, 213, 3, 8, 4, 0, 210, 213, 3, 20, 10, 0, 211, 213, 3, 26, 13, 0, 212, 203, 1, 0, 0, 0, 212, 204, 1, 0, 0, 0, 212, 205, 1, 0, 0, 0, 212, 206, 1, 0, 0, 0, 212, 207, 1, 0, 0, 0, 212, 208, 1, 0, 0, 0, 212, 209, 1, 0, 0, 0, 212, 210, 1, 0, 0, 0, 212, 211, 1, 0, 0, 0, 213, 13, 1, 0, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 145, 0, 0, 216, 15, 1, 0, 0, 0, 217, 218, 5, 38, 0, 0, 218, 219, 5, 126, 0, 0, 219, 220, 3, 4, 2, 0, 220, 221, 5, 144, 0, 0, 221, 224, 3, 12, 6, 0, 222, 223, 5, 24, 0, 0, 223, 225, 3, 12, 6, 0, 224, 222, 1, 0, 0, 0, 224, 225, 1, 0, 0, 0, 225, 17, 1, 0, 0, 0, 226, 227, 5, 96, 0, 0, 227, 228, 5, 126, 0, 0, 228, 229, 3, 4, 2, 0, 229, 230, 5, 144, 0, 0, 230, 231, 3, 12, 6, 0, 231, 19, 1, 0, 0, 0, 232, 233, 5, 70, 0, 0, 233, 234, 3, 4, 2, 0, 234, 235, 5, 145, 0, 0, 235, 21, 1, 0, 0, 0, 236, 237, 5, 29, 0, 0, 237, 238, 3, 150, 75, 0, 238, 240, 5, 126, 0, 0, 239, 241, 3, 10, 5, 0, 240, 239, 1, 0, 0, 0, 240, 241, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 5, 144, 0, 0, 243, 244, 3, 26, 13, 0, 244, 23, 1, 0, 0, 0, 245, 246, 5, 145, 0, 0, 246, 25, 1, 0, 0, 0, 247, 251, 5, 124, 0, 0, 248, 250, 3, 2, 1, 0, 249, 248, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 254, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 255, 5, 142, 0, 0, 255, 27, 1, 0, 0, 0, 256, 257, 3, 4, 2, 0, 257, 258, 5, 111, 0, 0, 258, 259, 3, 4, 2, 0, 259, 29, 1, 0, 0, 0, 260, 265, 3, 28, 14, 0, 261, 262, 5, 112, 0, 0, 262, 264, 3, 28, 14, 0, 263, 261, 1, 0, 0, 0, 264, 267, 1, 0, 0, 0, 265, 263, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 31, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 272, 3, 34, 17, 0, 269, 272, 3, 38, 19, 0, 270, 272, 3, 114, 57, 0, 271, 268, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 271, 270, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 274, 5, 0, 0, 1, 274, 33, 1, 0, 0, 0, 275, 281, 3, 36, 18, 0, 276, 277, 5, 91, 0, 0, 277, 278, 5, 1, 0, 0, 278, 280, 3, 36, 18, 0, 279, 276, 1, 0, 0, 0, 280, 283, 1, 0, 0, 0, 281, 279, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 35, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 284, 291, 3, 38, 19, 0, 285, 286, 5, 126, 0, 0, 286, 287, 3, 34, 17, 0, 287, 288, 5, 144, 0, 0, 288, 291, 1, 0, 0, 0, 289, 291, 3, 154, 77, 0, 290, 284, 1, 0, 0, 0, 290, 285, 1, 0, 0, 0, 290, 289, 1, 0, 0, 0, 291, 37, 1, 0, 0, 0, 292, 294, 3, 40, 20, 0, 293, 292, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 297, 5, 77, 0, 0, 296, 298, 5, 23, 0, 0, 297, 296, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 300, 1, 0, 0, 0, 299, 301, 3, 42, 21, 0, 300, 299, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 304, 3, 104, 52, 0, 303, 305, 3, 44, 22, 0, 304, 303, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 307, 1, 0, 0, 0, 306, 308, 3, 46, 23, 0, 307, 306, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308, 310, 1, 0, 0, 0, 309, 311, 3, 50, 25, 0, 310, 309, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 313, 1, 0, 0, 0, 312, 314, 3, 52, 26, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 316, 1, 0, 0, 0, 315, 317, 3, 54, 27, 0, 316, 315, 1, 0, 0, 0, 316, 317, 1, 0, 0, 0, 317, 320, 1, 0, 0, 0, 318, 319, 5, 98, 0, 0, 319, 321, 7, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 324, 1, 0, 0, 0, 322, 323, 5, 98, 0, 0, 323, 325, 5, 86, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 327, 1, 0, 0, 0, 326, 328, 3, 56, 28, 0, 327, 326, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328, 330, 1, 0, 0, 0, 329, 331, 3, 48, 24, 0, 330, 329, 1, 0, 0, 0, 330, 331, 1, 0, 0, 0, 331, 333, 1, 0, 0, 0, 332, 334, 3, 58, 29, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 337, 1, 0, 0, 0, 335, 338, 3, 62, 31, 0, 336, 338, 3, 64, 32, 0, 337, 335, 1, 0, 0, 0, 337, 336, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 340, 1, 0, 0, 0, 339, 341, 3, 66, 33, 0, 340, 339, 1, 0, 0, 0, 340, 341, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 343, 5, 98, 0, 0, 343, 344, 3, 118, 59, 0, 344, 41, 1, 0, 0, 0, 345, 346, 5, 85, 0, 0, 346, 349, 5, 104, 0, 0, 347, 348, 5, 98, 0, 0, 348, 350, 5, 82, 0, 0, 349, 347, 1, 0, 0, 0, 349, 350, 1, 0, 0, 0, 350, 43, 1, 0, 0, 0, 351, 352, 5, 32, 0, 0, 352, 353, 3, 68, 34, 0, 353, 45, 1, 0, 0, 0, 354, 356, 7, 1, 0, 0, 355, 354, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 357, 1, 0, 0, 0, 357, 358, 5, 5, 0, 0, 358, 359, 5, 45, 0, 0, 359, 360, 3, 104, 52, 0, 360, 47, 1, 0, 0, 0, 361, 362, 5, 97, 0, 0, 362, 363, 3, 150, 75, 0, 363, 364, 5, 6, 0, 0, 364, 365, 5, 126, 0, 0, 365, 366, 3, 88, 44, 0, 366, 376, 5, 144, 0, 0, 367, 368, 5, 112, 0, 0, 368, 369, 3, 150, 75, 0, 369, 370, 5, 6, 0, 0, 370, 371, 5, 126, 0, 0, 371, 372, 3, 88, 44, 0, 372, 373, 5, 144, 0, 0, 373, 375, 1, 0, 0, 0, 374, 367, 1, 0, 0, 0, 375, 378, 1, 0, 0, 0, 376, 374, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 49, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 379, 380, 5, 67, 0, 0, 380, 381, 3, 106, 53, 0, 381, 51, 1, 0, 0, 0, 382, 383, 5, 95, 0, 0, 383, 384, 3, 106, 53, 0, 384, 53, 1, 0, 0, 0, 385, 386, 5, 34, 0, 0, 386, 393, 5, 11, 0, 0, 387, 388, 7, 0, 0, 0, 388, 389, 5, 126, 0, 0, 389, 390, 3, 104, 52, 0, 390, 391, 5, 144, 0, 0, 391, 394, 1, 0, 0, 0, 392, 394, 3, 104, 52, 0, 393, 387, 1, 0, 0, 0, 393, 392, 1, 0, 0, 0, 394, 55, 1, 0, 0, 0, 395, 396, 5, 35, 0, 0, 396, 397, 3, 106, 53, 0, 397, 57, 1, 0, 0, 0, 398, 399, 5, 62, 0, 0, 399, 400, 5, 11, 0, 0, 400, 401, 3, 78, 39, 0, 401, 59, 1, 0, 0, 0, 402, 403, 5, 62, 0, 0, 403, 404, 5, 11, 0, 0, 404, 405, 3, 104, 52, 0, 405, 61, 1, 0, 0, 0, 406, 407, 5, 52, 0, 0, 407, 410, 3, 106, 53, 0, 408, 409, 5, 112, 0, 0, 409, 411, 3, 106, 53, 0, 410, 408, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 416, 1, 0, 0, 0, 412, 413, 5, 98, 0, 0, 413, 417, 5, 82, 0, 0, 414, 415, 5, 11, 0, 0, 415, 417, 3, 104, 52, 0, 416, 412, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 416, 417, 1, 0, 0, 0, 417, 436, 1, 0, 0, 0, 418, 419, 5, 52, 0, 0, 419, 422, 3, 106, 53, 0, 420, 421, 5, 98, 0, 0, 421, 423, 5, 82, 0, 0, 422, 420, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 425, 5, 59, 0, 0, 425, 426, 3, 106, 53, 0, 426, 436, 1, 0, 0, 0, 427, 428, 5, 52, 0, 0, 428, 429, 3, 106, 53, 0, 429, 430, 5, 59, 0, 0, 430, 433, 3, 106, 53, 0, 431, 432, 5, 11, 0, 0, 432, 434, 3, 104, 52, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 436, 1, 0, 0, 0, 435, 406, 1, 0, 0, 0, 435, 418, 1, 0, 0, 0, 435, 427, 1, 0, 0, 0, 436, 63, 1, 0, 0, 0, 437, 438, 5, 59, 0, 0, 438, 439, 3, 106, 53, 0, 439, 65, 1, 0, 0, 0, 440, 441, 5, 79, 0, 0, 441, 442, 3, 84, 42, 0, 442, 67, 1, 0, 0, 0, 443, 444, 6, 34, -1, 0, 444, 446, 3, 126, 63, 0, 445, 447, 5, 27, 0, 0, 446, 445, 1, 0, 0, 0, 446, 447, 1, 0, 0, 0, 447, 449, 1, 0, 0, 0, 448, 450, 3, 76, 38, 0, 449, 448, 1, 0, 0, 0, 449, 450, 1, 0, 0, 0, 450, 456, 1, 0, 0, 0, 451, 452, 5, 126, 0, 0, 452, 453, 3, 68, 34, 0, 453, 454, 5, 144, 0, 0, 454, 456, 1, 0, 0, 0, 455, 443, 1, 0, 0, 0, 455, 451, 1, 0, 0, 0, 456, 471, 1, 0, 0, 0, 457, 458, 10, 3, 0, 0, 458, 459, 3, 72, 36, 0, 459, 460, 3, 68, 34, 4, 460, 470, 1, 0, 0, 0, 461, 463, 10, 4, 0, 0, 462, 464, 3, 70, 35, 0, 463, 462, 1, 0, 0, 0, 463, 464, 1, 0, 0, 0, 464, 465, 1, 0, 0, 0, 465, 466, 5, 45, 0, 0, 466, 467, 3, 68, 34, 0, 467, 468, 3, 74, 37, 0, 468, 470, 1, 0, 0, 0, 469, 457, 1, 0, 0, 0, 469, 461, 1, 0, 0, 0, 470, 473, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 471, 472, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 474, 476, 7, 2, 0, 0, 475, 474, 1, 0, 0, 0, 475, 476, 1, 0, 0, 0, 476, 477, 1, 0, 0, 0, 477, 484, 5, 42, 0, 0, 478, 480, 5, 42, 0, 0, 479, 481, 7, 2, 0, 0, 480, 479, 1, 0, 0, 0, 480, 481, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 484, 7, 2, 0, 0, 483, 475, 1, 0, 0, 0, 483, 478, 1, 0, 0, 0, 483, 482, 1, 0, 0, 0, 484, 518, 1, 0, 0, 0, 485, 487, 7, 3, 0, 0, 486, 485, 1, 0, 0, 0, 486, 487, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 490, 7, 4, 0, 0, 489, 491, 5, 63, 0, 0, 490, 489, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 500, 1, 0, 0, 0, 492, 494, 7, 4, 0, 0, 493, 495, 5, 63, 0, 0, 494, 493, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 497, 1, 0, 0, 0, 496, 498, 7, 3, 0, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 500, 1, 0, 0, 0, 499, 486, 1, 0, 0, 0, 499, 492, 1, 0, 0, 0, 500, 518, 1, 0, 0, 0, 501, 503, 7, 5, 0, 0, 502, 501, 1, 0, 0, 0, 502, 503, 1, 0, 0, 0, 503, 504, 1, 0, 0, 0, 504, 506, 5, 33, 0, 0, 505, 507, 5, 63, 0, 0, 506, 505, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 516, 1, 0, 0, 0, 508, 510, 5, 33, 0, 0, 509, 511, 5, 63, 0, 0, 510, 509, 1, 0, 0, 0, 510, 511, 1, 0, 0, 0, 511, 513, 1, 0, 0, 0, 512, 514, 7, 5, 0, 0, 513, 512, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 516, 1, 0, 0, 0, 515, 502, 1, 0, 0, 0, 515, 508, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 483, 1, 0, 0, 0, 517, 499, 1, 0, 0, 0, 517, 515, 1, 0, 0, 0, 518, 71, 1, 0, 0, 0, 519, 520, 5, 16, 0, 0, 520, 523, 5, 45, 0, 0, 521, 523, 5, 112, 0, 0, 522, 519, 1, 0, 0, 0, 522, 521, 1, 0, 0, 0, 523, 73, 1, 0, 0, 0, 524, 525, 5, 60, 0, 0, 525, 534, 3, 104, 52, 0, 526, 527, 5, 92, 0, 0, 527, 528, 5, 126, 0, 0, 528, 529, 3, 104, 52, 0, 529, 530, 5, 144, 0, 0, 530, 534, 1, 0, 0, 0, 531, 532, 5, 92, 0, 0, 532, 534, 3, 104, 52, 0, 533, 524, 1, 0, 0, 0, 533, 526, 1, 0, 0, 0, 533, 531, 1, 0, 0, 0, 534, 75, 1, 0, 0, 0, 535, 536, 5, 75, 0, 0, 536, 539, 3, 82, 41, 0, 537, 538, 5, 59, 0, 0, 538, 540, 3, 82, 41, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 77, 1, 0, 0, 0, 541, 546, 3, 80, 40, 0, 542, 543, 5, 112, 0, 0, 543, 545, 3, 80, 40, 0, 544, 542, 1, 0, 0, 0, 545, 548, 1, 0, 0, 0, 546, 544, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 79, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 549, 551, 3, 106, 53, 0, 550, 552, 7, 6, 0, 0, 551, 550, 1, 0, 0, 0, 551, 552, 1, 0, 0, 0, 552, 555, 1, 0, 0, 0, 553, 554, 5, 58, 0, 0, 554, 556, 7, 7, 0, 0, 555, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 559, 1, 0, 0, 0, 557, 558, 5, 15, 0, 0, 558, 560, 5, 106, 0, 0, 559, 557, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 81, 1, 0, 0, 0, 561, 568, 3, 154, 77, 0, 562, 565, 3, 138, 69, 0, 563, 564, 5, 146, 0, 0, 564, 566, 3, 138, 69, 0, 565, 563, 1, 0, 0, 0, 565, 566, 1, 0, 0, 0, 566, 568, 1, 0, 0, 0, 567, 561, 1, 0, 0, 0, 567, 562, 1, 0, 0, 0, 568, 83, 1, 0, 0, 0, 569, 574, 3, 86, 43, 0, 570, 571, 5, 112, 0, 0, 571, 573, 3, 86, 43, 0, 572, 570, 1, 0, 0, 0, 573, 576, 1, 0, 0, 0, 574, 572, 1, 0, 0, 0, 574, 575, 1, 0, 0, 0, 575, 85, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 577, 578, 3, 150, 75, 0, 578, 579, 5, 118, 0, 0, 579, 580, 3, 140, 70, 0, 580, 87, 1, 0, 0, 0, 581, 583, 3, 90, 45, 0, 582, 581, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 585, 1, 0, 0, 0, 584, 586, 3, 92, 46, 0, 585, 584, 1, 0, 0, 0, 585, 586, 1, 0, 0, 0, 586, 588, 1, 0, 0, 0, 587, 589, 3, 94, 47, 0, 588, 587, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 89, 1, 0, 0, 0, 590, 591, 5, 65, 0, 0, 591, 592, 5, 11, 0, 0, 592, 593, 3, 104, 52, 0, 593, 91, 1, 0, 0, 0, 594, 595, 5, 62, 0, 0, 595, 596, 5, 11, 0, 0, 596, 597, 3, 78, 39, 0, 597, 93, 1, 0, 0, 0, 598, 599, 7, 8, 0, 0, 599, 600, 3, 96, 48, 0, 600, 95, 1, 0, 0, 0, 601, 608, 3, 98, 49, 0, 602, 603, 5, 9, 0, 0, 603, 604, 3, 98, 49, 0, 604, 605, 5, 2, 0, 0, 605, 606, 3, 98, 49, 0, 606, 608, 1, 0, 0, 0, 607, 601, 1, 0, 0, 0, 607, 602, 1, 0, 0, 0, 608, 97, 1, 0, 0, 0, 609, 610, 5, 18, 0, 0, 610, 622, 5, 73, 0, 0, 611, 612, 5, 90, 0, 0, 612, 622, 5, 66, 0, 0, 613, 614, 5, 90, 0, 0, 614, 622, 5, 30, 0, 0, 615, 616, 3, 138, 69, 0, 616, 617, 5, 66, 0, 0, 617, 622, 1, 0, 0, 0, 618, 619, 3, 138, 69, 0, 619, 620, 5, 30, 0, 0, 620, 622, 1, 0, 0, 0, 621, 609, 1, 0, 0, 0, 621, 611, 1, 0, 0, 0, 621, 613, 1, 0, 0, 0, 621, 615, 1, 0, 0, 0, 621, 618, 1, 0, 0, 0, 622, 99, 1, 0, 0, 0, 623, 624, 3, 106, 53, 0, 624, 625, 5, 0, 0, 1, 625, 101, 1, 0, 0, 0, 626, 674, 3, 150, 75, 0, 627, 628, 3, 150, 75, 0, 628, 629, 5, 126, 0, 0, 629, 630, 3, 150, 75, 0, 630, 637, 3, 102, 51, 0, 631, 632, 5, 112, 0, 0, 632, 633, 3, 150, 75, 0, 633, 634, 3, 102, 51, 0, 634, 636, 1, 0, 0, 0, 635, 631, 1, 0, 0, 0, 636, 639, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 637, 638, 1, 0, 0, 0, 638, 640, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 640, 641, 5, 144, 0, 0, 641, 674, 1, 0, 0, 0, 642, 643, 3, 150, 75, 0, 643, 644, 5, 126, 0, 0, 644, 649, 3, 152, 76, 0, 645, 646, 5, 112, 0, 0, 646, 648, 3, 152, 76, 0, 647, 645, 1, 0, 0, 0, 648, 651, 1, 0, 0, 0, 649, 647, 1, 0, 0, 0, 649, 650, 1, 0, 0, 0, 650, 652, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 652, 653, 5, 144, 0, 0, 653, 674, 1, 0, 0, 0, 654, 655, 3, 150, 75, 0, 655, 656, 5, 126, 0, 0, 656, 661, 3, 102, 51, 0, 657, 658, 5, 112, 0, 0, 658, 660, 3, 102, 51, 0, 659, 657, 1, 0, 0, 0, 660, 663, 1, 0, 0, 0, 661, 659, 1, 0, 0, 0, 661, 662, 1, 0, 0, 0, 662, 664, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 664, 665, 5, 144, 0, 0, 665, 674, 1, 0, 0, 0, 666, 667, 3, 150, 75, 0, 667, 669, 5, 126, 0, 0, 668, 670, 3, 104, 52, 0, 669, 668, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 672, 5, 144, 0, 0, 672, 674, 1, 0, 0, 0, 673, 626, 1, 0, 0, 0, 673, 627, 1, 0, 0, 0, 673, 642, 1, 0, 0, 0, 673, 654, 1, 0, 0, 0, 673, 666, 1, 0, 0, 0, 674, 103, 1, 0, 0, 0, 675, 680, 3, 106, 53, 0, 676, 677, 5, 112, 0, 0, 677, 679, 3, 106, 53, 0, 678, 676, 1, 0, 0, 0, 679, 682, 1, 0, 0, 0, 680, 678, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 681, 105, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 683, 684, 6, 53, -1, 0, 684, 686, 5, 12, 0, 0, 685, 687, 3, 106, 53, 0, 686, 685, 1, 0, 0, 0, 686, 687, 1, 0, 0, 0, 687, 693, 1, 0, 0, 0, 688, 689, 5, 94, 0, 0, 689, 690, 3, 106, 53, 0, 690, 691, 5, 81, 0, 0, 691, 692, 3, 106, 53, 0, 692, 694, 1, 0, 0, 0, 693, 688, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 693, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 699, 1, 0, 0, 0, 697, 698, 5, 24, 0, 0, 698, 700, 3, 106, 53, 0, 699, 697, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 702, 5, 25, 0, 0, 702, 833, 1, 0, 0, 0, 703, 704, 5, 13, 0, 0, 704, 705, 5, 126, 0, 0, 705, 706, 3, 106, 53, 0, 706, 707, 5, 6, 0, 0, 707, 708, 3, 102, 51, 0, 708, 709, 5, 144, 0, 0, 709, 833, 1, 0, 0, 0, 710, 711, 5, 19, 0, 0, 711, 833, 5, 106, 0, 0, 712, 713, 5, 43, 0, 0, 713, 714, 3, 106, 53, 0, 714, 715, 3, 142, 71, 0, 715, 833, 1, 0, 0, 0, 716, 717, 5, 80, 0, 0, 717, 718, 5, 126, 0, 0, 718, 719, 3, 106, 53, 0, 719, 720, 5, 32, 0, 0, 720, 723, 3, 106, 53, 0, 721, 722, 5, 31, 0, 0, 722, 724, 3, 106, 53, 0, 723, 721, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 144, 0, 0, 726, 833, 1, 0, 0, 0, 727, 728, 5, 83, 0, 0, 728, 833, 5, 106, 0, 0, 729, 730, 5, 88, 0, 0, 730, 731, 5, 126, 0, 0, 731, 732, 7, 9, 0, 0, 732, 733, 3, 156, 78, 0, 733, 734, 5, 32, 0, 0, 734, 735, 3, 106, 53, 0, 735, 736, 5, 144, 0, 0, 736, 833, 1, 0, 0, 0, 737, 738, 3, 150, 75, 0, 738, 740, 5, 126, 0, 0, 739, 741, 3, 104, 52, 0, 740, 739, 1, 0, 0, 0, 740, 741, 1, 0, 0, 0, 741, 742, 1, 0, 0, 0, 742, 743, 5, 144, 0, 0, 743, 752, 1, 0, 0, 0, 744, 746, 5, 126, 0, 0, 745, 747, 5, 23, 0, 0, 746, 745, 1, 0, 0, 0, 746, 747, 1, 0, 0, 0, 747, 749, 1, 0, 0, 0, 748, 750, 3, 108, 54, 0, 749, 748, 1, 0, 0, 0, 749, 750, 1, 0, 0, 0, 750, 751, 1, 0, 0, 0, 751, 753, 5, 144, 0, 0, 752, 744, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 754, 1, 0, 0, 0, 754, 755, 5, 64, 0, 0, 755, 756, 5, 126, 0, 0, 756, 757, 3, 88, 44, 0, 757, 758, 5, 144, 0, 0, 758, 833, 1, 0, 0, 0, 759, 760, 3, 150, 75, 0, 760, 762, 5, 126, 0, 0, 761, 763, 3, 104, 52, 0, 762, 761, 1, 0, 0, 0, 762, 763, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 765, 5, 144, 0, 0, 765, 774, 1, 0, 0, 0, 766, 768, 5, 126, 0, 0, 767, 769, 5, 23, 0, 0, 768, 767, 1, 0, 0, 0, 768, 769, 1, 0, 0, 0, 769, 771, 1, 0, 0, 0, 770, 772, 3, 108, 54, 0, 771, 770, 1, 0, 0, 0, 771, 772, 1, 0, 0, 0, 772, 773, 1, 0, 0, 0, 773, 775, 5, 144, 0, 0, 774, 766, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 776, 1, 0, 0, 0, 776, 777, 5, 64, 0, 0, 777, 778, 3, 150, 75, 0, 778, 833, 1, 0, 0, 0, 779, 785, 3, 150, 75, 0, 780, 782, 5, 126, 0, 0, 781, 783, 3, 104, 52, 0, 782, 781, 1, 0, 0, 0, 782, 783, 1, 0, 0, 0, 783, 784, 1, 0, 0, 0, 784, 786, 5, 144, 0, 0, 785, 780, 1, 0, 0, 0, 785, 786, 1, 0, 0, 0, 786, 787, 1, 0, 0, 0, 787, 789, 5, 126, 0, 0, 788, 790, 5, 23, 0, 0, 789, 788, 1, 0, 0, 0, 789, 790, 1, 0, 0, 0, 790, 792, 1, 0, 0, 0, 791, 793, 3, 108, 54, 0, 792, 791, 1, 0, 0, 0, 792, 793, 1, 0, 0, 0, 793, 794, 1, 0, 0, 0, 794, 795, 5, 144, 0, 0, 795, 833, 1, 0, 0, 0, 796, 833, 3, 114, 57, 0, 797, 833, 3, 158, 79, 0, 798, 833, 3, 140, 70, 0, 799, 800, 5, 114, 0, 0, 800, 833, 3, 106, 53, 19, 801, 802, 5, 56, 0, 0, 802, 833, 3, 106, 53, 13, 803, 804, 3, 130, 65, 0, 804, 805, 5, 116, 0, 0, 805, 807, 1, 0, 0, 0, 806, 803, 1, 0, 0, 0, 806, 807, 1, 0, 0, 0, 807, 808, 1, 0, 0, 0, 808, 833, 5, 108, 0, 0, 809, 810, 5, 126, 0, 0, 810, 811, 3, 34, 17, 0, 811, 812, 5, 144, 0, 0, 812, 833, 1, 0, 0, 0, 813, 814, 5, 126, 0, 0, 814, 815, 3, 106, 53, 0, 815, 816, 5, 144, 0, 0, 816, 833, 1, 0, 0, 0, 817, 818, 5, 126, 0, 0, 818, 819, 3, 104, 52, 0, 819, 820, 5, 144, 0, 0, 820, 833, 1, 0, 0, 0, 821, 823, 5, 125, 0, 0, 822, 824, 3, 104, 52, 0, 823, 822, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 825, 1, 0, 0, 0, 825, 833, 5, 143, 0, 0, 826, 828, 5, 124, 0, 0, 827, 829, 3, 30, 15, 0, 828, 827, 1, 0, 0, 0, 828, 829, 1, 0, 0, 0, 829, 830, 1, 0, 0, 0, 830, 833, 5, 142, 0, 0, 831, 833, 3, 122, 61, 0, 832, 683, 1, 0, 0, 0, 832, 703, 1, 0, 0, 0, 832, 710, 1, 0, 0, 0, 832, 712, 1, 0, 0, 0, 832, 716, 1, 0, 0, 0, 832, 727, 1, 0, 0, 0, 832, 729, 1, 0, 0, 0, 832, 737, 1, 0, 0, 0, 832, 759, 1, 0, 0, 0, 832, 779, 1, 0, 0, 0, 832, 796, 1, 0, 0, 0, 832, 797, 1, 0, 0, 0, 832, 798, 1, 0, 0, 0, 832, 799, 1, 0, 0, 0, 832, 801, 1, 0, 0, 0, 832, 806, 1, 0, 0, 0, 832, 809, 1, 0, 0, 0, 832, 813, 1, 0, 0, 0, 832, 817, 1, 0, 0, 0, 832, 821, 1, 0, 0, 0, 832, 826, 1, 0, 0, 0, 832, 831, 1, 0, 0, 0, 833, 927, 1, 0, 0, 0, 834, 838, 10, 18, 0, 0, 835, 839, 5, 108, 0, 0, 836, 839, 5, 146, 0, 0, 837, 839, 5, 133, 0, 0, 838, 835, 1, 0, 0, 0, 838, 836, 1, 0, 0, 0, 838, 837, 1, 0, 0, 0, 839, 840, 1, 0, 0, 0, 840, 926, 3, 106, 53, 19, 841, 845, 10, 17, 0, 0, 842, 846, 5, 134, 0, 0, 843, 846, 5, 114, 0, 0, 844, 846, 5, 113, 0, 0, 845, 842, 1, 0, 0, 0, 845, 843, 1, 0, 0, 0, 845, 844, 1, 0, 0, 0, 846, 847, 1, 0, 0, 0, 847, 926, 3, 106, 53, 18, 848, 873, 10, 16, 0, 0, 849, 874, 5, 117, 0, 0, 850, 874, 5, 118, 0, 0, 851, 874, 5, 129, 0, 0, 852, 874, 5, 127, 0, 0, 853, 874, 5, 128, 0, 0, 854, 874, 5, 119, 0, 0, 855, 874, 5, 120, 0, 0, 856, 858, 5, 56, 0, 0, 857, 856, 1, 0, 0, 0, 857, 858, 1, 0, 0, 0, 858, 859, 1, 0, 0, 0, 859, 861, 5, 40, 0, 0, 860, 862, 5, 14, 0, 0, 861, 860, 1, 0, 0, 0, 861, 862, 1, 0, 0, 0, 862, 874, 1, 0, 0, 0, 863, 865, 5, 56, 0, 0, 864, 863, 1, 0, 0, 0, 864, 865, 1, 0, 0, 0, 865, 866, 1, 0, 0, 0, 866, 874, 7, 10, 0, 0, 867, 874, 5, 140, 0, 0, 868, 874, 5, 141, 0, 0, 869, 874, 5, 131, 0, 0, 870, 874, 5, 122, 0, 0, 871, 874, 5, 123, 0, 0, 872, 874, 5, 130, 0, 0, 873, 849, 1, 0, 0, 0, 873, 850, 1, 0, 0, 0, 873, 851, 1, 0, 0, 0, 873, 852, 1, 0, 0, 0, 873, 853, 1, 0, 0, 0, 873, 854, 1, 0, 0, 0, 873, 855, 1, 0, 0, 0, 873, 857, 1, 0, 0, 0, 873, 864, 1, 0, 0, 0, 873, 867, 1, 0, 0, 0, 873, 868, 1, 0, 0, 0, 873, 869, 1, 0, 0, 0, 873, 870, 1, 0, 0, 0, 873, 871, 1, 0, 0, 0, 873, 872, 1, 0, 0, 0, 874, 875, 1, 0, 0, 0, 875, 926, 3, 106, 53, 17, 876, 877, 10, 14, 0, 0, 877, 878, 5, 132, 0, 0, 878, 926, 3, 106, 53, 15, 879, 880, 10, 12, 0, 0, 880, 881, 5, 2, 0, 0, 881, 926, 3, 106, 53, 13, 882, 883, 10, 11, 0, 0, 883, 884, 5, 61, 0, 0, 884, 926, 3, 106, 53, 12, 885, 887, 10, 10, 0, 0, 886, 888, 5, 56, 0, 0, 887, 886, 1, 0, 0, 0, 887, 888, 1, 0, 0, 0, 888, 889, 1, 0, 0, 0, 889, 890, 5, 9, 0, 0, 890, 891, 3, 106, 53, 0, 891, 892, 5, 2, 0, 0, 892, 893, 3, 106, 53, 11, 893, 926, 1, 0, 0, 0, 894, 895, 10, 9, 0, 0, 895, 896, 5, 135, 0, 0, 896, 897, 3, 106, 53, 0, 897, 898, 5, 111, 0, 0, 898, 899, 3, 106, 53, 9, 899, 926, 1, 0, 0, 0, 900, 901, 10, 22, 0, 0, 901, 902, 5, 125, 0, 0, 902, 903, 3, 106, 53, 0, 903, 904, 5, 143, 0, 0, 904, 926, 1, 0, 0, 0, 905, 906, 10, 21, 0, 0, 906, 907, 5, 116, 0, 0, 907, 926, 5, 104, 0, 0, 908, 909, 10, 20, 0, 0, 909, 910, 5, 116, 0, 0, 910, 926, 3, 150, 75, 0, 911, 912, 10, 15, 0, 0, 912, 914, 5, 44, 0, 0, 913, 915, 5, 56, 0, 0, 914, 913, 1, 0, 0, 0, 914, 915, 1, 0, 0, 0, 915, 916, 1, 0, 0, 0, 916, 926, 5, 57, 0, 0, 917, 923, 10, 8, 0, 0, 918, 924, 3, 148, 74, 0, 919, 920, 5, 6, 0, 0, 920, 924, 3, 150, 75, 0, 921, 922, 5, 6, 0, 0, 922, 924, 5, 106, 0, 0, 923, 918, 1, 0, 0, 0, 923, 919, 1, 0, 0, 0, 923, 921, 1, 0, 0, 0, 924, 926, 1, 0, 0, 0, 925, 834, 1, 0, 0, 0, 925, 841, 1, 0, 0, 0, 925, 848, 1, 0, 0, 0, 925, 876, 1, 0, 0, 0, 925, 879, 1, 0, 0, 0, 925, 882, 1, 0, 0, 0, 925, 885, 1, 0, 0, 0, 925, 894, 1, 0, 0, 0, 925, 900, 1, 0, 0, 0, 925, 905, 1, 0, 0, 0, 925, 908, 1, 0, 0, 0, 925, 911, 1, 0, 0, 0, 925, 917, 1, 0, 0, 0, 926, 929, 1, 0, 0, 0, 927, 925, 1, 0, 0, 0, 927, 928, 1, 0, 0, 0, 928, 107, 1, 0, 0, 0, 929, 927, 1, 0, 0, 0, 930, 935, 3, 110, 55, 0, 931, 932, 5, 112, 0, 0, 932, 934, 3, 110, 55, 0, 933, 931, 1, 0, 0, 0, 934, 937, 1, 0, 0, 0, 935, 933, 1, 0, 0, 0, 935, 936, 1, 0, 0, 0, 936, 109, 1, 0, 0, 0, 937, 935, 1, 0, 0, 0, 938, 941, 3, 112, 56, 0, 939, 941, 3, 106, 53, 0, 940, 938, 1, 0, 0, 0, 940, 939, 1, 0, 0, 0, 941, 111, 1, 0, 0, 0, 942, 943, 5, 126, 0, 0, 943, 948, 3, 150, 75, 0, 944, 945, 5, 112, 0, 0, 945, 947, 3, 150, 75, 0, 946, 944, 1, 0, 0, 0, 947, 950, 1, 0, 0, 0, 948, 946, 1, 0, 0, 0, 948, 949, 1, 0, 0, 0, 949, 951, 1, 0, 0, 0, 950, 948, 1, 0, 0, 0, 951, 952, 5, 144, 0, 0, 952, 962, 1, 0, 0, 0, 953, 958, 3, 150, 75, 0, 954, 955, 5, 112, 0, 0, 955, 957, 3, 150, 75, 0, 956, 954, 1, 0, 0, 0, 957, 960, 1, 0, 0, 0, 958, 956, 1, 0, 0, 0, 958, 959, 1, 0, 0, 0, 959, 962, 1, 0, 0, 0, 960, 958, 1, 0, 0, 0, 961, 942, 1, 0, 0, 0, 961, 953, 1, 0, 0, 0, 962, 963, 1, 0, 0, 0, 963, 964, 5, 107, 0, 0, 964, 965, 3, 106, 53, 0, 965, 113, 1, 0, 0, 0, 966, 967, 5, 128, 0, 0, 967, 971, 3, 150, 75, 0, 968, 970, 3, 116, 58, 0, 969, 968, 1, 0, 0, 0, 970, 973, 1, 0, 0, 0, 971, 969, 1, 0, 0, 0, 971, 972, 1, 0, 0, 0, 972, 974, 1, 0, 0, 0, 973, 971, 1, 0, 0, 0, 974, 975, 5, 146, 0, 0, 975, 976, 5, 120, 0, 0, 976, 995, 1, 0, 0, 0, 977, 978, 5, 128, 0, 0, 978, 982, 3, 150, 75, 0, 979, 981, 3, 116, 58, 0, 980, 979, 1, 0, 0, 0, 981, 984, 1, 0, 0, 0, 982, 980, 1, 0, 0, 0, 982, 983, 1, 0, 0, 0, 983, 985, 1, 0, 0, 0, 984, 982, 1, 0, 0, 0, 985, 987, 5, 120, 0, 0, 986, 988, 3, 114, 57, 0, 987, 986, 1, 0, 0, 0, 987, 988, 1, 0, 0, 0, 988, 989, 1, 0, 0, 0, 989, 990, 5, 128, 0, 0, 990, 991, 5, 146, 0, 0, 991, 992, 3, 150, 75, 0, 992, 993, 5, 120, 0, 0, 993, 995, 1, 0, 0, 0, 994, 966, 1, 0, 0, 0, 994, 977, 1, 0, 0, 0, 995, 115, 1, 0, 0, 0, 996, 997, 3, 150, 75, 0, 997, 998, 5, 118, 0, 0, 998, 999, 3, 156, 78, 0, 999, 1008, 1, 0, 0, 0, 1000, 1001, 3, 150, 75, 0, 1001, 1002, 5, 118, 0, 0, 1002, 1003, 5, 124, 0, 0, 1003, 1004, 3, 106, 53, 0, 1004, 1005, 5, 142, 0, 0, 1005, 1008, 1, 0, 0, 0, 1006, 1008, 3, 150, 75, 0, 1007, 996, 1, 0, 0, 0, 1007, 1000, 1, 0, 0, 0, 1007, 1006, 1, 0, 0, 0, 1008, 117, 1, 0, 0, 0, 1009, 1014, 3, 120, 60, 0, 1010, 1011, 5, 112, 0, 0, 1011, 1013, 3, 120, 60, 0, 1012, 1010, 1, 0, 0, 0, 1013, 1016, 1, 0, 0, 0, 1014, 1012, 1, 0, 0, 0, 1014, 1015, 1, 0, 0, 0, 1015, 119, 1, 0, 0, 0, 1016, 1014, 1, 0, 0, 0, 1017, 1018, 3, 150, 75, 0, 1018, 1019, 5, 6, 0, 0, 1019, 1020, 5, 126, 0, 0, 1020, 1021, 3, 34, 17, 0, 1021, 1022, 5, 144, 0, 0, 1022, 1028, 1, 0, 0, 0, 1023, 1024, 3, 106, 53, 0, 1024, 1025, 5, 6, 0, 0, 1025, 1026, 3, 150, 75, 0, 1026, 1028, 1, 0, 0, 0, 1027, 1017, 1, 0, 0, 0, 1027, 1023, 1, 0, 0, 0, 1028, 121, 1, 0, 0, 0, 1029, 1037, 3, 154, 77, 0, 1030, 1031, 3, 130, 65, 0, 1031, 1032, 5, 116, 0, 0, 1032, 1034, 1, 0, 0, 0, 1033, 1030, 1, 0, 0, 0, 1033, 1034, 1, 0, 0, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1037, 3, 124, 62, 0, 1036, 1029, 1, 0, 0, 0, 1036, 1033, 1, 0, 0, 0, 1037, 123, 1, 0, 0, 0, 1038, 1043, 3, 150, 75, 0, 1039, 1040, 5, 116, 0, 0, 1040, 1042, 3, 150, 75, 0, 1041, 1039, 1, 0, 0, 0, 1042, 1045, 1, 0, 0, 0, 1043, 1041, 1, 0, 0, 0, 1043, 1044, 1, 0, 0, 0, 1044, 125, 1, 0, 0, 0, 1045, 1043, 1, 0, 0, 0, 1046, 1047, 6, 63, -1, 0, 1047, 1056, 3, 130, 65, 0, 1048, 1056, 3, 128, 64, 0, 1049, 1050, 5, 126, 0, 0, 1050, 1051, 3, 34, 17, 0, 1051, 1052, 5, 144, 0, 0, 1052, 1056, 1, 0, 0, 0, 1053, 1056, 3, 114, 57, 0, 1054, 1056, 3, 154, 77, 0, 1055, 1046, 1, 0, 0, 0, 1055, 1048, 1, 0, 0, 0, 1055, 1049, 1, 0, 0, 0, 1055, 1053, 1, 0, 0, 0, 1055, 1054, 1, 0, 0, 0, 1056, 1065, 1, 0, 0, 0, 1057, 1061, 10, 3, 0, 0, 1058, 1062, 3, 148, 74, 0, 1059, 1060, 5, 6, 0, 0, 1060, 1062, 3, 150, 75, 0, 1061, 1058, 1, 0, 0, 0, 1061, 1059, 1, 0, 0, 0, 1062, 1064, 1, 0, 0, 0, 1063, 1057, 1, 0, 0, 0, 1064, 1067, 1, 0, 0, 0, 1065, 1063, 1, 0, 0, 0, 1065, 1066, 1, 0, 0, 0, 1066, 127, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1068, 1069, 3, 150, 75, 0, 1069, 1071, 5, 126, 0, 0, 1070, 1072, 3, 132, 66, 0, 1071, 1070, 1, 0, 0, 0, 1071, 1072, 1, 0, 0, 0, 1072, 1073, 1, 0, 0, 0, 1073, 1074, 5, 144, 0, 0, 1074, 129, 1, 0, 0, 0, 1075, 1076, 3, 134, 67, 0, 1076, 1077, 5, 116, 0, 0, 1077, 1079, 1, 0, 0, 0, 1078, 1075, 1, 0, 0, 0, 1078, 1079, 1, 0, 0, 0, 1079, 1080, 1, 0, 0, 0, 1080, 1081, 3, 150, 75, 0, 1081, 131, 1, 0, 0, 0, 1082, 1087, 3, 106, 53, 0, 1083, 1084, 5, 112, 0, 0, 1084, 1086, 3, 106, 53, 0, 1085, 1083, 1, 0, 0, 0, 1086, 1089, 1, 0, 0, 0, 1087, 1085, 1, 0, 0, 0, 1087, 1088, 1, 0, 0, 0, 1088, 133, 1, 0, 0, 0, 1089, 1087, 1, 0, 0, 0, 1090, 1091, 3, 150, 75, 0, 1091, 135, 1, 0, 0, 0, 1092, 1101, 5, 102, 0, 0, 1093, 1094, 5, 116, 0, 0, 1094, 1101, 7, 11, 0, 0, 1095, 1096, 5, 104, 0, 0, 1096, 1098, 5, 116, 0, 0, 1097, 1099, 7, 11, 0, 0, 1098, 1097, 1, 0, 0, 0, 1098, 1099, 1, 0, 0, 0, 1099, 1101, 1, 0, 0, 0, 1100, 1092, 1, 0, 0, 0, 1100, 1093, 1, 0, 0, 0, 1100, 1095, 1, 0, 0, 0, 1101, 137, 1, 0, 0, 0, 1102, 1104, 7, 12, 0, 0, 1103, 1102, 1, 0, 0, 0, 1103, 1104, 1, 0, 0, 0, 1104, 1111, 1, 0, 0, 0, 1105, 1112, 3, 136, 68, 0, 1106, 1112, 5, 103, 0, 0, 1107, 1112, 5, 104, 0, 0, 1108, 1112, 5, 105, 0, 0, 1109, 1112, 5, 41, 0, 0, 1110, 1112, 5, 55, 0, 0, 1111, 1105, 1, 0, 0, 0, 1111, 1106, 1, 0, 0, 0, 1111, 1107, 1, 0, 0, 0, 1111, 1108, 1, 0, 0, 0, 1111, 1109, 1, 0, 0, 0, 1111, 1110, 1, 0, 0, 0, 1112, 139, 1, 0, 0, 0, 1113, 1117, 3, 138, 69, 0, 1114, 1117, 5, 106, 0, 0, 1115, 1117, 5, 57, 0, 0, 1116, 1113, 1, 0, 0, 0, 1116, 1114, 1, 0, 0, 0, 1116, 1115, 1, 0, 0, 0, 1117, 141, 1, 0, 0, 0, 1118, 1119, 7, 13, 0, 0, 1119, 143, 1, 0, 0, 0, 1120, 1121, 7, 14, 0, 0, 1121, 145, 1, 0, 0, 0, 1122, 1123, 7, 15, 0, 0, 1123, 147, 1, 0, 0, 0, 1124, 1127, 5, 101, 0, 0, 1125, 1127, 3, 146, 73, 0, 1126, 1124, 1, 0, 0, 0, 1126, 1125, 1, 0, 0, 0, 1127, 149, 1, 0, 0, 0, 1128, 1132, 5, 101, 0, 0, 1129, 1132, 3, 142, 71, 0, 1130, 1132, 3, 144, 72, 0, 1131, 1128, 1, 0, 0, 0, 1131, 1129, 1, 0, 0, 0, 1131, 1130, 1, 0, 0, 0, 1132, 151, 1, 0, 0, 0, 1133, 1134, 3, 156, 78, 0, 1134, 1135, 5, 118, 0, 0, 1135, 1136, 3, 138, 69, 0, 1136, 153, 1, 0, 0, 0, 1137, 1138, 5, 124, 0, 0, 1138, 1139, 3, 150, 75, 0, 1139, 1140, 5, 142, 0, 0, 1140, 155, 1, 0, 0, 0, 1141, 1144, 5, 106, 0, 0, 1142, 1144, 3, 158, 79, 0, 1143, 1141, 1, 0, 0, 0, 1143, 1142, 1, 0, 0, 0, 1144, 157, 1, 0, 0, 0, 1145, 1149, 5, 137, 0, 0, 1146, 1148, 3, 160, 80, 0, 1147, 1146, 1, 0, 0, 0, 1148, 1151, 1, 0, 0, 0, 1149, 1147, 1, 0, 0, 0, 1149, 1150, 1, 0, 0, 0, 1150, 1152, 1, 0, 0, 0, 1151, 1149, 1, 0, 0, 0, 1152, 1153, 5, 139, 0, 0, 1153, 159, 1, 0, 0, 0, 1154, 1155, 5, 152, 0, 0, 1155, 1156, 3, 106, 53, 0, 1156, 1157, 5, 142, 0, 0, 1157, 1160, 1, 0, 0, 0, 1158, 1160, 5, 151, 0, 0, 1159, 1154, 1, 0, 0, 0, 1159, 1158, 1, 0, 0, 0, 1160, 161, 1, 0, 0, 0, 1161, 1165, 5, 138, 0, 0, 1162, 1164, 3, 164, 82, 0, 1163, 1162, 1, 0, 0, 0, 1164, 1167, 1, 0, 0, 0, 1165, 1163, 1, 0, 0, 0, 1165, 1166, 1, 0, 0, 0, 1166, 1168, 1, 0, 0, 0, 1167, 1165, 1, 0, 0, 0, 1168, 1169, 5, 0, 0, 1, 1169, 163, 1, 0, 0, 0, 1170, 1171, 5, 154, 0, 0, 1171, 1172, 3, 106, 53, 0, 1172, 1173, 5, 142, 0, 0, 1173, 1176, 1, 0, 0, 0, 1174, 1176, 5, 153, 0, 0, 1175, 1170, 1, 0, 0, 0, 1175, 1174, 1, 0, 0, 0, 1176, 165, 1, 0, 0, 0, 141, 169, 176, 185, 200, 212, 224, 240, 251, 265, 271, 281, 290, 293, 297, 300, 304, 307, 310, 313, 316, 320, 324, 327, 330, 333, 337, 340, 349, 355, 376, 393, 410, 416, 422, 433, 435, 446, 449, 455, 463, 469, 471, 475, 480, 483, 486, 490, 494, 497, 499, 502, 506, 510, 513, 515, 517, 522, 533, 539, 546, 551, 555, 559, 565, 567, 574, 582, 585, 588, 607, 621, 637, 649, 661, 669, 673, 680, 686, 695, 699, 723, 740, 746, 749, 752, 762, 768, 771, 774, 782, 785, 789, 792, 806, 823, 828, 832, 838, 845, 857, 861, 864, 873, 887, 914, 923, 925, 927, 935, 940, 948, 958, 961, 971, 982, 987, 994, 1007, 1014, 1027, 1033, 1036, 1043, 1055, 1061, 1065, 1071, 1078, 1087, 1098, 1100, 1103, 1111, 1116, 1126, 1131, 1143, 1149, 1159, 1165, 1175] \ No newline at end of file diff --git a/hogql_parser/parser.cpp b/hogql_parser/parser.cpp index 06e5ffb2e9d37..274aa741ae24a 100644 --- a/hogql_parser/parser.cpp +++ b/hogql_parser/parser.cpp @@ -1622,27 +1622,42 @@ class HogQLParseTreeConverter : public HogQLParserBaseVisitor { auto column_expr_list_ctx = ctx->columnExprList(); string name = visitAsString(ctx->identifier(0)); string over_identifier = visitAsString(ctx->identifier(1)); - PyObject* args = visitAsPyObjectOrEmptyList(column_expr_list_ctx); + PyObject* exprs = visitAsPyObjectOrEmptyList(column_expr_list_ctx); + PyObject* args; + try { + args = visitAsPyObjectOrEmptyList(ctx->columnArgList()); + } catch (...) { + Py_DECREF(exprs); + throw; + } RETURN_NEW_AST_NODE( - "WindowFunction", "{s:s#,s:N,s:s#}", "name", name.data(), name.size(), "args", args, "over_identifier", - over_identifier.data(), over_identifier.size() + "WindowFunction", "{s:s#,s:N,s:N,s:s#}", "name", name.data(), name.size(), "exprs", exprs, "args", args, + "over_identifier", over_identifier.data(), over_identifier.size() ); } VISIT(ColumnExprWinFunction) { string identifier = visitAsString(ctx->identifier()); auto column_expr_list_ctx = ctx->columnExprList(); - PyObject* args = visitAsPyObjectOrEmptyList(column_expr_list_ctx); + PyObject* exprs = visitAsPyObjectOrEmptyList(column_expr_list_ctx); + PyObject* args; + try { + args = visitAsPyObjectOrEmptyList(ctx->columnArgList()); + } catch (...) { + Py_DECREF(exprs); + throw; + } PyObject* over_expr; try { over_expr = visitAsPyObjectOrNone(ctx->windowExpr()); } catch (...) { + Py_DECREF(exprs); Py_DECREF(args); throw; } RETURN_NEW_AST_NODE( - "WindowFunction", "{s:s#,s:N,s:N}", "name", identifier.data(), identifier.size(), "args", args, "over_expr", - over_expr + "WindowFunction", "{s:s#,s:N,s:N,s:N}", "name", identifier.data(), identifier.size(), "exprs", exprs, + "args", args, "over_expr", over_expr ); } diff --git a/hogql_parser/setup.py b/hogql_parser/setup.py index 030b98ddb58be..ae4aff4cf8581 100644 --- a/hogql_parser/setup.py +++ b/hogql_parser/setup.py @@ -32,7 +32,7 @@ setup( name="hogql_parser", - version="1.0.11", + version="1.0.12", url="https://github.com/PostHog/posthog/tree/master/hogql_parser", author="PostHog Inc.", author_email="hey@posthog.com", diff --git a/hogvm/typescript/package.json b/hogvm/typescript/package.json index b7977c949edad..3080a86f544f9 100644 --- a/hogvm/typescript/package.json +++ b/hogvm/typescript/package.json @@ -1,9 +1,9 @@ { "name": "@posthog/hogvm", - "version": "1.0.10", - "description": "PostHog HogQL Virtual Machine", - "types": "dist/execute.d.ts", - "main": "dist/execute.js", + "version": "1.0.11", + "description": "PostHog Hog Virtual Machine", + "types": "dist/index.d.ts", + "main": "dist/index.js", "packageManager": "pnpm@8.3.1", "scripts": { "test": "jest --runInBand --forceExit", diff --git a/hogvm/typescript/src/execute.ts b/hogvm/typescript/src/execute.ts index 0bb1c0b5c81d5..4101d64f69d1b 100644 --- a/hogvm/typescript/src/execute.ts +++ b/hogvm/typescript/src/execute.ts @@ -1,6 +1,6 @@ import { Operation } from './operation' import { ASYNC_STL, STL } from './stl/stl' -import { convertJSToHog, getNestedValue, like, setNestedValue } from './utils' +import { convertHogToJS, convertJSToHog, getNestedValue, like, setNestedValue } from './utils' const DEFAULT_MAX_ASYNC_STEPS = 100 const DEFAULT_TIMEOUT = 5 // seconds @@ -58,8 +58,10 @@ export async function execAsync(bytecode: any[], options?: ExecOptions): Promise if (response.state && response.asyncFunctionName && response.asyncFunctionArgs) { vmState = response.state if (options?.asyncFunctions && response.asyncFunctionName in options.asyncFunctions) { - const result = await options?.asyncFunctions[response.asyncFunctionName](...response.asyncFunctionArgs) - vmState.stack.push(result) + const result = await options?.asyncFunctions[response.asyncFunctionName]( + ...response.asyncFunctionArgs.map(convertHogToJS) + ) + vmState.stack.push(convertJSToHog(result)) } else if (response.asyncFunctionName in ASYNC_STL) { const result = await ASYNC_STL[response.asyncFunctionName]( response.asyncFunctionArgs, @@ -333,7 +335,7 @@ export function exec(code: any[] | VMState, options?: ExecOptions): ExecResult { .fill(null) .map(() => popStack()) if (options?.functions && options.functions[name] && name !== 'toString') { - stack.push(options.functions[name](...args)) + stack.push(convertJSToHog(options.functions[name](...args.map(convertHogToJS)))) } else if ( name !== 'toString' && ((options?.asyncFunctions && options.asyncFunctions[name]) || name in ASYNC_STL) diff --git a/latest_migrations.manifest b/latest_migrations.manifest index 32da32018dacd..905aeb627006b 100644 --- a/latest_migrations.manifest +++ b/latest_migrations.manifest @@ -5,7 +5,7 @@ contenttypes: 0002_remove_content_type_name ee: 0016_rolemembership_organization_member otp_static: 0002_throttling otp_totp: 0002_auto_20190420_0723 -posthog: 0424_survey_current_iteration_and_more +posthog: 0426_externaldatasource_sync_frequency sessions: 0001_initial social_django: 0010_uid_db_index two_factor: 0007_auto_20201201_1019 diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 6a12d0a8cd137..d3e4f2d3fc605 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -3,6 +3,45 @@ posthog/temporal/common/utils.py:0: note: This is likely because "from_activity" posthog/temporal/common/utils.py:0: error: Argument 2 to "__get__" of "classmethod" has incompatible type "type[HeartbeatType]"; expected "type[Never]" [arg-type] posthog/warehouse/models/ssh_tunnel.py:0: error: Incompatible types in assignment (expression has type "NoEncryption", variable has type "BestAvailableEncryption") [assignment] posthog/temporal/data_imports/pipelines/zendesk/talk_api.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Dict entry 2 has incompatible type "Literal['auto']": "None"; expected "Literal['json_response', 'header_link', 'auto', 'single_page', 'cursor', 'offset', 'page_number']": "type[BasePaginator]" [dict-item] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "AuthConfigBase") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Argument 1 to "get_auth_class" has incompatible type "Literal['bearer', 'api_key', 'http_basic'] | None"; expected "Literal['bearer', 'api_key', 'http_basic']" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Need type annotation for "dependency_graph" [var-annotated] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Incompatible types in assignment (expression has type "None", target has type "ResolvedParam") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Incompatible return value type (got "tuple[TopologicalSorter[Any], dict[str, EndpointResource], dict[str, ResolvedParam]]", expected "tuple[Any, dict[str, EndpointResource], dict[str, ResolvedParam | None]]") [return-value] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unsupported right operand type for in ("str | Endpoint | None") [operator] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Value of type variable "StrOrLiteralStr" of "parse" of "Formatter" cannot be "str | None" [type-var] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unsupported right operand type for in ("dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None") [operator] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unsupported right operand type for in ("dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None") [operator] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Value of type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None" is not indexable [index] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Item "None" of "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None" has no attribute "pop" [union-attr] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Value of type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None" is not indexable [index] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Item "None" of "str | None" has no attribute "format" [union-attr] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Argument 1 to "single_entity_path" has incompatible type "str | None"; expected "str" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Item "None" of "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None" has no attribute "items" [union-attr] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "str") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "str") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Statement is unreachable [unreachable] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unpacked dict entry 0 has incompatible type "dict[str, Any] | None"; expected "SupportsKeysAndGetItem[str, Any]" [dict-item] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unpacked dict entry 1 has incompatible type "dict[str, Any] | None"; expected "SupportsKeysAndGetItem[str, Any]" [dict-item] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unpacked dict entry 0 has incompatible type "dict[str, Any] | None"; expected "SupportsKeysAndGetItem[str, ResolveParamConfig | IncrementalParamConfig | Any]" [dict-item] +posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unpacked dict entry 1 has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "SupportsKeysAndGetItem[str, ResolveParamConfig | IncrementalParamConfig | Any]" [dict-item] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Not all union combinations were tried because there are too many unions [misc] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 2 to "source" has incompatible type "str | None"; expected "str" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 3 to "source" has incompatible type "str | None"; expected "str" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 4 to "source" has incompatible type "int | None"; expected "int" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 6 to "source" has incompatible type "Schema | None"; expected "Schema" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 7 to "source" has incompatible type "Literal['evolve', 'discard_value', 'freeze', 'discard_row'] | TSchemaContractDict | None"; expected "Literal['evolve', 'discard_value', 'freeze', 'discard_row'] | TSchemaContractDict" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 8 to "source" has incompatible type "type[BaseConfiguration] | None"; expected "type[BaseConfiguration]" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "build_resource_dependency_graph" has incompatible type "EndpointResourceBase | None"; expected "EndpointResourceBase" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Need type annotation for "resources" (hint: "resources: dict[, ] = ...") [var-annotated] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible types in assignment (expression has type "ResolvedParam | None", variable has type "ResolvedParam") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible types in assignment (expression has type "list[str] | None", variable has type "list[str]") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "setup_incremental_object" has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "dict[str, Any]" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Statement is unreachable [unreachable] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "exclude_keys" has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "Mapping[str, Any]" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible default for argument "incremental_param" (default has type "IncrementalParam | None", argument has type "IncrementalParam") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument "module" to "SourceInfo" has incompatible type Module | None; expected Module [arg-type] posthog/hogql/database/schema/numbers.py:0: error: Incompatible types in assignment (expression has type "dict[str, IntegerDatabaseField]", variable has type "dict[str, FieldOrTable]") [assignment] posthog/hogql/database/schema/numbers.py:0: note: "Dict" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/numbers.py:0: note: Consider using "Mapping" instead, which is covariant in the value type @@ -700,7 +739,6 @@ posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: posthog/temporal/tests/batch_exports/test_snowflake_batch_export_workflow.py:0: error: List item 0 has incompatible type "tuple[str, str, int, int, int, int, str, int]"; expected "tuple[str, str, int, int, str, str, str, str]" [list-item] posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py:0: error: "tuple[Any, ...]" has no attribute "last_uploaded_part_timestamp" [attr-defined] posthog/temporal/tests/batch_exports/test_s3_batch_export_workflow.py:0: error: "tuple[Any, ...]" has no attribute "upload_state" [attr-defined] -posthog/temporal/data_imports/pipelines/test/test_pipeline.py:0: error: Argument "run_id" to "PipelineInputs" has incompatible type "UUID"; expected "str" [arg-type] posthog/migrations/0237_remove_timezone_from_teams.py:0: error: Argument 2 to "RunPython" has incompatible type "Callable[[Migration, Any], None]"; expected "_CodeCallable | None" [arg-type] posthog/migrations/0228_fix_tile_layouts.py:0: error: Argument 2 to "RunPython" has incompatible type "Callable[[Migration, Any], None]"; expected "_CodeCallable | None" [arg-type] posthog/api/plugin_log_entry.py:0: error: Name "timezone.datetime" is not defined [name-defined] @@ -709,11 +747,6 @@ posthog/api/plugin_log_entry.py:0: error: Name "timezone.datetime" is not define posthog/api/plugin_log_entry.py:0: error: Module "django.utils.timezone" does not explicitly export attribute "datetime" [attr-defined] posthog/api/action.py:0: error: Argument 1 to has incompatible type "*tuple[str, ...]"; expected "type[BaseRenderer]" [arg-type] posthog/temporal/tests/batch_exports/test_redshift_batch_export_workflow.py:0: error: Incompatible types in assignment (expression has type "str | int", variable has type "int") [assignment] -posthog/temporal/tests/external_data/test_external_data_job.py:0: error: Argument "run_id" to "ImportDataActivityInputs" has incompatible type "UUID"; expected "str" [arg-type] -posthog/temporal/tests/external_data/test_external_data_job.py:0: error: Argument "run_id" to "ImportDataActivityInputs" has incompatible type "UUID"; expected "str" [arg-type] -posthog/temporal/tests/external_data/test_external_data_job.py:0: error: Argument "run_id" to "ImportDataActivityInputs" has incompatible type "UUID"; expected "str" [arg-type] -posthog/temporal/tests/external_data/test_external_data_job.py:0: error: Argument "run_id" to "ImportDataActivityInputs" has incompatible type "UUID"; expected "str" [arg-type] -posthog/temporal/tests/external_data/test_external_data_job.py:0: error: Argument "run_id" to "ImportDataActivityInputs" has incompatible type "UUID"; expected "str" [arg-type] posthog/api/test/batch_exports/conftest.py:0: error: Argument "activities" to "ThreadedWorker" has incompatible type "list[function]"; expected "Sequence[Callable[..., Any]]" [arg-type] posthog/api/test/test_team.py:0: error: "HttpResponse" has no attribute "json" [attr-defined] posthog/api/test/test_team.py:0: error: "HttpResponse" has no attribute "json" [attr-defined] diff --git a/package.json b/package.json index f075f3dabba46..63f1cdcce3688 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "pmtiles": "^2.11.0", "postcss": "^8.4.31", "postcss-preset-env": "^9.3.0", - "posthog-js": "1.138.2", + "posthog-js": "1.139.0", "posthog-js-lite": "3.0.0", "prettier": "^2.8.8", "prop-types": "^15.7.2", diff --git a/plugin-server/package.json b/plugin-server/package.json index e5766fa3d44f5..3344291ce0d1d 100644 --- a/plugin-server/package.json +++ b/plugin-server/package.json @@ -50,6 +50,7 @@ "@google-cloud/storage": "^5.8.5", "@maxmind/geoip2-node": "^3.4.0", "@posthog/clickhouse": "^1.7.0", + "@posthog/hogvm": "^1.0.11", "@posthog/plugin-scaffold": "1.4.4", "@sentry/node": "^7.49.0", "@sentry/profiling-node": "^0.3.0", diff --git a/plugin-server/pnpm-lock.yaml b/plugin-server/pnpm-lock.yaml index 05fb5a15d84bd..af85a11df6436 100644 --- a/plugin-server/pnpm-lock.yaml +++ b/plugin-server/pnpm-lock.yaml @@ -43,6 +43,9 @@ dependencies: '@posthog/clickhouse': specifier: ^1.7.0 version: 1.7.0 + '@posthog/hogvm': + specifier: ^1.0.11 + version: 1.0.11 '@posthog/plugin-scaffold': specifier: 1.4.4 version: 1.4.4 @@ -3104,6 +3107,10 @@ packages: engines: {node: '>=12'} dev: false + /@posthog/hogvm@1.0.11: + resolution: {integrity: sha512-W1m4UPmpaNwm9+Rwpb3rjuZd3z+/gO9MsxibCnxdTndrFgIrNjGOas2ZEpZqJblV3sgubFbGq6IXdORbM+nv5w==} + dev: false + /@posthog/plugin-scaffold@1.4.4: resolution: {integrity: sha512-3z1ENm1Ys5lEQil0H7TVOqHvD24+ydiZFk5hggpbHRx1iOxAK+Eu5qFyAROwPUcCo7NOYjmH2xL1C4B1vaHilg==} dependencies: diff --git a/plugin-server/src/capabilities.ts b/plugin-server/src/capabilities.ts index cda8da5b20abd..47d30482bd72d 100644 --- a/plugin-server/src/capabilities.ts +++ b/plugin-server/src/capabilities.ts @@ -23,6 +23,7 @@ export function getPluginServerCapabilities(config: PluginsServerConfig): Plugin personOverrides: true, appManagementSingleton: true, preflightSchedules: true, + cdpProcessedEvents: true, ...sharedCapabilities, } case PluginServerMode.ingestion: @@ -87,5 +88,11 @@ export function getPluginServerCapabilities(config: PluginsServerConfig): Plugin personOverrides: true, ...sharedCapabilities, } + + case PluginServerMode.cdp_processed_events: + return { + cdpProcessedEvents: true, + ...sharedCapabilities, + } } } diff --git a/plugin-server/src/config/config.ts b/plugin-server/src/config/config.ts index f3584ded83880..3ab2cb79a536d 100644 --- a/plugin-server/src/config/config.ts +++ b/plugin-server/src/config/config.ts @@ -137,7 +137,6 @@ export function getDefaultConfig(): PluginsServerConfig { RUSTY_HOOK_ROLLOUT_PERCENTAGE: 0, RUSTY_HOOK_URL: '', CAPTURE_CONFIG_REDIS_HOST: null, - LAZY_PERSON_CREATION_TEAMS: '', STARTUP_PROFILE_DURATION_SECONDS: 300, // 5 minutes STARTUP_PROFILE_CPU: false, diff --git a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts index 8d7f2d79da3b9..3be63ed2bb0c6 100644 --- a/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts +++ b/plugin-server/src/main/ingestion-queues/session-recording/session-recordings-consumer.ts @@ -333,11 +333,13 @@ export class SessionRecordingIngester { } public async handleEachBatch(messages: Message[], heartbeat: () => void): Promise { - status.info('🔁', `blob_ingester_consumer - handling batch`, { - size: messages.length, - partitionsInBatch: [...new Set(messages.map((x) => x.partition))], - assignedPartitions: this.assignedPartitions, - }) + if (messages.length !== 0) { + status.info('🔁', `blob_ingester_consumer - handling batch`, { + size: messages.length, + partitionsInBatch: [...new Set(messages.map((x) => x.partition))], + assignedPartitions: this.assignedPartitions, + }) + } await runInstrumentedFunction({ statsKey: `recordingingester.handleEachBatch`, sendTimeoutGuardToSentry: false, diff --git a/plugin-server/src/main/pluginsServer.ts b/plugin-server/src/main/pluginsServer.ts index 3a1ba51f87992..4cc219522003f 100644 --- a/plugin-server/src/main/pluginsServer.ts +++ b/plugin-server/src/main/pluginsServer.ts @@ -10,7 +10,8 @@ import { Counter } from 'prom-client' import v8Profiler from 'v8-profiler-next' import { getPluginServerCapabilities } from '../capabilities' -import { buildIntegerMatcher, defaultConfig, sessionRecordingConsumerConfig } from '../config/config' +import { CdpProcessedEventsConsumer } from '../cdp/cdp-processed-events-consumer' +import { defaultConfig, sessionRecordingConsumerConfig } from '../config/config' import { Hub, PluginServerCapabilities, PluginsServerConfig } from '../types' import { createHub, createKafkaClient, createKafkaProducerWrapper } from '../utils/db/hub' import { PostgresRouter } from '../utils/db/postgres' @@ -105,6 +106,8 @@ export async function startPluginsServer( let onEventHandlerConsumer: KafkaJSIngestionConsumer | undefined let stopWebhooksHandlerConsumer: () => Promise | undefined + const shutdownCallbacks: (() => Promise)[] = [] + // Kafka consumer. Handles events that we couldn't find an existing person // to associate. The buffer handles delaying the ingestion of these events // (default 60 seconds) to allow for the person to be created in the @@ -157,6 +160,7 @@ export async function startPluginsServer( stopSessionRecordingBlobOverflowConsumer?.(), schedulerTasksConsumer?.disconnect(), personOverridesPeriodicTask?.stop(), + ...shutdownCallbacks.map((cb) => cb()), ]) if (piscina) { @@ -370,14 +374,7 @@ export async function startPluginsServer( const teamManager = hub?.teamManager ?? new TeamManager(postgres, serverConfig) const organizationManager = hub?.organizationManager ?? new OrganizationManager(postgres, teamManager) const KafkaProducerWrapper = hub?.kafkaProducer ?? (await createKafkaProducerWrapper(serverConfig)) - const rustyHook = - hub?.rustyHook ?? - new RustyHook( - buildIntegerMatcher(serverConfig.RUSTY_HOOK_FOR_TEAMS, true), - serverConfig.RUSTY_HOOK_ROLLOUT_PERCENTAGE, - serverConfig.RUSTY_HOOK_URL, - serverConfig.EXTERNAL_REQUEST_TIMEOUT_MS - ) + const rustyHook = hub?.rustyHook ?? new RustyHook(serverConfig) const appMetrics = hub?.appMetrics ?? new AppMetrics( @@ -494,6 +491,21 @@ export async function startPluginsServer( } } + if (capabilities.cdpProcessedEvents) { + ;[hub, closeHub] = hub ? [hub, closeHub] : await createHub(serverConfig, capabilities) + const consumer = new CdpProcessedEventsConsumer(serverConfig, hub) + await consumer.start() + + if (consumer.batchConsumer) { + shutdownOnConsumerExit(consumer.batchConsumer) + } + + shutdownCallbacks.push(async () => { + await consumer.stop() + }) + healthChecks['cdp-processed-events'] = () => consumer.isHealthy() ?? false + } + if (capabilities.personOverrides) { const postgres = hub?.postgres ?? new PostgresRouter(serverConfig) const kafkaProducer = hub?.kafkaProducer ?? (await createKafkaProducerWrapper(serverConfig)) diff --git a/plugin-server/src/types.ts b/plugin-server/src/types.ts index 7fffd6930bac2..78996b2a4fad9 100644 --- a/plugin-server/src/types.ts +++ b/plugin-server/src/types.ts @@ -81,6 +81,7 @@ export enum PluginServerMode { recordings_blob_ingestion = 'recordings-blob-ingestion', recordings_blob_ingestion_overflow = 'recordings-blob-ingestion-overflow', person_overrides = 'person-overrides', + cdp_processed_events = 'cdp-processed-events', } export const stringToPluginServerMode = Object.fromEntries( @@ -211,7 +212,6 @@ export interface PluginsServerConfig { SKIP_UPDATE_EVENT_AND_PROPERTIES_STEP: boolean PIPELINE_STEP_STALLED_LOG_TIMEOUT: number CAPTURE_CONFIG_REDIS_HOST: string | null // Redis cluster to use to coordinate with capture (overflow, routing) - LAZY_PERSON_CREATION_TEAMS: string // dump profiles to disk, covering the first N seconds of runtime STARTUP_PROFILE_DURATION_SECONDS: number @@ -298,7 +298,6 @@ export interface Hub extends PluginsServerConfig { pluginConfigsToSkipElementsParsing: ValueMatcher poeEmbraceJoinForTeams: ValueMatcher poeWritesExcludeTeams: ValueMatcher - lazyPersonCreationTeams: ValueMatcher // lookups eventsToDropByToken: Map } @@ -315,6 +314,7 @@ export interface PluginServerCapabilities { processAsyncWebhooksHandlers?: boolean sessionRecordingBlobIngestion?: boolean sessionRecordingBlobOverflowIngestion?: boolean + cdpProcessedEvents?: boolean personOverrides?: boolean appManagementSingleton?: boolean preflightSchedules?: boolean // Used for instance health checks on hobby deploy, not useful on cloud diff --git a/plugin-server/src/utils/db/db.ts b/plugin-server/src/utils/db/db.ts index 500044a815e90..a7cd6d0b23dd9 100644 --- a/plugin-server/src/utils/db/db.ts +++ b/plugin-server/src/utils/db/db.ts @@ -754,20 +754,14 @@ export class DB { personUpdateVersionMismatchCounter.inc() } - const kafkaMessages = [] - const message = generateKafkaPersonUpdateMessage(updatedPerson) - if (tx) { - kafkaMessages.push(message) - } else { - await this.kafkaProducer.queueMessage({ kafkaMessage: message, waitForAck: true }) - } + const kafkaMessage = generateKafkaPersonUpdateMessage(updatedPerson) status.debug( '🧑‍🦰', `Updated person ${updatedPerson.uuid} of team ${updatedPerson.team_id} to version ${updatedPerson.version}.` ) - return [updatedPerson, kafkaMessages] + return [updatedPerson, [kafkaMessage]] } public async deletePerson(person: InternalPerson, tx?: TransactionClient): Promise { diff --git a/plugin-server/src/utils/db/hub.ts b/plugin-server/src/utils/db/hub.ts index 331f95c95d6bb..3feaf4cd63c63 100644 --- a/plugin-server/src/utils/db/hub.ts +++ b/plugin-server/src/utils/db/hub.ts @@ -146,12 +146,7 @@ export async function createHub( const organizationManager = new OrganizationManager(postgres, teamManager) const pluginsApiKeyManager = new PluginsApiKeyManager(db) const rootAccessManager = new RootAccessManager(db) - const rustyHook = new RustyHook( - buildIntegerMatcher(serverConfig.RUSTY_HOOK_FOR_TEAMS, true), - serverConfig.RUSTY_HOOK_ROLLOUT_PERCENTAGE, - serverConfig.RUSTY_HOOK_URL, - serverConfig.EXTERNAL_REQUEST_TIMEOUT_MS - ) + const rustyHook = new RustyHook(serverConfig) const actionManager = new ActionManager(postgres, serverConfig) const actionMatcher = new ActionMatcher(postgres, actionManager, teamManager) @@ -209,7 +204,6 @@ export async function createHub( pluginConfigsToSkipElementsParsing: buildIntegerMatcher(process.env.SKIP_ELEMENTS_PARSING_PLUGINS, true), poeEmbraceJoinForTeams: buildIntegerMatcher(process.env.POE_EMBRACE_JOIN_FOR_TEAMS, true), poeWritesExcludeTeams: buildIntegerMatcher(process.env.POE_WRITES_EXCLUDE_TEAMS, false), - lazyPersonCreationTeams: buildIntegerMatcher(process.env.LAZY_PERSON_CREATION_TEAMS, true), eventsToDropByToken: createEventsToDropByToken(process.env.DROP_EVENTS_BY_TOKEN_DISTINCT_ID), } diff --git a/plugin-server/src/worker/ingestion/event-pipeline/processPersonsStep.ts b/plugin-server/src/worker/ingestion/event-pipeline/processPersonsStep.ts index a0978497d7e34..377981fe64b09 100644 --- a/plugin-server/src/worker/ingestion/event-pipeline/processPersonsStep.ts +++ b/plugin-server/src/worker/ingestion/event-pipeline/processPersonsStep.ts @@ -10,22 +10,21 @@ export async function processPersonsStep( event: PluginEvent, timestamp: DateTime, processPerson: boolean -): Promise<[PluginEvent, Person]> { +): Promise<[PluginEvent, Person, Promise]> { let overridesWriter: DeferredPersonOverrideWriter | undefined = undefined if (runner.poEEmbraceJoin) { overridesWriter = new DeferredPersonOverrideWriter(runner.hub.db.postgres) } - const person = await new PersonState( + const [person, kafkaAck] = await new PersonState( event, event.team_id, String(event.distinct_id), timestamp, processPerson, runner.hub.db, - runner.hub.lazyPersonCreationTeams(event.team_id), overridesWriter ).update() - return [event, person] + return [event, person, kafkaAck] } diff --git a/plugin-server/src/worker/ingestion/event-pipeline/runner.ts b/plugin-server/src/worker/ingestion/event-pipeline/runner.ts index 52e762949a924..26c645e5089ca 100644 --- a/plugin-server/src/worker/ingestion/event-pipeline/runner.ts +++ b/plugin-server/src/worker/ingestion/event-pipeline/runner.ts @@ -175,19 +175,17 @@ export class EventPipelineRunner { } if (event.event === '$$client_ingestion_warning') { - kafkaAcks.push( - captureIngestionWarning( - this.hub.db.kafkaProducer, - event.team_id, - 'client_ingestion_warning', - { - eventUuid: event.uuid, - event: event.event, - distinctId: event.distinct_id, - message: event.properties?.$$client_ingestion_warning_message, - }, - { alwaysSend: true } - ) + await captureIngestionWarning( + this.hub.db.kafkaProducer, + event.team_id, + 'client_ingestion_warning', + { + eventUuid: event.uuid, + event: event.event, + distinctId: event.distinct_id, + message: event.properties?.$$client_ingestion_warning_message, + }, + { alwaysSend: true } ) return this.registerLastStep('clientIngestionWarning', [event], kafkaAcks) @@ -205,11 +203,12 @@ export class EventPipelineRunner { event.team_id ) - const [postPersonEvent, person] = await this.runStep( + const [postPersonEvent, person, personKafkaAck] = await this.runStep( processPersonsStep, [this, normalizedEvent, timestamp, processPerson], event.team_id ) + kafkaAcks.push(personKafkaAck) const preparedEvent = await this.runStep( prepareEventStep, diff --git a/plugin-server/src/worker/ingestion/person-state.ts b/plugin-server/src/worker/ingestion/person-state.ts index 3f9cfa2f9d3b6..d3bf32e21310b 100644 --- a/plugin-server/src/worker/ingestion/person-state.ts +++ b/plugin-server/src/worker/ingestion/person-state.ts @@ -92,7 +92,6 @@ export class PersonState { private timestamp: DateTime, private processPerson: boolean, // $process_person_profile flag from the event private db: DB, - private lazyPersonCreation: boolean, private personOverrideWriter?: DeferredPersonOverrideWriter ) { this.eventProperties = event.properties! @@ -102,68 +101,62 @@ export class PersonState { this.updateIsIdentified = false } - async update(): Promise { + async update(): Promise<[Person, Promise]> { if (!this.processPerson) { - if (this.lazyPersonCreation) { - const existingPerson = await this.db.fetchPerson(this.teamId, this.distinctId, { useReadReplica: true }) - if (existingPerson) { - const person = existingPerson as Person - - // Ensure person properties don't propagate elsewhere, such as onto the event itself. - person.properties = {} - - if (this.timestamp > person.created_at.plus({ minutes: 1 })) { - // See documentation on the field. - // - // Note that we account for timestamp vs person creation time (with a little - // padding for good measure) to account for ingestion lag. It's possible for - // events to be processed after person creation even if they were sent prior - // to person creation, and the user did nothing wrong in that case. - person.force_upgrade = true - } - - return person - } - - // We need a value from the `person_created_column` in ClickHouse. This should be - // hidden from users for events without a real person, anyway. It's slightly offset - // from the 0 date (by 5 seconds) in order to assist in debugging by being - // harmlessly distinct from Unix UTC "0". - const createdAt = DateTime.utc(1970, 1, 1, 0, 0, 5) - - const fakePerson: Person = { - team_id: this.teamId, - properties: {}, - uuid: uuidFromDistinctId(this.teamId, this.distinctId), - created_at: createdAt, - } - return fakePerson - } else { - // We don't need to handle any properties for `processPerson=false` events, so we can - // short circuit by just finding or creating a person and returning early. - const [person, _] = await promiseRetry(() => this.createOrGetPerson(), 'get_person_personless') + const existingPerson = await this.db.fetchPerson(this.teamId, this.distinctId, { useReadReplica: true }) + if (existingPerson) { + const person = existingPerson as Person // Ensure person properties don't propagate elsewhere, such as onto the event itself. person.properties = {} - return person + if (this.timestamp > person.created_at.plus({ minutes: 1 })) { + // See documentation on the field. + // + // Note that we account for timestamp vs person creation time (with a little + // padding for good measure) to account for ingestion lag. It's possible for + // events to be processed after person creation even if they were sent prior + // to person creation, and the user did nothing wrong in that case. + person.force_upgrade = true + } + + return [person, Promise.resolve()] } + + // We need a value from the `person_created_column` in ClickHouse. This should be + // hidden from users for events without a real person, anyway. It's slightly offset + // from the 0 date (by 5 seconds) in order to assist in debugging by being + // harmlessly distinct from Unix UTC "0". + const createdAt = DateTime.utc(1970, 1, 1, 0, 0, 5) + + const fakePerson: Person = { + team_id: this.teamId, + properties: {}, + uuid: uuidFromDistinctId(this.teamId, this.distinctId), + created_at: createdAt, + } + return [fakePerson, Promise.resolve()] } - const person: InternalPerson | undefined = await this.handleIdentifyOrAlias() // TODO: make it also return a boolean for if we can exit early here + const [person, identifyOrAliasKafkaAck]: [InternalPerson | undefined, Promise] = + await this.handleIdentifyOrAlias() // TODO: make it also return a boolean for if we can exit early here + if (person) { // try to shortcut if we have the person from identify or alias try { - return await this.updatePersonProperties(person) + const [updatedPerson, updateKafkaAck] = await this.updatePersonProperties(person) + return [updatedPerson, Promise.all([identifyOrAliasKafkaAck, updateKafkaAck]).then(() => undefined)] } catch (error) { // shortcut didn't work, swallow the error and try normal retry loop below status.debug('🔁', `failed update after adding distinct IDs, retrying`, { error }) } } - return await this.handleUpdate() + + const [updatedPerson, updateKafkaAck] = await this.handleUpdate() + return [updatedPerson, Promise.all([identifyOrAliasKafkaAck, updateKafkaAck]).then(() => undefined)] } - async handleUpdate(): Promise { + async handleUpdate(): Promise<[InternalPerson, Promise]> { // There are various reasons why update can fail: // - anothe thread created the person during a race // - the person might have been merged between start of processing and now @@ -171,10 +164,10 @@ export class PersonState { return await promiseRetry(() => this.updateProperties(), 'update_person') } - async updateProperties(): Promise { + async updateProperties(): Promise<[InternalPerson, Promise]> { const [person, propertiesHandled] = await this.createOrGetPerson() if (propertiesHandled) { - return person + return [person, Promise.resolve()] } return await this.updatePersonProperties(person) } @@ -251,7 +244,7 @@ export class PersonState { ) } - private async updatePersonProperties(person: InternalPerson): Promise { + private async updatePersonProperties(person: InternalPerson): Promise<[InternalPerson, Promise]> { person.properties ||= {} const update: Partial = {} @@ -263,10 +256,12 @@ export class PersonState { } if (Object.keys(update).length > 0) { - // Note: we're not passing the client, so kafka messages are waited for within the function - ;[person] = await this.db.updatePersonDeprecated(person, update) + const [updatedPerson, kafkaMessages] = await this.db.updatePersonDeprecated(person, update) + const kafkaAck = this.db.kafkaProducer.queueMessages({ kafkaMessages, waitForAck: true }) + return [updatedPerson, kafkaAck] } - return person + + return [person, Promise.resolve()] } /** @@ -306,7 +301,7 @@ export class PersonState { // Alias & merge - async handleIdentifyOrAlias(): Promise { + async handleIdentifyOrAlias(): Promise<[InternalPerson | undefined, Promise]> { /** * strategy: * - if the two distinct ids passed don't match and aren't illegal, then mark `is_identified` to be true for the `distinct_id` person @@ -350,7 +345,7 @@ export class PersonState { } finally { clearTimeout(timeout) } - return undefined + return [undefined, Promise.resolve()] } public async merge( @@ -358,10 +353,10 @@ export class PersonState { mergeIntoDistinctId: string, teamId: number, timestamp: DateTime - ): Promise { + ): Promise<[InternalPerson | undefined, Promise]> { // No reason to alias person against itself. Done by posthog-node when updating user properties if (mergeIntoDistinctId === otherPersonDistinctId) { - return undefined + return [undefined, Promise.resolve()] } if (isDistinctIdIllegal(mergeIntoDistinctId)) { await captureIngestionWarning( @@ -375,7 +370,7 @@ export class PersonState { }, { alwaysSend: true } ) - return undefined + return [undefined, Promise.resolve()] } if (isDistinctIdIllegal(otherPersonDistinctId)) { await captureIngestionWarning( @@ -389,7 +384,7 @@ export class PersonState { }, { alwaysSend: true } ) - return undefined + return [undefined, Promise.resolve()] } return promiseRetry( () => this.mergeDistinctIds(otherPersonDistinctId, mergeIntoDistinctId, teamId, timestamp), @@ -402,7 +397,7 @@ export class PersonState { mergeIntoDistinctId: string, teamId: number, timestamp: DateTime - ): Promise { + ): Promise<[InternalPerson, Promise]> { this.updateIsIdentified = true const otherPerson = await this.db.fetchPerson(teamId, otherPersonDistinctId) @@ -412,8 +407,9 @@ export class PersonState { // Overrides are only created when the version is > 0, see: // https://github.com/PostHog/posthog/blob/92e17ce307a577c4233d4ab252eebc6c2207a5ee/posthog/models/person/sql.py#L269-L287 // - // With the addition of optional person processing, we are now rolling out a change to - // lazily create `posthog_persondistinctid` and `posthog_person` rows. This means that: + // With the addition of optional person processing, we are no longer creating + // `posthog_persondistinctid` and `posthog_person` rows when $process_person_profile=false. + // This means that: // 1. At merge time, it's possible this `distinct_id` and its deterministically generated // `person.uuid` has already been used for events in ClickHouse, but they have no // corresponding rows in the `posthog_persondistinctid` or `posthog_person` tables @@ -422,20 +418,17 @@ export class PersonState { // `distinct_id` even though we're just now INSERT-ing it into Postgres/ClickHouse. We do // this by starting with `version=1`, as if we had just deleted the old user and were // updating the `distinct_id` row as part of the merge - let addDistinctIdVersion = 0 - if (this.lazyPersonCreation) { - addDistinctIdVersion = 1 - } + const addDistinctIdVersion = 1 if (otherPerson && !mergeIntoPerson) { await this.db.addDistinctId(otherPerson, mergeIntoDistinctId, addDistinctIdVersion) - return otherPerson + return [otherPerson, Promise.resolve()] } else if (!otherPerson && mergeIntoPerson) { await this.db.addDistinctId(mergeIntoPerson, otherPersonDistinctId, addDistinctIdVersion) - return mergeIntoPerson + return [mergeIntoPerson, Promise.resolve()] } else if (otherPerson && mergeIntoPerson) { if (otherPerson.id == mergeIntoPerson.id) { - return mergeIntoPerson + return [mergeIntoPerson, Promise.resolve()] } return await this.mergePeople({ mergeInto: mergeIntoPerson, @@ -446,18 +439,21 @@ export class PersonState { } // The last case: (!oldPerson && !newPerson) - return await this.createPerson( - // TODO: in this case we could skip the properties updates later - timestamp, - this.eventProperties['$set'] || {}, - this.eventProperties['$set_once'] || {}, - teamId, - null, - true, - this.event.uuid, - [mergeIntoDistinctId, otherPersonDistinctId], - addDistinctIdVersion - ) + return [ + await this.createPerson( + // TODO: in this case we could skip the properties updates later + timestamp, + this.eventProperties['$set'] || {}, + this.eventProperties['$set_once'] || {}, + teamId, + null, + true, + this.event.uuid, + [mergeIntoDistinctId, otherPersonDistinctId], + addDistinctIdVersion + ), + Promise.resolve(), + ] } public async mergePeople({ @@ -470,7 +466,7 @@ export class PersonState { mergeIntoDistinctId: string otherPerson: InternalPerson otherPersonDistinctId: string - }): Promise { + }): Promise<[InternalPerson, Promise]> { const olderCreatedAt = DateTime.min(mergeInto.created_at, otherPerson.created_at) const mergeAllowed = this.isMergeAllowed(otherPerson) @@ -488,7 +484,7 @@ export class PersonState { { alwaysSend: true } ) status.warn('🤔', 'refused to merge an already identified user via an $identify or $create_alias call') - return mergeInto // We're returning the original person tied to distinct_id used for the event + return [mergeInto, Promise.resolve()] // We're returning the original person tied to distinct_id used for the event } // How the merge works: @@ -508,14 +504,14 @@ export class PersonState { const properties: Properties = { ...otherPerson.properties, ...mergeInto.properties } this.applyEventPropertyUpdates(properties) - const [kafkaMessages, mergedPerson] = await this.handleMergeTransaction( + const [mergedPerson, kafkaMessages] = await this.handleMergeTransaction( mergeInto, otherPerson, olderCreatedAt, // Keep the oldest created_at (i.e. the first time we've seen either person) properties ) - await this.db.kafkaProducer.queueMessages({ kafkaMessages, waitForAck: true }) - return mergedPerson + + return [mergedPerson, kafkaMessages] } private isMergeAllowed(mergeFrom: InternalPerson): boolean { @@ -529,7 +525,7 @@ export class PersonState { otherPerson: InternalPerson, createdAt: DateTime, properties: Properties - ): Promise<[ProducerRecord[], InternalPerson]> { + ): Promise<[InternalPerson, Promise]> { mergeTxnAttemptCounter .labels({ call: this.event.event, // $identify, $create_alias or $merge_dangerously @@ -539,7 +535,7 @@ export class PersonState { }) .inc() - const result: [ProducerRecord[], InternalPerson] = await this.db.postgres.transaction( + const [mergedPerson, kafkaMessages]: [InternalPerson, ProducerRecord[]] = await this.db.postgres.transaction( PostgresUse.COMMON_WRITE, 'mergePeople', async (tx) => { @@ -573,7 +569,7 @@ export class PersonState { ) } - return [[...updatePersonMessages, ...distinctIdMessages, ...deletePersonMessages], person] + return [person, [...updatePersonMessages, ...distinctIdMessages, ...deletePersonMessages]] } ) @@ -585,7 +581,10 @@ export class PersonState { poEEmbraceJoin: String(!!this.personOverrideWriter), }) .inc() - return result + + const kafkaAck = this.db.kafkaProducer.queueMessages({ kafkaMessages, waitForAck: true }) + + return [mergedPerson, kafkaAck] } } diff --git a/plugin-server/src/worker/ingestion/utils.ts b/plugin-server/src/worker/ingestion/utils.ts index 9488ee759581b..c6c313d74d459 100644 --- a/plugin-server/src/worker/ingestion/utils.ts +++ b/plugin-server/src/worker/ingestion/utils.ts @@ -94,7 +94,7 @@ export async function captureIngestionWarning( }, ], }, - waitForAck: true, + waitForAck: false, }) } else { return Promise.resolve() diff --git a/plugin-server/src/worker/rusty-hook.ts b/plugin-server/src/worker/rusty-hook.ts index cb829800cbaa3..a4d1c6c6b2d81 100644 --- a/plugin-server/src/worker/rusty-hook.ts +++ b/plugin-server/src/worker/rusty-hook.ts @@ -2,7 +2,8 @@ import { Webhook } from '@posthog/plugin-scaffold' import * as Sentry from '@sentry/node' import fetch from 'node-fetch' -import { ValueMatcher } from '../types' +import { buildIntegerMatcher } from '../config/config' +import { PluginsServerConfig, ValueMatcher } from '../types' import { isProdEnv } from '../utils/env-utils' import { raiseIfUserProvidedUrlUnsafe } from '../utils/fetch' import { status } from '../utils/status' @@ -23,12 +24,16 @@ interface RustyWebhookPayload { } export class RustyHook { + private enabledForTeams: ValueMatcher + constructor( - private enabledForTeams: ValueMatcher, - private rolloutPercentage: number, - private serviceUrl: string, - private requestTimeoutMs: number - ) {} + private serverConfig: Pick< + PluginsServerConfig, + 'RUSTY_HOOK_URL' | 'RUSTY_HOOK_FOR_TEAMS' | 'RUSTY_HOOK_ROLLOUT_PERCENTAGE' | 'EXTERNAL_REQUEST_TIMEOUT_MS' + > + ) { + this.enabledForTeams = buildIntegerMatcher(serverConfig.RUSTY_HOOK_FOR_TEAMS, true) + } public async enqueueIfEnabledForTeam({ webhook, @@ -43,7 +48,7 @@ export class RustyHook { }): Promise { // A simple and blunt rollout that just uses the last digits of the Team ID as a stable // selection against the `rolloutPercentage`. - const enabledByRolloutPercentage = (teamId % 1000) / 1000 < this.rolloutPercentage + const enabledByRolloutPercentage = (teamId % 1000) / 1000 < this.serverConfig.RUSTY_HOOK_ROLLOUT_PERCENTAGE if (!enabledByRolloutPercentage && !this.enabledForTeams(teamId)) { return false } @@ -75,14 +80,14 @@ export class RustyHook { const timer = new Date() try { attempt += 1 - const response = await fetch(this.serviceUrl, { + const response = await fetch(this.serverConfig.RUSTY_HOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, // Sure, it's not an external request, but we should have a timeout and this is as // good as any. - timeout: this.requestTimeoutMs, + timeout: this.serverConfig.EXTERNAL_REQUEST_TIMEOUT_MS, }) if (response.ok) { diff --git a/plugin-server/tests/main/db.test.ts b/plugin-server/tests/main/db.test.ts index e5c08a092099b..94b8d8d310363 100644 --- a/plugin-server/tests/main/db.test.ts +++ b/plugin-server/tests/main/db.test.ts @@ -360,7 +360,11 @@ describe('DB', () => { const personProvided = { ...personDbBefore, properties: { c: 'bbb' }, created_at: providedPersonTs } const updateTs = DateTime.fromISO('2000-04-04T11:42:06.502Z').toUTC() const update = { created_at: updateTs } - const [updatedPerson] = await db.updatePersonDeprecated(personProvided, update) + const [updatedPerson, kafkaMessages] = await db.updatePersonDeprecated(personProvided, update) + await hub.db.kafkaProducer.queueMessages({ + kafkaMessages, + waitForAck: true, + }) // verify we have the correct update in Postgres db const personDbAfter = await fetchPersonByPersonId(personDbBefore.team_id, personDbBefore.id) @@ -418,7 +422,13 @@ describe('DB', () => { await delayUntilEventIngested(fetchPersonsRows, 1) // We do an update to verify - await db.updatePersonDeprecated(person, { properties: { foo: 'bar' } }) + const [_p, updatePersonKafkaMessages] = await db.updatePersonDeprecated(person, { + properties: { foo: 'bar' }, + }) + await hub.db.kafkaProducer.queueMessages({ + kafkaMessages: updatePersonKafkaMessages, + waitForAck: true, + }) await db.kafkaProducer.flush() await delayUntilEventIngested(fetchPersonsRows, 2) diff --git a/plugin-server/tests/main/ingestion-queues/analytics-events-ingestion-overflow-consumer.test.ts b/plugin-server/tests/main/ingestion-queues/analytics-events-ingestion-overflow-consumer.test.ts index 774475a5b34aa..e0f46c4a39d31 100644 --- a/plugin-server/tests/main/ingestion-queues/analytics-events-ingestion-overflow-consumer.test.ts +++ b/plugin-server/tests/main/ingestion-queues/analytics-events-ingestion-overflow-consumer.test.ts @@ -113,7 +113,7 @@ describe('eachBatchParallelIngestion with overflow consume', () => { }, ], }, - waitForAck: true, + waitForAck: false, }) // Event is processed diff --git a/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts b/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts index edd4f95bebda2..6678c28d92a3e 100644 --- a/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts +++ b/plugin-server/tests/main/ingestion-queues/session-recording/utils.test.ts @@ -221,7 +221,7 @@ describe('session-recording utils', () => { ], topic: 'clickhouse_ingestion_warnings_test', }, - waitForAck: true, + waitForAck: false, }, ], ], @@ -241,7 +241,7 @@ describe('session-recording utils', () => { ], topic: 'clickhouse_ingestion_warnings_test', }, - waitForAck: true, + waitForAck: false, }, ], ], diff --git a/plugin-server/tests/main/process-event.test.ts b/plugin-server/tests/main/process-event.test.ts index 71432287879c3..f5a9576d07c7c 100644 --- a/plugin-server/tests/main/process-event.test.ts +++ b/plugin-server/tests/main/process-event.test.ts @@ -209,11 +209,20 @@ test('merge people', async () => { const p0 = await createPerson(hub, team, ['person_0'], { $os: 'Microsoft' }) await delayUntilEventIngested(() => hub.db.fetchPersons(Database.ClickHouse), 1) - await hub.db.updatePersonDeprecated(p0, { created_at: DateTime.fromISO('2020-01-01T00:00:00Z') }) + const [_person0, kafkaMessages0] = await hub.db.updatePersonDeprecated(p0, { + created_at: DateTime.fromISO('2020-01-01T00:00:00Z'), + }) const p1 = await createPerson(hub, team, ['person_1'], { $os: 'Chrome', $browser: 'Chrome' }) await delayUntilEventIngested(() => hub.db.fetchPersons(Database.ClickHouse), 2) - await hub.db.updatePersonDeprecated(p1, { created_at: DateTime.fromISO('2019-07-01T00:00:00Z') }) + const [_person1, kafkaMessages1] = await hub.db.updatePersonDeprecated(p1, { + created_at: DateTime.fromISO('2019-07-01T00:00:00Z'), + }) + + await hub.db.kafkaProducer.queueMessages({ + kafkaMessages: [...kafkaMessages0, ...kafkaMessages1], + waitForAck: true, + }) await processEvent( 'person_1', diff --git a/plugin-server/tests/worker/ingestion/person-state.test.ts b/plugin-server/tests/worker/ingestion/person-state.test.ts index d3a04018e0d96..ab921d71902cc 100644 --- a/plugin-server/tests/worker/ingestion/person-state.test.ts +++ b/plugin-server/tests/worker/ingestion/person-state.test.ts @@ -111,7 +111,6 @@ describe('PersonState.update()', () => { event: Partial, customHub?: Hub, processPerson = true, - lazyPersonCreation = false, timestampParam = timestamp ) { const fullEvent = { @@ -127,7 +126,6 @@ describe('PersonState.update()', () => { timestampParam, processPerson, customHub ? customHub.db : hub.db, - lazyPersonCreation, overridesMode?.getWriter(customHub ?? hub) ) } @@ -164,7 +162,7 @@ describe('PersonState.update()', () => { it('creates deterministic person uuids that are different between teams', async () => { const event_uuid = new UUIDT().toString() const primaryTeamId = teamId - const personPrimaryTeam = await personState({ + const [personPrimaryTeam, kafkaAcks] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, uuid: event_uuid, @@ -172,26 +170,27 @@ describe('PersonState.update()', () => { const otherTeamId = await createTeam(hub.db.postgres, organizationId) teamId = otherTeamId - const personOtherTeam = await personState({ + const [personOtherTeam, kafkaAcksOther] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, uuid: event_uuid, }).updateProperties() await hub.db.kafkaProducer.flush() + await kafkaAcks + await kafkaAcksOther expect(personPrimaryTeam.uuid).toEqual(uuidFromDistinctId(primaryTeamId, newUserDistinctId)) expect(personOtherTeam.uuid).toEqual(uuidFromDistinctId(otherTeamId, newUserDistinctId)) expect(personPrimaryTeam.uuid).not.toEqual(personOtherTeam.uuid) }) - it('returns an ephemeral user object when lazy creation is enabled and $process_person_profile=false', async () => { + it('returns an ephemeral user object when $process_person_profile=false', async () => { const event_uuid = new UUIDT().toString() const hubParam = undefined const processPerson = false - const lazyPersonCreation = true - const fakePerson = await personState( + const [fakePerson, kafkaAcks] = await personState( { event: '$pageview', distinct_id: newUserDistinctId, @@ -199,10 +198,10 @@ describe('PersonState.update()', () => { properties: { $set: { should_be_dropped: 100 } }, }, hubParam, - processPerson, - lazyPersonCreation + processPerson ).update() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(fakePerson).toEqual( expect.objectContaining({ @@ -223,13 +222,12 @@ describe('PersonState.update()', () => { expect(distinctIds).toEqual(expect.arrayContaining([])) }) - it('merging with lazy person creation creates an override and force_upgrade works', async () => { + it('merging creates an override and force_upgrade works', async () => { await hub.db.createPerson(timestamp, {}, {}, {}, teamId, null, false, oldUserUuid, [oldUserDistinctId]) const hubParam = undefined let processPerson = true - const lazyPersonCreation = true - await personState( + const [_person, kafkaAcks] = await personState( { event: '$identify', distinct_id: newUserDistinctId, @@ -238,10 +236,10 @@ describe('PersonState.update()', () => { }, }, hubParam, - processPerson, - lazyPersonCreation + processPerson ).update() await hub.db.kafkaProducer.flush() + await kafkaAcks await delayUntilEventIngested(() => fetchOverridesForDistinctId(newUserDistinctId)) const chOverrides = await fetchOverridesForDistinctId(newUserDistinctId) @@ -263,7 +261,7 @@ describe('PersonState.update()', () => { processPerson = false const event_uuid = new UUIDT().toString() const timestampParam = timestamp.plus({ minutes: 5 }) // Event needs to happen after Person creation - const fakePerson = await personState( + const [fakePerson, kafkaAcks2] = await personState( { event: '$pageview', distinct_id: newUserDistinctId, @@ -272,10 +270,10 @@ describe('PersonState.update()', () => { }, hubParam, processPerson, - lazyPersonCreation, timestampParam ).update() await hub.db.kafkaProducer.flush() + await kafkaAcks2 expect(fakePerson).toEqual( expect.objectContaining({ @@ -290,7 +288,7 @@ describe('PersonState.update()', () => { it('creates person if they are new', async () => { const event_uuid = new UUIDT().toString() - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, uuid: event_uuid, @@ -298,6 +296,7 @@ describe('PersonState.update()', () => { properties: { $set: { null_byte: '\u0000' } }, }).updateProperties() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -323,58 +322,16 @@ describe('PersonState.update()', () => { expect(distinctIds).toEqual(expect.arrayContaining([newUserDistinctId])) }) - it('creates person if they are new and $process_person_profile=false', async () => { - // Note that eventually $process_person_profile=false will be optimized so that the person is - // *not* created here. - const event_uuid = new UUIDT().toString() - const processPerson = false - const person = await personState( - { - event: '$pageview', - distinct_id: newUserDistinctId, - uuid: event_uuid, - properties: { $process_person_profile: false, $set: { a: 1 }, $set_once: { b: 2 } }, - }, - hub, - processPerson - ).update() - await hub.db.kafkaProducer.flush() - - expect(person).toEqual( - expect.objectContaining({ - id: expect.any(Number), - uuid: newUserUuid, - properties: {}, - created_at: timestamp, - version: 0, - is_identified: false, - }) - ) - - expect(hub.db.fetchPerson).toHaveBeenCalledTimes(1) - expect(hub.db.updatePersonDeprecated).not.toHaveBeenCalled() - - // verify Postgres persons - const persons = await fetchPostgresPersonsH() - expect(persons.length).toEqual(1) - // For parity with existing functionality, the Person created in the DB actually gets - // the $creator_event_uuid property. When we stop creating person rows this won't matter. - expect(persons[0]).toEqual({ ...person, properties: { $creator_event_uuid: event_uuid } }) - - // verify Postgres distinct_ids - const distinctIds = await hub.db.fetchDistinctIdValues(persons[0]) - expect(distinctIds).toEqual(expect.arrayContaining([newUserDistinctId])) - }) - it('does not attach existing person properties to $process_person_profile=false events', async () => { const originalEventUuid = new UUIDT().toString() - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, uuid: originalEventUuid, properties: { $set: { c: 420 } }, }).update() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -398,7 +355,7 @@ describe('PersonState.update()', () => { // OK, a person now exists with { c: 420 }, let's prove the properties come back out // of the DB. - const personVerifyProps = await personState({ + const [personVerifyProps] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, uuid: new UUIDT().toString(), @@ -407,7 +364,7 @@ describe('PersonState.update()', () => { expect(personVerifyProps.properties).toEqual({ $creator_event_uuid: originalEventUuid, c: 420 }) // But they don't when $process_person_profile=false - const processPersonFalseResult = await personState( + const [processPersonFalseResult] = await personState( { event: '$pageview', distinct_id: newUserDistinctId, @@ -427,8 +384,12 @@ describe('PersonState.update()', () => { return Promise.resolve(undefined) }) - const person = await personState({ event: '$pageview', distinct_id: newUserDistinctId }).handleUpdate() + const [person, kafkaAcks] = await personState({ + event: '$pageview', + distinct_id: newUserDistinctId, + }).handleUpdate() await hub.db.kafkaProducer.flush() + await kafkaAcks // if creation fails we should return the person that another thread already created expect(person).toEqual( @@ -461,7 +422,7 @@ describe('PersonState.update()', () => { return Promise.resolve(undefined) }) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, properties: { @@ -470,6 +431,7 @@ describe('PersonState.update()', () => { }, }).handleUpdate() await hub.db.kafkaProducer.flush() + await kafkaAcks // if creation fails we should return the person that another thread already created expect(person).toEqual( @@ -494,7 +456,7 @@ describe('PersonState.update()', () => { }) it('creates person with properties', async () => { - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, properties: { @@ -503,6 +465,7 @@ describe('PersonState.update()', () => { }, }).updateProperties() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -543,7 +506,7 @@ describe('PersonState.update()', () => { [newUserDistinctId] ) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, properties: { @@ -552,6 +515,7 @@ describe('PersonState.update()', () => { }, }).updateProperties() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -594,9 +558,12 @@ describe('PersonState.update()', () => { $set: { b: 4 }, }, }) - jest.spyOn(personS, 'handleIdentifyOrAlias').mockReturnValue(Promise.resolve(personInitial)) - const person = await personS.update() + jest.spyOn(personS, 'handleIdentifyOrAlias').mockReturnValue( + Promise.resolve([personInitial, Promise.resolve()]) + ) + const [person, kafkaAcks] = await personS.update() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -622,7 +589,7 @@ describe('PersonState.update()', () => { newUserDistinctId, ]) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$pageview', distinct_id: newUserDistinctId, properties: { @@ -631,6 +598,7 @@ describe('PersonState.update()', () => { }, }).updateProperties() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -661,8 +629,9 @@ describe('PersonState.update()', () => { }) personS.updateIsIdentified = true - const person = await personS.updateProperties() + const [person, kafkaAcks] = await personS.updateProperties() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ id: expect.any(Number), @@ -713,10 +682,13 @@ describe('PersonState.update()', () => { distinct_id: newUserDistinctId, properties: { $set: { a: 7, d: 9 } }, }) - jest.spyOn(personS, 'handleIdentifyOrAlias').mockReturnValue(Promise.resolve(mergeDeletedPerson)) + jest.spyOn(personS, 'handleIdentifyOrAlias').mockReturnValue( + Promise.resolve([mergeDeletedPerson, Promise.resolve()]) + ) - const person = await personS.update() + const [person, kafkaAcks] = await personS.update() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -746,7 +718,7 @@ describe('PersonState.update()', () => { describe(`overrides: ${useOverridesMode}`, () => { it(`no-op when $anon_distinct_id not passed`, async () => { - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$identify', distinct_id: newUserDistinctId, properties: { @@ -754,6 +726,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual(undefined) const persons = await fetchPostgresPersonsH() @@ -761,7 +734,7 @@ describe('PersonState.update()', () => { }) it(`creates person with both distinct_ids and marks user as is_identified when $anon_distinct_id passed`, async () => { - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$identify', distinct_id: newUserDistinctId, properties: { @@ -770,6 +743,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -777,7 +751,7 @@ describe('PersonState.update()', () => { uuid: newUserUuid, properties: { foo: 'bar' }, created_at: timestamp, - version: 0, + version: 1, is_identified: true, }) ) @@ -807,8 +781,9 @@ describe('PersonState.update()', () => { $anon_distinct_id: oldUserDistinctId, }, }) - const person = await personS.handleIdentifyOrAlias() + const [person, kafkaAcks] = await personS.handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -838,8 +813,9 @@ describe('PersonState.update()', () => { $anon_distinct_id: oldUserDistinctId, }, }) - const person = await personS.handleIdentifyOrAlias() + const [person, kafkaAcks] = await personS.handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks const persons = await fetchPostgresPersonsH() expect(person).toEqual( @@ -873,8 +849,9 @@ describe('PersonState.update()', () => { $anon_distinct_id: oldUserDistinctId, }, }) - const person = await personS.handleIdentifyOrAlias() + const [person, kafkaAcks] = await personS.handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks const persons = await fetchPostgresPersonsH() @@ -903,7 +880,7 @@ describe('PersonState.update()', () => { await hub.db.createPerson(timestamp, {}, {}, {}, teamId, null, false, oldUserUuid, [oldUserDistinctId]) await hub.db.createPerson(timestamp2, {}, {}, {}, teamId, null, false, newUserUuid, [newUserDistinctId]) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$identify', distinct_id: newUserDistinctId, properties: { @@ -911,6 +888,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -965,7 +943,7 @@ describe('PersonState.update()', () => { await hub.db.createPerson(timestamp, {}, {}, {}, teamId, null, false, oldUserUuid, [oldUserDistinctId]) await hub.db.createPerson(timestamp2, {}, {}, {}, teamId, null, true, newUserUuid, [newUserDistinctId]) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$identify', distinct_id: newUserDistinctId, properties: { @@ -973,6 +951,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -1034,8 +1013,9 @@ describe('PersonState.update()', () => { $anon_distinct_id: oldUserDistinctId, }, }) - const person = await personS.handleIdentifyOrAlias() + const [person, kafkaAcks] = await personS.handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(personS.updateIsIdentified).toBeTruthy() expect(person).toEqual( @@ -1075,7 +1055,7 @@ describe('PersonState.update()', () => { await hub.db.createPerson(timestamp, {}, {}, {}, teamId, null, true, oldUserUuid, [oldUserDistinctId]) await hub.db.createPerson(timestamp2, {}, {}, {}, teamId, null, true, newUserUuid, [newUserDistinctId]) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$identify', distinct_id: newUserDistinctId, properties: { @@ -1083,6 +1063,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -1125,7 +1106,7 @@ describe('PersonState.update()', () => { newUserDistinctId, ]) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$identify', distinct_id: newUserDistinctId, properties: { @@ -1135,6 +1116,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -1204,7 +1186,7 @@ describe('PersonState.update()', () => { await hub.db.addDistinctId(person, distinctId, 0) // this throws }) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$identify', distinct_id: oldUserDistinctId, properties: { @@ -1212,6 +1194,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks jest.spyOn(hub.db, 'addDistinctId').mockRestore() // Necessary for other tests not to fail // if creation fails we should return the person that another thread already created @@ -1250,7 +1233,7 @@ describe('PersonState.update()', () => { hub ) jest.spyOn(state, 'merge').mockImplementation(() => { - return Promise.resolve(undefined) + return Promise.resolve([undefined, Promise.resolve()]) }) await state.handleIdentifyOrAlias() expect(state.merge).toHaveBeenCalledWith(oldUserDistinctId, newUserDistinctId, teamId, timestamp) @@ -1267,7 +1250,7 @@ describe('PersonState.update()', () => { hub ) jest.spyOn(state, 'merge').mockImplementation(() => { - return Promise.resolve(undefined) + return Promise.resolve([undefined, Promise.resolve()]) }) await state.handleIdentifyOrAlias() @@ -1285,7 +1268,7 @@ describe('PersonState.update()', () => { hub ) jest.spyOn(state, 'merge').mockImplementation(() => { - return Promise.resolve(undefined) + return Promise.resolve([undefined, Promise.resolve()]) }) await state.handleIdentifyOrAlias() @@ -1305,7 +1288,7 @@ describe('PersonState.update()', () => { await hub.db.createPerson(timestamp, {}, {}, {}, teamId, null, true, oldUserUuid, [oldUserDistinctId]) await hub.db.createPerson(timestamp2, {}, {}, {}, teamId, null, true, newUserUuid, [newUserDistinctId]) - const person = await personState({ + const [person, kafkaAcks] = await personState({ event: '$merge_dangerously', distinct_id: newUserDistinctId, properties: { @@ -1313,6 +1296,7 @@ describe('PersonState.update()', () => { }, }).handleIdentifyOrAlias() await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -1368,7 +1352,7 @@ describe('PersonState.update()', () => { describe('illegal aliasing', () => { const illegalIds = ['', ' ', 'null', 'undefined', '"undefined"', '[object Object]', '"[object Object]"'] it.each(illegalIds)('stops $identify if current distinct_id is illegal: `%s`', async (illegalId: string) => { - const person = await personState({ + const [person] = await personState({ event: '$identify', distinct_id: illegalId, properties: { @@ -1382,7 +1366,7 @@ describe('PersonState.update()', () => { }) it.each(illegalIds)('stops $identify if $anon_distinct_id is illegal: `%s`', async (illegalId: string) => { - const person = await personState({ + const [person] = await personState({ event: '$identify', distinct_id: 'some_distinct_id', properties: { @@ -1396,7 +1380,7 @@ describe('PersonState.update()', () => { }) it('stops $create_alias if current distinct_id is illegal', async () => { - const person = await personState({ + const [person] = await personState({ event: '$create_alias', distinct_id: 'false', properties: { @@ -1410,7 +1394,7 @@ describe('PersonState.update()', () => { }) it('stops $create_alias if alias is illegal', async () => { - const person = await personState({ + const [person] = await personState({ event: '$create_alias', distinct_id: 'some_distinct_id', properties: { @@ -1685,8 +1669,14 @@ describe('PersonState.update()', () => { ]) const state: PersonState = personState({}, hub) jest.spyOn(hub.db.kafkaProducer, 'queueMessages') - const person = await state.merge(secondUserDistinctId, firstUserDistinctId, teamId, timestamp) + const [person, kafkaAcks] = await state.merge( + secondUserDistinctId, + firstUserDistinctId, + teamId, + timestamp + ) await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -1728,13 +1718,14 @@ describe('PersonState.update()', () => { const state: PersonState = personState({}, hub) jest.spyOn(hub.db.kafkaProducer, 'queueMessages') - const person = await state.mergePeople({ + const [person, kafkaAcks] = await state.mergePeople({ mergeInto: first, mergeIntoDistinctId: firstUserDistinctId, otherPerson: second, otherPersonDistinctId: secondUserDistinctId, }) await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ @@ -2060,13 +2051,14 @@ describe('PersonState.update()', () => { // Now verify we successfully get to our target state if we do not have // any db errors. mockPostgresQuery.mockRestore() - const person = await state.mergePeople({ + const [person, kafkaAcks] = await state.mergePeople({ mergeInto: first, mergeIntoDistinctId: firstUserDistinctId, otherPerson: second, otherPersonDistinctId: secondUserDistinctId, }) await hub.db.kafkaProducer.flush() + await kafkaAcks expect(person).toEqual( expect.objectContaining({ diff --git a/plugin-server/tests/worker/ingestion/postgres-parity.test.ts b/plugin-server/tests/worker/ingestion/postgres-parity.test.ts index 142b7c6938bd6..5b12393a63b10 100644 --- a/plugin-server/tests/worker/ingestion/postgres-parity.test.ts +++ b/plugin-server/tests/worker/ingestion/postgres-parity.test.ts @@ -170,11 +170,16 @@ describe('postgres parity', () => { await delayUntilEventIngested(() => hub.db.fetchDistinctIdValues(person, Database.ClickHouse), 2) // update properties and set is_identified to true - await hub.db.updatePersonDeprecated(person, { + const [_p, kafkaMessages] = await hub.db.updatePersonDeprecated(person, { properties: { replacedUserProp: 'propValue' }, is_identified: true, }) + await hub.db.kafkaProducer.queueMessages({ + kafkaMessages, + waitForAck: true, + }) + await delayUntilEventIngested(async () => (await hub.db.fetchPersons(Database.ClickHouse)).filter((p) => p.is_identified) ) @@ -196,11 +201,16 @@ describe('postgres parity', () => { // update date and boolean to false const randomDate = DateTime.utc().minus(100000).setZone('UTC') - const [updatedPerson] = await hub.db.updatePersonDeprecated(person, { + const [updatedPerson, kafkaMessages2] = await hub.db.updatePersonDeprecated(person, { created_at: randomDate, is_identified: false, }) + await hub.db.kafkaProducer.queueMessages({ + kafkaMessages: kafkaMessages2, + waitForAck: true, + }) + expect(updatedPerson.version).toEqual(2) await delayUntilEventIngested(async () => diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4789afdebaff4..6c9eab81c8a89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -260,8 +260,8 @@ dependencies: specifier: ^9.3.0 version: 9.3.0(postcss@8.4.31) posthog-js: - specifier: 1.138.2 - version: 1.138.2 + specifier: 1.139.0 + version: 1.139.0 posthog-js-lite: specifier: 3.0.0 version: 3.0.0 @@ -17707,8 +17707,8 @@ packages: resolution: {integrity: sha512-dyajjnfzZD1tht4N7p7iwf7nBnR1MjVaVu+MKr+7gBgA39bn28wizCIJZztZPtHy4PY0YwtSGgwfBCuG/hnHgA==} dev: false - /posthog-js@1.138.2: - resolution: {integrity: sha512-siin1JCAe8UIrc39qV5SFwxBcUB7zp80KNKp175McMGh3Vtw056AccFTBw6xpuIjX5hh23gfw7Pnr/VnI7MSfw==} + /posthog-js@1.139.0: + resolution: {integrity: sha512-FuYlxQFO0Dq5X1/bFEM8F+NgOqZiVh4fPVHHeOTWMkqVP+pCnODQitbtW0hgT0/EE665w0xpZBk93YavaZRhzQ==} dependencies: fflate: 0.4.8 preact: 10.22.0 diff --git a/posthog/api/__init__.py b/posthog/api/__init__.py index 9e1f2d8458886..dbe3d1bd17515 100644 --- a/posthog/api/__init__.py +++ b/posthog/api/__init__.py @@ -20,6 +20,7 @@ event_definition, exports, feature_flag, + hog_function, ingestion_warnings, instance_settings, instance_status, @@ -408,6 +409,13 @@ def api_not_found(request): ["team_id"], ) +projects_router.register( + r"hog_functions", + hog_function.HogFunctionViewSet, + "project_hog_functions", + ["team_id"], +) + projects_router.register( r"alerts", alert.AlertViewSet, diff --git a/posthog/api/app_metrics.py b/posthog/api/app_metrics.py index 8f97638cd0fbf..61612980e24f5 100644 --- a/posthog/api/app_metrics.py +++ b/posthog/api/app_metrics.py @@ -56,23 +56,30 @@ def retrieve(self, request: request.Request, *args: Any, **kwargs: Any) -> respo except ValueError: pass - plugin_config = self.get_object() - filter = AppMetricsRequestSerializer(data=request.query_params) filter.is_valid(raise_exception=True) - metric_results = AppMetricsQuery(self.team, plugin_config.pk, filter).run() - errors = AppMetricsErrorsQuery(self.team, plugin_config.pk, filter).run() + if "hog-" in kwargs["pk"]: + # TODO: Make app metrics work with string IDs + metric_results = { + "dates": [], + "successes": [], + "successes_on_retry": [], + "failures": [], + "totals": {"successes": 0, "successes_on_retry": 0, "failures": 0}, + } + errors = [] + else: + metric_results = AppMetricsQuery(self.team, kwargs["pk"], filter).run() + errors = AppMetricsErrorsQuery(self.team, kwargs["pk"], filter).run() return response.Response({"metrics": metric_results, "errors": errors}) @action(methods=["GET"], detail=True) def error_details(self, request: request.Request, *args: Any, **kwargs: Any) -> response.Response: - plugin_config = self.get_object() - filter = AppMetricsErrorsRequestSerializer(data=request.query_params) filter.is_valid(raise_exception=True) - error_details = AppMetricsErrorDetailsQuery(self.team, plugin_config.pk, filter).run() + error_details = AppMetricsErrorDetailsQuery(self.team, kwargs["pk"], filter).run() return response.Response({"result": error_details}) def get_batch_export_runs_app_metrics_queryset(self, batch_export_id: str): diff --git a/posthog/api/team.py b/posthog/api/team.py index bb3395a5c56ea..e96ab0820eb55 100644 --- a/posthog/api/team.py +++ b/posthog/api/team.py @@ -199,9 +199,9 @@ def get_groups_on_events_querying_enabled(self, team: Team) -> bool: def get_live_events_token(self, team: Team) -> Optional[str]: return encode_jwt( - {"team_id": 2}, + {"team_id": team.id}, timedelta(days=7), - PosthogJwtAudience.LIVE_EVENTS, + PosthogJwtAudience.LIVESTREAM, ) def validate_session_recording_linked_flag(self, value) -> dict | None: diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index eb0091a95ba5c..d03538893572b 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -66,6 +66,29 @@ ''' # --- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.10 + ''' + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."active" + AND NOT "posthog_featureflag"."deleted" + AND "posthog_featureflag"."team_id" = 2) + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11 ''' SELECT "posthog_pluginconfig"."id", "posthog_pluginconfig"."web_token", @@ -81,17 +104,6 @@ AND "posthog_pluginconfig"."team_id" = 2) ''' # --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.11 - ''' - SELECT "posthog_instancesetting"."id", - "posthog_instancesetting"."key", - "posthog_instancesetting"."raw_value" - FROM "posthog_instancesetting" - WHERE "posthog_instancesetting"."key" = 'constance:posthog:PERSON_ON_EVENTS_V2_ENABLED' - ORDER BY "posthog_instancesetting"."id" ASC - LIMIT 1 - ''' -# --- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.12 ''' SELECT "posthog_instancesetting"."id", @@ -316,6 +328,83 @@ ''' # --- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.7 + ''' + SELECT "posthog_hogfunction"."id", + "posthog_hogfunction"."team_id", + "posthog_hogfunction"."name", + "posthog_hogfunction"."description", + "posthog_hogfunction"."created_at", + "posthog_hogfunction"."created_by_id", + "posthog_hogfunction"."deleted", + "posthog_hogfunction"."updated_at", + "posthog_hogfunction"."enabled", + "posthog_hogfunction"."hog", + "posthog_hogfunction"."bytecode", + "posthog_hogfunction"."inputs_schema", + "posthog_hogfunction"."inputs", + "posthog_hogfunction"."filters", + "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_hogfunction" + INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id") + WHERE ("posthog_hogfunction"."team_id" = 2 + AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb) + ''' +# --- +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 ''' SELECT 1 AS "a" FROM "posthog_grouptypemapping" @@ -323,7 +412,7 @@ LIMIT 1 ''' # --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.8 +# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 ''' SELECT "posthog_user"."id", "posthog_user"."password", @@ -355,30 +444,84 @@ LIMIT 21 ''' # --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.9 +# name: TestDecide.test_flag_with_behavioural_cohorts ''' - SELECT "posthog_featureflag"."id", - "posthog_featureflag"."key", - "posthog_featureflag"."name", - "posthog_featureflag"."filters", - "posthog_featureflag"."rollout_percentage", - "posthog_featureflag"."team_id", - "posthog_featureflag"."created_by_id", - "posthog_featureflag"."created_at", - "posthog_featureflag"."deleted", - "posthog_featureflag"."active", - "posthog_featureflag"."rollback_conditions", - "posthog_featureflag"."performed_rollback", - "posthog_featureflag"."ensure_experience_continuity", - "posthog_featureflag"."usage_dashboard_id", - "posthog_featureflag"."has_enriched_analytics" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."active" - AND NOT "posthog_featureflag"."deleted" - AND "posthog_featureflag"."team_id" = 2) + SELECT "posthog_hogfunction"."id", + "posthog_hogfunction"."team_id", + "posthog_hogfunction"."name", + "posthog_hogfunction"."description", + "posthog_hogfunction"."created_at", + "posthog_hogfunction"."created_by_id", + "posthog_hogfunction"."deleted", + "posthog_hogfunction"."updated_at", + "posthog_hogfunction"."enabled", + "posthog_hogfunction"."hog", + "posthog_hogfunction"."bytecode", + "posthog_hogfunction"."inputs_schema", + "posthog_hogfunction"."inputs", + "posthog_hogfunction"."filters", + "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_hogfunction" + INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id") + WHERE ("posthog_hogfunction"."team_id" = 2 + AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb) ''' # --- -# name: TestDecide.test_flag_with_behavioural_cohorts +# name: TestDecide.test_flag_with_behavioural_cohorts.1 ''' SELECT "posthog_user"."id", "posthog_user"."password", @@ -410,7 +553,7 @@ LIMIT 21 ''' # --- -# name: TestDecide.test_flag_with_behavioural_cohorts.1 +# name: TestDecide.test_flag_with_behavioural_cohorts.2 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -472,7 +615,7 @@ LIMIT 21 ''' # --- -# name: TestDecide.test_flag_with_behavioural_cohorts.2 +# name: TestDecide.test_flag_with_behavioural_cohorts.3 ''' SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", @@ -495,7 +638,7 @@ AND "posthog_featureflag"."team_id" = 2) ''' # --- -# name: TestDecide.test_flag_with_behavioural_cohorts.3 +# name: TestDecide.test_flag_with_behavioural_cohorts.4 ''' SELECT "posthog_cohort"."id", "posthog_cohort"."name", @@ -519,7 +662,7 @@ AND "posthog_cohort"."team_id" = 2) ''' # --- -# name: TestDecide.test_flag_with_behavioural_cohorts.4 +# name: TestDecide.test_flag_with_behavioural_cohorts.5 ''' SELECT "posthog_cohort"."id", "posthog_cohort"."name", @@ -544,6 +687,83 @@ ''' # --- # name: TestDecide.test_flag_with_regular_cohorts + ''' + SELECT "posthog_hogfunction"."id", + "posthog_hogfunction"."team_id", + "posthog_hogfunction"."name", + "posthog_hogfunction"."description", + "posthog_hogfunction"."created_at", + "posthog_hogfunction"."created_by_id", + "posthog_hogfunction"."deleted", + "posthog_hogfunction"."updated_at", + "posthog_hogfunction"."enabled", + "posthog_hogfunction"."hog", + "posthog_hogfunction"."bytecode", + "posthog_hogfunction"."inputs_schema", + "posthog_hogfunction"."inputs", + "posthog_hogfunction"."filters", + "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_hogfunction" + INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id") + WHERE ("posthog_hogfunction"."team_id" = 2 + AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb) + ''' +# --- +# name: TestDecide.test_flag_with_regular_cohorts.1 ''' SELECT "posthog_user"."id", "posthog_user"."password", @@ -575,7 +795,7 @@ LIMIT 21 ''' # --- -# name: TestDecide.test_flag_with_regular_cohorts.1 +# name: TestDecide.test_flag_with_regular_cohorts.2 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -637,7 +857,7 @@ LIMIT 21 ''' # --- -# name: TestDecide.test_flag_with_regular_cohorts.2 +# name: TestDecide.test_flag_with_regular_cohorts.3 ''' SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", @@ -660,7 +880,7 @@ AND "posthog_featureflag"."team_id" = 2) ''' # --- -# name: TestDecide.test_flag_with_regular_cohorts.3 +# name: TestDecide.test_flag_with_regular_cohorts.4 ''' SELECT "posthog_cohort"."id", "posthog_cohort"."name", @@ -684,7 +904,7 @@ AND "posthog_cohort"."team_id" = 2) ''' # --- -# name: TestDecide.test_flag_with_regular_cohorts.4 +# name: TestDecide.test_flag_with_regular_cohorts.5 ''' SELECT (("posthog_person"."properties" -> '$some_prop_1') = '"something_1"'::jsonb AND "posthog_person"."properties" ? '$some_prop_1' @@ -696,7 +916,7 @@ AND "posthog_person"."team_id" = 2) ''' # --- -# name: TestDecide.test_flag_with_regular_cohorts.5 +# name: TestDecide.test_flag_with_regular_cohorts.6 ''' SELECT "posthog_cohort"."id", "posthog_cohort"."name", @@ -720,7 +940,7 @@ AND "posthog_cohort"."team_id" = 2) ''' # --- -# name: TestDecide.test_flag_with_regular_cohorts.6 +# name: TestDecide.test_flag_with_regular_cohorts.7 ''' SELECT (("posthog_person"."properties" -> '$some_prop_1') = '"something_1"'::jsonb AND "posthog_person"."properties" ? '$some_prop_1' @@ -827,6 +1047,83 @@ ''' # --- # name: TestDecide.test_web_app_queries.3 + ''' + SELECT "posthog_hogfunction"."id", + "posthog_hogfunction"."team_id", + "posthog_hogfunction"."name", + "posthog_hogfunction"."description", + "posthog_hogfunction"."created_at", + "posthog_hogfunction"."created_by_id", + "posthog_hogfunction"."deleted", + "posthog_hogfunction"."updated_at", + "posthog_hogfunction"."enabled", + "posthog_hogfunction"."hog", + "posthog_hogfunction"."bytecode", + "posthog_hogfunction"."inputs_schema", + "posthog_hogfunction"."inputs", + "posthog_hogfunction"."filters", + "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_replay_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_hogfunction" + INNER JOIN "posthog_team" ON ("posthog_hogfunction"."team_id" = "posthog_team"."id") + WHERE ("posthog_hogfunction"."team_id" = 2 + AND "posthog_hogfunction"."filters" @> '{"filter_test_accounts": true}'::jsonb) + ''' +# --- +# name: TestDecide.test_web_app_queries.4 ''' SELECT "posthog_pluginconfig"."id", "posthog_pluginconfig"."web_token", @@ -842,7 +1139,7 @@ AND "posthog_pluginconfig"."team_id" = 2) ''' # --- -# name: TestDecide.test_web_app_queries.4 +# name: TestDecide.test_web_app_queries.5 ''' SELECT "posthog_pluginconfig"."id", "posthog_pluginconfig"."web_token", diff --git a/posthog/api/test/test_cohort.py b/posthog/api/test/test_cohort.py index 1876463862584..82740d73ed515 100644 --- a/posthog/api/test/test_cohort.py +++ b/posthog/api/test/test_cohort.py @@ -816,7 +816,7 @@ def test_creating_update_and_calculating_with_new_cohort_query(self, patch_captu "key": "$some_prop", "value": "something", "type": "person", - "operator": PropertyOperator.exact, + "operator": PropertyOperator.EXACT, } ], }, @@ -846,7 +846,7 @@ def test_creating_update_and_calculating_with_new_cohort_query_dynamic_error(sel "key": "$some_prop", "value": "something", "type": "person", - "operator": PropertyOperator.exact, + "operator": PropertyOperator.EXACT, } ], }, diff --git a/posthog/api/test/test_insight.py b/posthog/api/test/test_insight.py index 64d4cd7791d38..1fce31df54a8d 100644 --- a/posthog/api/test/test_insight.py +++ b/posthog/api/test/test_insight.py @@ -1145,10 +1145,10 @@ def test_insight_refreshing_legacy_conversion(self) -> None: [ [ # Property group filter, which is what's actually used these days PropertyGroupFilter( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[EventPropertyFilter(key="another", value="never_return_this", operator="is_not")], ) ], @@ -1377,10 +1377,10 @@ def test_insight_refreshing_legacy_with_background_update(self, spy_calculate_ta [ [ # Property group filter, which is what's actually used these days PropertyGroupFilter( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[EventPropertyFilter(key="another", value="never_return_this", operator="is_not")], ) ], @@ -1461,10 +1461,10 @@ def test_insight_refreshing_query_with_background_update( [ [ # Property group filter, which is what's actually used these days PropertyGroupFilter( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[EventPropertyFilter(key="another", value="never_return_this", operator="is_not")], ) ], diff --git a/posthog/api/test/test_query.py b/posthog/api/test/test_query.py index 099aebf055697..4ec129186fcb9 100644 --- a/posthog/api/test/test_query.py +++ b/posthog/api/test/test_query.py @@ -261,7 +261,7 @@ def test_event_property_filter(self): type="event", key="key", value="test_val3", - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, ) ] response = self.client.post(f"/api/projects/{self.team.id}/query/", {"query": query.dict()}).json() @@ -272,7 +272,7 @@ def test_event_property_filter(self): type="event", key="path", value="/", - operator=PropertyOperator.icontains, + operator=PropertyOperator.ICONTAINS, ) ] response = self.client.post(f"/api/projects/{self.team.id}/query/", {"query": query.dict()}).json() @@ -331,7 +331,7 @@ def test_person_property_filter(self): type="person", key="email", value="tom@posthog.com", - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, ) ], ) diff --git a/posthog/celery.py b/posthog/celery.py index 29c45c9b60729..00c039de17864 100644 --- a/posthog/celery.py +++ b/posthog/celery.py @@ -43,7 +43,7 @@ CELERY_TASK_RETRY_COUNTER = Counter( "posthog_celery_task_retry", "task retry signal is dispatched when a task will be retried.", - labelnames=["task_name"], + labelnames=["task_name", "reason"], ) @@ -145,8 +145,8 @@ def failure_signal_handler(sender, **kwargs): @task_retry.connect -def retry_signal_handler(sender, **kwargs): - CELERY_TASK_RETRY_COUNTER.labels(task_name=sender.name).inc() +def retry_signal_handler(sender, reason, **kwargs): + CELERY_TASK_RETRY_COUNTER.labels(task_name=sender.name, reason=str(reason)).inc() @app.on_after_finalize.connect diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index d296a354b7c50..d9e71e34e4cac 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -717,6 +717,7 @@ class WindowExpr(Expr): class WindowFunction(Expr): name: str args: Optional[list[Expr]] = None + exprs: Optional[list[Expr]] = None over_expr: Optional[WindowExpr] = None over_identifier: Optional[str] = None diff --git a/posthog/hogql/autocomplete.py b/posthog/hogql/autocomplete.py index 4440368bccc1b..0f73304061f33 100644 --- a/posthog/hogql/autocomplete.py +++ b/posthog/hogql/autocomplete.py @@ -258,7 +258,7 @@ def append_table_field_to_response(table: Table, suggestions: list[AutocompleteC extend_responses( available_functions, suggestions, - Kind.Function, + Kind.FUNCTION, insert_text=lambda key: f"{key}()", ) @@ -266,7 +266,7 @@ def append_table_field_to_response(table: Table, suggestions: list[AutocompleteC def extend_responses( keys: list[str], suggestions: list[AutocompleteCompletionItem], - kind: Kind = Kind.Variable, + kind: Kind = Kind.VARIABLE, insert_text: Optional[Callable[[str], str]] = None, details: Optional[list[str | None]] = None, ) -> None: @@ -365,7 +365,7 @@ def get_hogql_autocomplete( extend_responses( keys=table_aliases, suggestions=response.suggestions, - kind=Kind.Folder, + kind=Kind.FOLDER, details=["Table"] * len(table_aliases), ) break @@ -459,7 +459,7 @@ def get_hogql_autocomplete( extend_responses( keys=table_names, suggestions=response.suggestions, - kind=Kind.Folder, + kind=Kind.FOLDER, details=["Table"] * len(table_names), ) except Exception: diff --git a/posthog/hogql/database/database.py b/posthog/hogql/database/database.py index e7a21888e112d..ce714239d43d7 100644 --- a/posthog/hogql/database/database.py +++ b/posthog/hogql/database/database.py @@ -217,21 +217,21 @@ def create_hogql_database( modifiers = create_default_modifiers_for_team(team, modifiers) database = Database(timezone=team.timezone, week_start_day=team.week_start_day) - if modifiers.personsOnEventsMode == PersonsOnEventsMode.disabled: + if modifiers.personsOnEventsMode == PersonsOnEventsMode.DISABLED: # no change database.events.fields["person"] = FieldTraverser(chain=["pdi", "person"]) database.events.fields["person_id"] = FieldTraverser(chain=["pdi", "person_id"]) - elif modifiers.personsOnEventsMode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + elif modifiers.personsOnEventsMode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: database.events.fields["person_id"] = StringDatabaseField(name="person_id") _use_person_properties_from_events(database) - elif modifiers.personsOnEventsMode == PersonsOnEventsMode.person_id_override_properties_on_events: + elif modifiers.personsOnEventsMode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: _use_person_id_from_person_overrides(database) _use_person_properties_from_events(database) database.events.fields["poe"].fields["id"] = database.events.fields["person_id"] - elif modifiers.personsOnEventsMode == PersonsOnEventsMode.person_id_override_properties_joined: + elif modifiers.personsOnEventsMode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED: _use_person_id_from_person_overrides(database) database.events.fields["person"] = LazyJoin( from_field=["person_id"], @@ -509,23 +509,23 @@ def serialize_database( def constant_type_to_serialized_field_type(constant_type: ast.ConstantType) -> DatabaseSerializedFieldType | None: if isinstance(constant_type, ast.StringType): - return DatabaseSerializedFieldType.string + return DatabaseSerializedFieldType.STRING if isinstance(constant_type, ast.BooleanType): - return DatabaseSerializedFieldType.boolean + return DatabaseSerializedFieldType.BOOLEAN if isinstance(constant_type, ast.DateType): - return DatabaseSerializedFieldType.date + return DatabaseSerializedFieldType.DATE if isinstance(constant_type, ast.DateTimeType): - return DatabaseSerializedFieldType.datetime + return DatabaseSerializedFieldType.DATETIME if isinstance(constant_type, ast.UUIDType): - return DatabaseSerializedFieldType.string + return DatabaseSerializedFieldType.STRING if isinstance(constant_type, ast.ArrayType): - return DatabaseSerializedFieldType.array + return DatabaseSerializedFieldType.ARRAY if isinstance(constant_type, ast.TupleType): - return DatabaseSerializedFieldType.json + return DatabaseSerializedFieldType.JSON if isinstance(constant_type, ast.IntegerType): - return DatabaseSerializedFieldType.integer + return DatabaseSerializedFieldType.INTEGER if isinstance(constant_type, ast.FloatType): - return DatabaseSerializedFieldType.float + return DatabaseSerializedFieldType.FLOAT return None @@ -569,7 +569,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.integer, + type=DatabaseSerializedFieldType.INTEGER, schema_valid=schema_valid, ) ) @@ -578,7 +578,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.float, + type=DatabaseSerializedFieldType.FLOAT, schema_valid=schema_valid, ) ) @@ -587,7 +587,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.string, + type=DatabaseSerializedFieldType.STRING, schema_valid=schema_valid, ) ) @@ -596,7 +596,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.datetime, + type=DatabaseSerializedFieldType.DATETIME, schema_valid=schema_valid, ) ) @@ -605,7 +605,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.date, + type=DatabaseSerializedFieldType.DATE, schema_valid=schema_valid, ) ) @@ -614,7 +614,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.boolean, + type=DatabaseSerializedFieldType.BOOLEAN, schema_valid=schema_valid, ) ) @@ -623,7 +623,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.json, + type=DatabaseSerializedFieldType.JSON, schema_valid=schema_valid, ) ) @@ -632,7 +632,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.array, + type=DatabaseSerializedFieldType.ARRAY, schema_valid=schema_valid, ) ) @@ -643,7 +643,7 @@ def serialize_fields( field_type = constant_type_to_serialized_field_type(constant_type) if field_type is None: - field_type = DatabaseSerializedFieldType.expression + field_type = DatabaseSerializedFieldType.EXPRESSION field_output.append( DatabaseSchemaField( @@ -659,7 +659,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.view if is_view else DatabaseSerializedFieldType.lazy_table, + type=DatabaseSerializedFieldType.VIEW if is_view else DatabaseSerializedFieldType.LAZY_TABLE, schema_valid=schema_valid, table=field.resolve_table(context).to_printed_hogql(), fields=list(field.resolve_table(context).fields.keys()), @@ -670,7 +670,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.virtual_table, + type=DatabaseSerializedFieldType.VIRTUAL_TABLE, schema_valid=schema_valid, table=field.to_printed_hogql(), fields=list(field.fields.keys()), @@ -681,7 +681,7 @@ def serialize_fields( DatabaseSchemaField( name=field_key, hogql_value=hogql_value, - type=DatabaseSerializedFieldType.field_traverser, + type=DatabaseSerializedFieldType.FIELD_TRAVERSER, schema_valid=schema_valid, chain=field.chain, ) diff --git a/posthog/hogql/database/schema/persons.py b/posthog/hogql/database/schema/persons.py index 2af0a95381b67..54cf36645f506 100644 --- a/posthog/hogql/database/schema/persons.py +++ b/posthog/hogql/database/schema/persons.py @@ -40,15 +40,15 @@ def select_from_persons_table(join_or_table: LazyJoinToAdd | LazyTableToAdd, context: HogQLContext, node: SelectQuery): version = context.modifiers.personsArgMaxVersion - if version == PersonsArgMaxVersion.auto: - version = PersonsArgMaxVersion.v1 + if version == PersonsArgMaxVersion.AUTO: + version = PersonsArgMaxVersion.V1 # If selecting properties, use the faster v2 query. Otherwise, v1 is faster. for field_chain in join_or_table.fields_accessed.values(): if field_chain[0] == "properties": - version = PersonsArgMaxVersion.v2 + version = PersonsArgMaxVersion.V2 break - if version == PersonsArgMaxVersion.v2: + if version == PersonsArgMaxVersion.V2: from posthog.hogql import ast from posthog.hogql.parser import parse_select diff --git a/posthog/hogql/database/schema/sessions.py b/posthog/hogql/database/schema/sessions.py index 586da5d102206..fba4f4656b012 100644 --- a/posthog/hogql/database/schema/sessions.py +++ b/posthog/hogql/database/schema/sessions.py @@ -198,7 +198,7 @@ def arg_max_merge_field(field_name: str) -> ast.Call: args=[aggregate_fields["$urls"]], ) - if context.modifiers.bounceRatePageViewMode == BounceRatePageViewMode.uniq_urls: + if context.modifiers.bounceRatePageViewMode == BounceRatePageViewMode.UNIQ_URLS: bounce_pageview_count = aggregate_fields["$num_uniq_urls"] else: bounce_pageview_count = aggregate_fields["$pageview_count"] diff --git a/posthog/hogql/database/schema/test/test_sessions.py b/posthog/hogql/database/schema/test/test_sessions.py index 53e13beee53c5..e17f8208b567c 100644 --- a/posthog/hogql/database/schema/test/test_sessions.py +++ b/posthog/hogql/database/schema/test/test_sessions.py @@ -142,7 +142,7 @@ def test_persons_and_sessions_on_events(self): self.assertEqual(row1, (p1.uuid, "source1")) self.assertEqual(row2, (p2.uuid, "source2")) - @parameterized.expand([(BounceRatePageViewMode.uniq_urls,), (BounceRatePageViewMode.count_pageviews,)]) + @parameterized.expand([(BounceRatePageViewMode.UNIQ_URLS,), (BounceRatePageViewMode.COUNT_PAGEVIEWS,)]) def test_bounce_rate(self, bounceRatePageViewMode): # person with 2 different sessions _create_event( diff --git a/posthog/hogql/database/schema/util/test/test_person_where_clause_extractor.py b/posthog/hogql/database/schema/util/test/test_person_where_clause_extractor.py index 65f2be22a2c17..b58a03fe15647 100644 --- a/posthog/hogql/database/schema/util/test/test_person_where_clause_extractor.py +++ b/posthog/hogql/database/schema/util/test/test_person_where_clause_extractor.py @@ -40,8 +40,8 @@ def get_clause(self, query: str): team = self.team modifiers = create_default_modifiers_for_team(team) modifiers.optimizeJoinedFilters = True - modifiers.personsOnEventsMode = PersonsOnEventsMode.disabled - modifiers.personsArgMaxVersion = PersonsArgMaxVersion.v1 + modifiers.personsOnEventsMode = PersonsOnEventsMode.DISABLED + modifiers.personsArgMaxVersion = PersonsArgMaxVersion.V1 context = HogQLContext( team_id=team.pk, team=team, diff --git a/posthog/hogql/database/test/test_database.py b/posthog/hogql/database/test/test_database.py index 3e733b4cb22db..cb380bce9da94 100644 --- a/posthog/hogql/database/test/test_database.py +++ b/posthog/hogql/database/test/test_database.py @@ -474,7 +474,7 @@ def test_selecting_persons_from_events_ignores_future_persons(self): database=db, # disable PoE modifiers=create_default_modifiers_for_team( - self.team, HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.disabled) + self.team, HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.DISABLED) ), ) sql = "select person.id from events" diff --git a/posthog/hogql/functions/mapping.py b/posthog/hogql/functions/mapping.py index 2c7052905d46d..bda37830c4b37 100644 --- a/posthog/hogql/functions/mapping.py +++ b/posthog/hogql/functions/mapping.py @@ -854,6 +854,7 @@ def compare_types(arg_types: list[ConstantType], sig_arg_types: tuple[ConstantTy "covarPopIf": HogQLFunctionMeta("covarPopIf", 3, 3, aggregate=True), "covarSamp": HogQLFunctionMeta("covarSamp", 2, 2, aggregate=True), "covarSampIf": HogQLFunctionMeta("covarSampIf", 3, 3, aggregate=True), + "corr": HogQLFunctionMeta("corr", 2, 2, aggregate=True), # ClickHouse-specific aggregate functions "anyHeavy": HogQLFunctionMeta("anyHeavy", 1, 1, aggregate=True), "anyHeavyIf": HogQLFunctionMeta("anyHeavyIf", 2, 2, aggregate=True), diff --git a/posthog/hogql/grammar/HogQLParser.g4 b/posthog/hogql/grammar/HogQLParser.g4 index bc3c954de10aa..911f5827073d0 100644 --- a/posthog/hogql/grammar/HogQLParser.g4 +++ b/posthog/hogql/grammar/HogQLParser.g4 @@ -140,9 +140,9 @@ columnExpr | SUBSTRING LPAREN columnExpr FROM columnExpr (FOR columnExpr)? RPAREN # ColumnExprSubstring | TIMESTAMP STRING_LITERAL # ColumnExprTimestamp | TRIM LPAREN (BOTH | LEADING | TRAILING) string FROM columnExpr RPAREN # ColumnExprTrim - | identifier (LPAREN columnExprList? RPAREN) OVER LPAREN windowExpr RPAREN # ColumnExprWinFunction - | identifier (LPAREN columnExprList? RPAREN) OVER identifier # ColumnExprWinFunctionTarget - | identifier (LPAREN columnExprList? RPAREN)? LPAREN DISTINCT? columnArgList? RPAREN # ColumnExprFunction + | identifier (LPAREN columnExprList? RPAREN) (LPAREN DISTINCT? columnArgList? RPAREN)? OVER LPAREN windowExpr RPAREN # ColumnExprWinFunction + | identifier (LPAREN columnExprList? RPAREN) (LPAREN DISTINCT? columnArgList? RPAREN)? OVER identifier # ColumnExprWinFunctionTarget + | identifier (LPAREN columnExprList? RPAREN)? LPAREN DISTINCT? columnArgList? RPAREN # ColumnExprFunction | hogqlxTagElement # ColumnExprTagElement | templateString # ColumnExprTemplateString | literal # ColumnExprLiteral diff --git a/posthog/hogql/grammar/HogQLParser.interp b/posthog/hogql/grammar/HogQLParser.interp index a2f030a7eb8fa..086eca220c32f 100644 --- a/posthog/hogql/grammar/HogQLParser.interp +++ b/posthog/hogql/grammar/HogQLParser.interp @@ -399,4 +399,4 @@ stringContentsFull atn: -[4, 1, 154, 1158, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 199, 8, 5, 10, 5, 12, 5, 202, 9, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 213, 8, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 225, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 241, 8, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 250, 8, 13, 10, 13, 12, 13, 253, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 264, 8, 15, 10, 15, 12, 15, 267, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 272, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 280, 8, 17, 10, 17, 12, 17, 283, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 291, 8, 18, 1, 19, 3, 19, 294, 8, 19, 1, 19, 1, 19, 3, 19, 298, 8, 19, 1, 19, 3, 19, 301, 8, 19, 1, 19, 1, 19, 3, 19, 305, 8, 19, 1, 19, 3, 19, 308, 8, 19, 1, 19, 3, 19, 311, 8, 19, 1, 19, 3, 19, 314, 8, 19, 1, 19, 3, 19, 317, 8, 19, 1, 19, 1, 19, 3, 19, 321, 8, 19, 1, 19, 1, 19, 3, 19, 325, 8, 19, 1, 19, 3, 19, 328, 8, 19, 1, 19, 3, 19, 331, 8, 19, 1, 19, 3, 19, 334, 8, 19, 1, 19, 1, 19, 3, 19, 338, 8, 19, 1, 19, 3, 19, 341, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 350, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 356, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 375, 8, 24, 10, 24, 12, 24, 378, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 394, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 411, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 417, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 423, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 434, 8, 31, 3, 31, 436, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 447, 8, 34, 1, 34, 3, 34, 450, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 456, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 464, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 470, 8, 34, 10, 34, 12, 34, 473, 9, 34, 1, 35, 3, 35, 476, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 481, 8, 35, 1, 35, 3, 35, 484, 8, 35, 1, 35, 3, 35, 487, 8, 35, 1, 35, 1, 35, 3, 35, 491, 8, 35, 1, 35, 1, 35, 3, 35, 495, 8, 35, 1, 35, 3, 35, 498, 8, 35, 3, 35, 500, 8, 35, 1, 35, 3, 35, 503, 8, 35, 1, 35, 1, 35, 3, 35, 507, 8, 35, 1, 35, 1, 35, 3, 35, 511, 8, 35, 1, 35, 3, 35, 514, 8, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 523, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 534, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 540, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 545, 8, 39, 10, 39, 12, 39, 548, 9, 39, 1, 40, 1, 40, 3, 40, 552, 8, 40, 1, 40, 1, 40, 3, 40, 556, 8, 40, 1, 40, 1, 40, 3, 40, 560, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 566, 8, 41, 3, 41, 568, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 573, 8, 42, 10, 42, 12, 42, 576, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 583, 8, 44, 1, 44, 3, 44, 586, 8, 44, 1, 44, 3, 44, 589, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 608, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 622, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 636, 8, 51, 10, 51, 12, 51, 639, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 648, 8, 51, 10, 51, 12, 51, 651, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 660, 8, 51, 10, 51, 12, 51, 663, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 670, 8, 51, 1, 51, 1, 51, 3, 51, 674, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 679, 8, 52, 10, 52, 12, 52, 682, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 687, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 694, 8, 53, 11, 53, 12, 53, 695, 1, 53, 1, 53, 3, 53, 700, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 724, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 741, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 753, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 763, 8, 53, 1, 53, 3, 53, 766, 8, 53, 1, 53, 1, 53, 3, 53, 770, 8, 53, 1, 53, 3, 53, 773, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 787, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 804, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 809, 8, 53, 1, 53, 1, 53, 3, 53, 813, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 819, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 826, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 838, 8, 53, 1, 53, 1, 53, 3, 53, 842, 8, 53, 1, 53, 3, 53, 845, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 854, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 868, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 895, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 904, 8, 53, 5, 53, 906, 8, 53, 10, 53, 12, 53, 909, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 914, 8, 54, 10, 54, 12, 54, 917, 9, 54, 1, 55, 1, 55, 3, 55, 921, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 927, 8, 56, 10, 56, 12, 56, 930, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 937, 8, 56, 10, 56, 12, 56, 940, 9, 56, 3, 56, 942, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 950, 8, 57, 10, 57, 12, 57, 953, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 961, 8, 57, 10, 57, 12, 57, 964, 9, 57, 1, 57, 1, 57, 3, 57, 968, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 975, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 988, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 993, 8, 59, 10, 59, 12, 59, 996, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1008, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1014, 8, 61, 1, 61, 3, 61, 1017, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1022, 8, 62, 10, 62, 12, 62, 1025, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1036, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1042, 8, 63, 5, 63, 1044, 8, 63, 10, 63, 12, 63, 1047, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1052, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1059, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1066, 8, 66, 10, 66, 12, 66, 1069, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1079, 8, 68, 3, 68, 1081, 8, 68, 1, 69, 3, 69, 1084, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1092, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1097, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1107, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1112, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1124, 8, 78, 1, 79, 1, 79, 5, 79, 1128, 8, 79, 10, 79, 12, 79, 1131, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1140, 8, 80, 1, 81, 1, 81, 5, 81, 1144, 8, 81, 10, 81, 12, 81, 1147, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1156, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1288, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 189, 1, 0, 0, 0, 10, 195, 1, 0, 0, 0, 12, 212, 1, 0, 0, 0, 14, 214, 1, 0, 0, 0, 16, 217, 1, 0, 0, 0, 18, 226, 1, 0, 0, 0, 20, 232, 1, 0, 0, 0, 22, 236, 1, 0, 0, 0, 24, 245, 1, 0, 0, 0, 26, 247, 1, 0, 0, 0, 28, 256, 1, 0, 0, 0, 30, 260, 1, 0, 0, 0, 32, 271, 1, 0, 0, 0, 34, 275, 1, 0, 0, 0, 36, 290, 1, 0, 0, 0, 38, 293, 1, 0, 0, 0, 40, 342, 1, 0, 0, 0, 42, 345, 1, 0, 0, 0, 44, 351, 1, 0, 0, 0, 46, 355, 1, 0, 0, 0, 48, 361, 1, 0, 0, 0, 50, 379, 1, 0, 0, 0, 52, 382, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 395, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 402, 1, 0, 0, 0, 62, 435, 1, 0, 0, 0, 64, 437, 1, 0, 0, 0, 66, 440, 1, 0, 0, 0, 68, 455, 1, 0, 0, 0, 70, 517, 1, 0, 0, 0, 72, 522, 1, 0, 0, 0, 74, 533, 1, 0, 0, 0, 76, 535, 1, 0, 0, 0, 78, 541, 1, 0, 0, 0, 80, 549, 1, 0, 0, 0, 82, 567, 1, 0, 0, 0, 84, 569, 1, 0, 0, 0, 86, 577, 1, 0, 0, 0, 88, 582, 1, 0, 0, 0, 90, 590, 1, 0, 0, 0, 92, 594, 1, 0, 0, 0, 94, 598, 1, 0, 0, 0, 96, 607, 1, 0, 0, 0, 98, 621, 1, 0, 0, 0, 100, 623, 1, 0, 0, 0, 102, 673, 1, 0, 0, 0, 104, 675, 1, 0, 0, 0, 106, 812, 1, 0, 0, 0, 108, 910, 1, 0, 0, 0, 110, 920, 1, 0, 0, 0, 112, 941, 1, 0, 0, 0, 114, 974, 1, 0, 0, 0, 116, 987, 1, 0, 0, 0, 118, 989, 1, 0, 0, 0, 120, 1007, 1, 0, 0, 0, 122, 1016, 1, 0, 0, 0, 124, 1018, 1, 0, 0, 0, 126, 1035, 1, 0, 0, 0, 128, 1048, 1, 0, 0, 0, 130, 1058, 1, 0, 0, 0, 132, 1062, 1, 0, 0, 0, 134, 1070, 1, 0, 0, 0, 136, 1080, 1, 0, 0, 0, 138, 1083, 1, 0, 0, 0, 140, 1096, 1, 0, 0, 0, 142, 1098, 1, 0, 0, 0, 144, 1100, 1, 0, 0, 0, 146, 1102, 1, 0, 0, 0, 148, 1106, 1, 0, 0, 0, 150, 1111, 1, 0, 0, 0, 152, 1113, 1, 0, 0, 0, 154, 1117, 1, 0, 0, 0, 156, 1123, 1, 0, 0, 0, 158, 1125, 1, 0, 0, 0, 160, 1139, 1, 0, 0, 0, 162, 1141, 1, 0, 0, 0, 164, 1155, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 12, 6, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 188, 5, 145, 0, 0, 188, 7, 1, 0, 0, 0, 189, 190, 3, 4, 2, 0, 190, 191, 5, 111, 0, 0, 191, 192, 5, 118, 0, 0, 192, 193, 3, 4, 2, 0, 193, 194, 5, 145, 0, 0, 194, 9, 1, 0, 0, 0, 195, 200, 3, 150, 75, 0, 196, 197, 5, 112, 0, 0, 197, 199, 3, 150, 75, 0, 198, 196, 1, 0, 0, 0, 199, 202, 1, 0, 0, 0, 200, 198, 1, 0, 0, 0, 200, 201, 1, 0, 0, 0, 201, 11, 1, 0, 0, 0, 202, 200, 1, 0, 0, 0, 203, 213, 3, 20, 10, 0, 204, 213, 3, 24, 12, 0, 205, 213, 3, 14, 7, 0, 206, 213, 3, 16, 8, 0, 207, 213, 3, 18, 9, 0, 208, 213, 3, 22, 11, 0, 209, 213, 3, 8, 4, 0, 210, 213, 3, 20, 10, 0, 211, 213, 3, 26, 13, 0, 212, 203, 1, 0, 0, 0, 212, 204, 1, 0, 0, 0, 212, 205, 1, 0, 0, 0, 212, 206, 1, 0, 0, 0, 212, 207, 1, 0, 0, 0, 212, 208, 1, 0, 0, 0, 212, 209, 1, 0, 0, 0, 212, 210, 1, 0, 0, 0, 212, 211, 1, 0, 0, 0, 213, 13, 1, 0, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 145, 0, 0, 216, 15, 1, 0, 0, 0, 217, 218, 5, 38, 0, 0, 218, 219, 5, 126, 0, 0, 219, 220, 3, 4, 2, 0, 220, 221, 5, 144, 0, 0, 221, 224, 3, 12, 6, 0, 222, 223, 5, 24, 0, 0, 223, 225, 3, 12, 6, 0, 224, 222, 1, 0, 0, 0, 224, 225, 1, 0, 0, 0, 225, 17, 1, 0, 0, 0, 226, 227, 5, 96, 0, 0, 227, 228, 5, 126, 0, 0, 228, 229, 3, 4, 2, 0, 229, 230, 5, 144, 0, 0, 230, 231, 3, 12, 6, 0, 231, 19, 1, 0, 0, 0, 232, 233, 5, 70, 0, 0, 233, 234, 3, 4, 2, 0, 234, 235, 5, 145, 0, 0, 235, 21, 1, 0, 0, 0, 236, 237, 5, 29, 0, 0, 237, 238, 3, 150, 75, 0, 238, 240, 5, 126, 0, 0, 239, 241, 3, 10, 5, 0, 240, 239, 1, 0, 0, 0, 240, 241, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 5, 144, 0, 0, 243, 244, 3, 26, 13, 0, 244, 23, 1, 0, 0, 0, 245, 246, 5, 145, 0, 0, 246, 25, 1, 0, 0, 0, 247, 251, 5, 124, 0, 0, 248, 250, 3, 2, 1, 0, 249, 248, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 254, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 255, 5, 142, 0, 0, 255, 27, 1, 0, 0, 0, 256, 257, 3, 4, 2, 0, 257, 258, 5, 111, 0, 0, 258, 259, 3, 4, 2, 0, 259, 29, 1, 0, 0, 0, 260, 265, 3, 28, 14, 0, 261, 262, 5, 112, 0, 0, 262, 264, 3, 28, 14, 0, 263, 261, 1, 0, 0, 0, 264, 267, 1, 0, 0, 0, 265, 263, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 31, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 272, 3, 34, 17, 0, 269, 272, 3, 38, 19, 0, 270, 272, 3, 114, 57, 0, 271, 268, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 271, 270, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 274, 5, 0, 0, 1, 274, 33, 1, 0, 0, 0, 275, 281, 3, 36, 18, 0, 276, 277, 5, 91, 0, 0, 277, 278, 5, 1, 0, 0, 278, 280, 3, 36, 18, 0, 279, 276, 1, 0, 0, 0, 280, 283, 1, 0, 0, 0, 281, 279, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 35, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 284, 291, 3, 38, 19, 0, 285, 286, 5, 126, 0, 0, 286, 287, 3, 34, 17, 0, 287, 288, 5, 144, 0, 0, 288, 291, 1, 0, 0, 0, 289, 291, 3, 154, 77, 0, 290, 284, 1, 0, 0, 0, 290, 285, 1, 0, 0, 0, 290, 289, 1, 0, 0, 0, 291, 37, 1, 0, 0, 0, 292, 294, 3, 40, 20, 0, 293, 292, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 297, 5, 77, 0, 0, 296, 298, 5, 23, 0, 0, 297, 296, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 300, 1, 0, 0, 0, 299, 301, 3, 42, 21, 0, 300, 299, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 304, 3, 104, 52, 0, 303, 305, 3, 44, 22, 0, 304, 303, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 307, 1, 0, 0, 0, 306, 308, 3, 46, 23, 0, 307, 306, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308, 310, 1, 0, 0, 0, 309, 311, 3, 50, 25, 0, 310, 309, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 313, 1, 0, 0, 0, 312, 314, 3, 52, 26, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 316, 1, 0, 0, 0, 315, 317, 3, 54, 27, 0, 316, 315, 1, 0, 0, 0, 316, 317, 1, 0, 0, 0, 317, 320, 1, 0, 0, 0, 318, 319, 5, 98, 0, 0, 319, 321, 7, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 324, 1, 0, 0, 0, 322, 323, 5, 98, 0, 0, 323, 325, 5, 86, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 327, 1, 0, 0, 0, 326, 328, 3, 56, 28, 0, 327, 326, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328, 330, 1, 0, 0, 0, 329, 331, 3, 48, 24, 0, 330, 329, 1, 0, 0, 0, 330, 331, 1, 0, 0, 0, 331, 333, 1, 0, 0, 0, 332, 334, 3, 58, 29, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 337, 1, 0, 0, 0, 335, 338, 3, 62, 31, 0, 336, 338, 3, 64, 32, 0, 337, 335, 1, 0, 0, 0, 337, 336, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 340, 1, 0, 0, 0, 339, 341, 3, 66, 33, 0, 340, 339, 1, 0, 0, 0, 340, 341, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 343, 5, 98, 0, 0, 343, 344, 3, 118, 59, 0, 344, 41, 1, 0, 0, 0, 345, 346, 5, 85, 0, 0, 346, 349, 5, 104, 0, 0, 347, 348, 5, 98, 0, 0, 348, 350, 5, 82, 0, 0, 349, 347, 1, 0, 0, 0, 349, 350, 1, 0, 0, 0, 350, 43, 1, 0, 0, 0, 351, 352, 5, 32, 0, 0, 352, 353, 3, 68, 34, 0, 353, 45, 1, 0, 0, 0, 354, 356, 7, 1, 0, 0, 355, 354, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 357, 1, 0, 0, 0, 357, 358, 5, 5, 0, 0, 358, 359, 5, 45, 0, 0, 359, 360, 3, 104, 52, 0, 360, 47, 1, 0, 0, 0, 361, 362, 5, 97, 0, 0, 362, 363, 3, 150, 75, 0, 363, 364, 5, 6, 0, 0, 364, 365, 5, 126, 0, 0, 365, 366, 3, 88, 44, 0, 366, 376, 5, 144, 0, 0, 367, 368, 5, 112, 0, 0, 368, 369, 3, 150, 75, 0, 369, 370, 5, 6, 0, 0, 370, 371, 5, 126, 0, 0, 371, 372, 3, 88, 44, 0, 372, 373, 5, 144, 0, 0, 373, 375, 1, 0, 0, 0, 374, 367, 1, 0, 0, 0, 375, 378, 1, 0, 0, 0, 376, 374, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 49, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 379, 380, 5, 67, 0, 0, 380, 381, 3, 106, 53, 0, 381, 51, 1, 0, 0, 0, 382, 383, 5, 95, 0, 0, 383, 384, 3, 106, 53, 0, 384, 53, 1, 0, 0, 0, 385, 386, 5, 34, 0, 0, 386, 393, 5, 11, 0, 0, 387, 388, 7, 0, 0, 0, 388, 389, 5, 126, 0, 0, 389, 390, 3, 104, 52, 0, 390, 391, 5, 144, 0, 0, 391, 394, 1, 0, 0, 0, 392, 394, 3, 104, 52, 0, 393, 387, 1, 0, 0, 0, 393, 392, 1, 0, 0, 0, 394, 55, 1, 0, 0, 0, 395, 396, 5, 35, 0, 0, 396, 397, 3, 106, 53, 0, 397, 57, 1, 0, 0, 0, 398, 399, 5, 62, 0, 0, 399, 400, 5, 11, 0, 0, 400, 401, 3, 78, 39, 0, 401, 59, 1, 0, 0, 0, 402, 403, 5, 62, 0, 0, 403, 404, 5, 11, 0, 0, 404, 405, 3, 104, 52, 0, 405, 61, 1, 0, 0, 0, 406, 407, 5, 52, 0, 0, 407, 410, 3, 106, 53, 0, 408, 409, 5, 112, 0, 0, 409, 411, 3, 106, 53, 0, 410, 408, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 416, 1, 0, 0, 0, 412, 413, 5, 98, 0, 0, 413, 417, 5, 82, 0, 0, 414, 415, 5, 11, 0, 0, 415, 417, 3, 104, 52, 0, 416, 412, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 416, 417, 1, 0, 0, 0, 417, 436, 1, 0, 0, 0, 418, 419, 5, 52, 0, 0, 419, 422, 3, 106, 53, 0, 420, 421, 5, 98, 0, 0, 421, 423, 5, 82, 0, 0, 422, 420, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 425, 5, 59, 0, 0, 425, 426, 3, 106, 53, 0, 426, 436, 1, 0, 0, 0, 427, 428, 5, 52, 0, 0, 428, 429, 3, 106, 53, 0, 429, 430, 5, 59, 0, 0, 430, 433, 3, 106, 53, 0, 431, 432, 5, 11, 0, 0, 432, 434, 3, 104, 52, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 436, 1, 0, 0, 0, 435, 406, 1, 0, 0, 0, 435, 418, 1, 0, 0, 0, 435, 427, 1, 0, 0, 0, 436, 63, 1, 0, 0, 0, 437, 438, 5, 59, 0, 0, 438, 439, 3, 106, 53, 0, 439, 65, 1, 0, 0, 0, 440, 441, 5, 79, 0, 0, 441, 442, 3, 84, 42, 0, 442, 67, 1, 0, 0, 0, 443, 444, 6, 34, -1, 0, 444, 446, 3, 126, 63, 0, 445, 447, 5, 27, 0, 0, 446, 445, 1, 0, 0, 0, 446, 447, 1, 0, 0, 0, 447, 449, 1, 0, 0, 0, 448, 450, 3, 76, 38, 0, 449, 448, 1, 0, 0, 0, 449, 450, 1, 0, 0, 0, 450, 456, 1, 0, 0, 0, 451, 452, 5, 126, 0, 0, 452, 453, 3, 68, 34, 0, 453, 454, 5, 144, 0, 0, 454, 456, 1, 0, 0, 0, 455, 443, 1, 0, 0, 0, 455, 451, 1, 0, 0, 0, 456, 471, 1, 0, 0, 0, 457, 458, 10, 3, 0, 0, 458, 459, 3, 72, 36, 0, 459, 460, 3, 68, 34, 4, 460, 470, 1, 0, 0, 0, 461, 463, 10, 4, 0, 0, 462, 464, 3, 70, 35, 0, 463, 462, 1, 0, 0, 0, 463, 464, 1, 0, 0, 0, 464, 465, 1, 0, 0, 0, 465, 466, 5, 45, 0, 0, 466, 467, 3, 68, 34, 0, 467, 468, 3, 74, 37, 0, 468, 470, 1, 0, 0, 0, 469, 457, 1, 0, 0, 0, 469, 461, 1, 0, 0, 0, 470, 473, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 471, 472, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 474, 476, 7, 2, 0, 0, 475, 474, 1, 0, 0, 0, 475, 476, 1, 0, 0, 0, 476, 477, 1, 0, 0, 0, 477, 484, 5, 42, 0, 0, 478, 480, 5, 42, 0, 0, 479, 481, 7, 2, 0, 0, 480, 479, 1, 0, 0, 0, 480, 481, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 484, 7, 2, 0, 0, 483, 475, 1, 0, 0, 0, 483, 478, 1, 0, 0, 0, 483, 482, 1, 0, 0, 0, 484, 518, 1, 0, 0, 0, 485, 487, 7, 3, 0, 0, 486, 485, 1, 0, 0, 0, 486, 487, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 490, 7, 4, 0, 0, 489, 491, 5, 63, 0, 0, 490, 489, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 500, 1, 0, 0, 0, 492, 494, 7, 4, 0, 0, 493, 495, 5, 63, 0, 0, 494, 493, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 497, 1, 0, 0, 0, 496, 498, 7, 3, 0, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 500, 1, 0, 0, 0, 499, 486, 1, 0, 0, 0, 499, 492, 1, 0, 0, 0, 500, 518, 1, 0, 0, 0, 501, 503, 7, 5, 0, 0, 502, 501, 1, 0, 0, 0, 502, 503, 1, 0, 0, 0, 503, 504, 1, 0, 0, 0, 504, 506, 5, 33, 0, 0, 505, 507, 5, 63, 0, 0, 506, 505, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 516, 1, 0, 0, 0, 508, 510, 5, 33, 0, 0, 509, 511, 5, 63, 0, 0, 510, 509, 1, 0, 0, 0, 510, 511, 1, 0, 0, 0, 511, 513, 1, 0, 0, 0, 512, 514, 7, 5, 0, 0, 513, 512, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 516, 1, 0, 0, 0, 515, 502, 1, 0, 0, 0, 515, 508, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 483, 1, 0, 0, 0, 517, 499, 1, 0, 0, 0, 517, 515, 1, 0, 0, 0, 518, 71, 1, 0, 0, 0, 519, 520, 5, 16, 0, 0, 520, 523, 5, 45, 0, 0, 521, 523, 5, 112, 0, 0, 522, 519, 1, 0, 0, 0, 522, 521, 1, 0, 0, 0, 523, 73, 1, 0, 0, 0, 524, 525, 5, 60, 0, 0, 525, 534, 3, 104, 52, 0, 526, 527, 5, 92, 0, 0, 527, 528, 5, 126, 0, 0, 528, 529, 3, 104, 52, 0, 529, 530, 5, 144, 0, 0, 530, 534, 1, 0, 0, 0, 531, 532, 5, 92, 0, 0, 532, 534, 3, 104, 52, 0, 533, 524, 1, 0, 0, 0, 533, 526, 1, 0, 0, 0, 533, 531, 1, 0, 0, 0, 534, 75, 1, 0, 0, 0, 535, 536, 5, 75, 0, 0, 536, 539, 3, 82, 41, 0, 537, 538, 5, 59, 0, 0, 538, 540, 3, 82, 41, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 77, 1, 0, 0, 0, 541, 546, 3, 80, 40, 0, 542, 543, 5, 112, 0, 0, 543, 545, 3, 80, 40, 0, 544, 542, 1, 0, 0, 0, 545, 548, 1, 0, 0, 0, 546, 544, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 79, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 549, 551, 3, 106, 53, 0, 550, 552, 7, 6, 0, 0, 551, 550, 1, 0, 0, 0, 551, 552, 1, 0, 0, 0, 552, 555, 1, 0, 0, 0, 553, 554, 5, 58, 0, 0, 554, 556, 7, 7, 0, 0, 555, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 559, 1, 0, 0, 0, 557, 558, 5, 15, 0, 0, 558, 560, 5, 106, 0, 0, 559, 557, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 81, 1, 0, 0, 0, 561, 568, 3, 154, 77, 0, 562, 565, 3, 138, 69, 0, 563, 564, 5, 146, 0, 0, 564, 566, 3, 138, 69, 0, 565, 563, 1, 0, 0, 0, 565, 566, 1, 0, 0, 0, 566, 568, 1, 0, 0, 0, 567, 561, 1, 0, 0, 0, 567, 562, 1, 0, 0, 0, 568, 83, 1, 0, 0, 0, 569, 574, 3, 86, 43, 0, 570, 571, 5, 112, 0, 0, 571, 573, 3, 86, 43, 0, 572, 570, 1, 0, 0, 0, 573, 576, 1, 0, 0, 0, 574, 572, 1, 0, 0, 0, 574, 575, 1, 0, 0, 0, 575, 85, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 577, 578, 3, 150, 75, 0, 578, 579, 5, 118, 0, 0, 579, 580, 3, 140, 70, 0, 580, 87, 1, 0, 0, 0, 581, 583, 3, 90, 45, 0, 582, 581, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 585, 1, 0, 0, 0, 584, 586, 3, 92, 46, 0, 585, 584, 1, 0, 0, 0, 585, 586, 1, 0, 0, 0, 586, 588, 1, 0, 0, 0, 587, 589, 3, 94, 47, 0, 588, 587, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 89, 1, 0, 0, 0, 590, 591, 5, 65, 0, 0, 591, 592, 5, 11, 0, 0, 592, 593, 3, 104, 52, 0, 593, 91, 1, 0, 0, 0, 594, 595, 5, 62, 0, 0, 595, 596, 5, 11, 0, 0, 596, 597, 3, 78, 39, 0, 597, 93, 1, 0, 0, 0, 598, 599, 7, 8, 0, 0, 599, 600, 3, 96, 48, 0, 600, 95, 1, 0, 0, 0, 601, 608, 3, 98, 49, 0, 602, 603, 5, 9, 0, 0, 603, 604, 3, 98, 49, 0, 604, 605, 5, 2, 0, 0, 605, 606, 3, 98, 49, 0, 606, 608, 1, 0, 0, 0, 607, 601, 1, 0, 0, 0, 607, 602, 1, 0, 0, 0, 608, 97, 1, 0, 0, 0, 609, 610, 5, 18, 0, 0, 610, 622, 5, 73, 0, 0, 611, 612, 5, 90, 0, 0, 612, 622, 5, 66, 0, 0, 613, 614, 5, 90, 0, 0, 614, 622, 5, 30, 0, 0, 615, 616, 3, 138, 69, 0, 616, 617, 5, 66, 0, 0, 617, 622, 1, 0, 0, 0, 618, 619, 3, 138, 69, 0, 619, 620, 5, 30, 0, 0, 620, 622, 1, 0, 0, 0, 621, 609, 1, 0, 0, 0, 621, 611, 1, 0, 0, 0, 621, 613, 1, 0, 0, 0, 621, 615, 1, 0, 0, 0, 621, 618, 1, 0, 0, 0, 622, 99, 1, 0, 0, 0, 623, 624, 3, 106, 53, 0, 624, 625, 5, 0, 0, 1, 625, 101, 1, 0, 0, 0, 626, 674, 3, 150, 75, 0, 627, 628, 3, 150, 75, 0, 628, 629, 5, 126, 0, 0, 629, 630, 3, 150, 75, 0, 630, 637, 3, 102, 51, 0, 631, 632, 5, 112, 0, 0, 632, 633, 3, 150, 75, 0, 633, 634, 3, 102, 51, 0, 634, 636, 1, 0, 0, 0, 635, 631, 1, 0, 0, 0, 636, 639, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 637, 638, 1, 0, 0, 0, 638, 640, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 640, 641, 5, 144, 0, 0, 641, 674, 1, 0, 0, 0, 642, 643, 3, 150, 75, 0, 643, 644, 5, 126, 0, 0, 644, 649, 3, 152, 76, 0, 645, 646, 5, 112, 0, 0, 646, 648, 3, 152, 76, 0, 647, 645, 1, 0, 0, 0, 648, 651, 1, 0, 0, 0, 649, 647, 1, 0, 0, 0, 649, 650, 1, 0, 0, 0, 650, 652, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 652, 653, 5, 144, 0, 0, 653, 674, 1, 0, 0, 0, 654, 655, 3, 150, 75, 0, 655, 656, 5, 126, 0, 0, 656, 661, 3, 102, 51, 0, 657, 658, 5, 112, 0, 0, 658, 660, 3, 102, 51, 0, 659, 657, 1, 0, 0, 0, 660, 663, 1, 0, 0, 0, 661, 659, 1, 0, 0, 0, 661, 662, 1, 0, 0, 0, 662, 664, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 664, 665, 5, 144, 0, 0, 665, 674, 1, 0, 0, 0, 666, 667, 3, 150, 75, 0, 667, 669, 5, 126, 0, 0, 668, 670, 3, 104, 52, 0, 669, 668, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 672, 5, 144, 0, 0, 672, 674, 1, 0, 0, 0, 673, 626, 1, 0, 0, 0, 673, 627, 1, 0, 0, 0, 673, 642, 1, 0, 0, 0, 673, 654, 1, 0, 0, 0, 673, 666, 1, 0, 0, 0, 674, 103, 1, 0, 0, 0, 675, 680, 3, 106, 53, 0, 676, 677, 5, 112, 0, 0, 677, 679, 3, 106, 53, 0, 678, 676, 1, 0, 0, 0, 679, 682, 1, 0, 0, 0, 680, 678, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 681, 105, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 683, 684, 6, 53, -1, 0, 684, 686, 5, 12, 0, 0, 685, 687, 3, 106, 53, 0, 686, 685, 1, 0, 0, 0, 686, 687, 1, 0, 0, 0, 687, 693, 1, 0, 0, 0, 688, 689, 5, 94, 0, 0, 689, 690, 3, 106, 53, 0, 690, 691, 5, 81, 0, 0, 691, 692, 3, 106, 53, 0, 692, 694, 1, 0, 0, 0, 693, 688, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 693, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 699, 1, 0, 0, 0, 697, 698, 5, 24, 0, 0, 698, 700, 3, 106, 53, 0, 699, 697, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 702, 5, 25, 0, 0, 702, 813, 1, 0, 0, 0, 703, 704, 5, 13, 0, 0, 704, 705, 5, 126, 0, 0, 705, 706, 3, 106, 53, 0, 706, 707, 5, 6, 0, 0, 707, 708, 3, 102, 51, 0, 708, 709, 5, 144, 0, 0, 709, 813, 1, 0, 0, 0, 710, 711, 5, 19, 0, 0, 711, 813, 5, 106, 0, 0, 712, 713, 5, 43, 0, 0, 713, 714, 3, 106, 53, 0, 714, 715, 3, 142, 71, 0, 715, 813, 1, 0, 0, 0, 716, 717, 5, 80, 0, 0, 717, 718, 5, 126, 0, 0, 718, 719, 3, 106, 53, 0, 719, 720, 5, 32, 0, 0, 720, 723, 3, 106, 53, 0, 721, 722, 5, 31, 0, 0, 722, 724, 3, 106, 53, 0, 723, 721, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 144, 0, 0, 726, 813, 1, 0, 0, 0, 727, 728, 5, 83, 0, 0, 728, 813, 5, 106, 0, 0, 729, 730, 5, 88, 0, 0, 730, 731, 5, 126, 0, 0, 731, 732, 7, 9, 0, 0, 732, 733, 3, 156, 78, 0, 733, 734, 5, 32, 0, 0, 734, 735, 3, 106, 53, 0, 735, 736, 5, 144, 0, 0, 736, 813, 1, 0, 0, 0, 737, 738, 3, 150, 75, 0, 738, 740, 5, 126, 0, 0, 739, 741, 3, 104, 52, 0, 740, 739, 1, 0, 0, 0, 740, 741, 1, 0, 0, 0, 741, 742, 1, 0, 0, 0, 742, 743, 5, 144, 0, 0, 743, 744, 1, 0, 0, 0, 744, 745, 5, 64, 0, 0, 745, 746, 5, 126, 0, 0, 746, 747, 3, 88, 44, 0, 747, 748, 5, 144, 0, 0, 748, 813, 1, 0, 0, 0, 749, 750, 3, 150, 75, 0, 750, 752, 5, 126, 0, 0, 751, 753, 3, 104, 52, 0, 752, 751, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 754, 1, 0, 0, 0, 754, 755, 5, 144, 0, 0, 755, 756, 1, 0, 0, 0, 756, 757, 5, 64, 0, 0, 757, 758, 3, 150, 75, 0, 758, 813, 1, 0, 0, 0, 759, 765, 3, 150, 75, 0, 760, 762, 5, 126, 0, 0, 761, 763, 3, 104, 52, 0, 762, 761, 1, 0, 0, 0, 762, 763, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 766, 5, 144, 0, 0, 765, 760, 1, 0, 0, 0, 765, 766, 1, 0, 0, 0, 766, 767, 1, 0, 0, 0, 767, 769, 5, 126, 0, 0, 768, 770, 5, 23, 0, 0, 769, 768, 1, 0, 0, 0, 769, 770, 1, 0, 0, 0, 770, 772, 1, 0, 0, 0, 771, 773, 3, 108, 54, 0, 772, 771, 1, 0, 0, 0, 772, 773, 1, 0, 0, 0, 773, 774, 1, 0, 0, 0, 774, 775, 5, 144, 0, 0, 775, 813, 1, 0, 0, 0, 776, 813, 3, 114, 57, 0, 777, 813, 3, 158, 79, 0, 778, 813, 3, 140, 70, 0, 779, 780, 5, 114, 0, 0, 780, 813, 3, 106, 53, 19, 781, 782, 5, 56, 0, 0, 782, 813, 3, 106, 53, 13, 783, 784, 3, 130, 65, 0, 784, 785, 5, 116, 0, 0, 785, 787, 1, 0, 0, 0, 786, 783, 1, 0, 0, 0, 786, 787, 1, 0, 0, 0, 787, 788, 1, 0, 0, 0, 788, 813, 5, 108, 0, 0, 789, 790, 5, 126, 0, 0, 790, 791, 3, 34, 17, 0, 791, 792, 5, 144, 0, 0, 792, 813, 1, 0, 0, 0, 793, 794, 5, 126, 0, 0, 794, 795, 3, 106, 53, 0, 795, 796, 5, 144, 0, 0, 796, 813, 1, 0, 0, 0, 797, 798, 5, 126, 0, 0, 798, 799, 3, 104, 52, 0, 799, 800, 5, 144, 0, 0, 800, 813, 1, 0, 0, 0, 801, 803, 5, 125, 0, 0, 802, 804, 3, 104, 52, 0, 803, 802, 1, 0, 0, 0, 803, 804, 1, 0, 0, 0, 804, 805, 1, 0, 0, 0, 805, 813, 5, 143, 0, 0, 806, 808, 5, 124, 0, 0, 807, 809, 3, 30, 15, 0, 808, 807, 1, 0, 0, 0, 808, 809, 1, 0, 0, 0, 809, 810, 1, 0, 0, 0, 810, 813, 5, 142, 0, 0, 811, 813, 3, 122, 61, 0, 812, 683, 1, 0, 0, 0, 812, 703, 1, 0, 0, 0, 812, 710, 1, 0, 0, 0, 812, 712, 1, 0, 0, 0, 812, 716, 1, 0, 0, 0, 812, 727, 1, 0, 0, 0, 812, 729, 1, 0, 0, 0, 812, 737, 1, 0, 0, 0, 812, 749, 1, 0, 0, 0, 812, 759, 1, 0, 0, 0, 812, 776, 1, 0, 0, 0, 812, 777, 1, 0, 0, 0, 812, 778, 1, 0, 0, 0, 812, 779, 1, 0, 0, 0, 812, 781, 1, 0, 0, 0, 812, 786, 1, 0, 0, 0, 812, 789, 1, 0, 0, 0, 812, 793, 1, 0, 0, 0, 812, 797, 1, 0, 0, 0, 812, 801, 1, 0, 0, 0, 812, 806, 1, 0, 0, 0, 812, 811, 1, 0, 0, 0, 813, 907, 1, 0, 0, 0, 814, 818, 10, 18, 0, 0, 815, 819, 5, 108, 0, 0, 816, 819, 5, 146, 0, 0, 817, 819, 5, 133, 0, 0, 818, 815, 1, 0, 0, 0, 818, 816, 1, 0, 0, 0, 818, 817, 1, 0, 0, 0, 819, 820, 1, 0, 0, 0, 820, 906, 3, 106, 53, 19, 821, 825, 10, 17, 0, 0, 822, 826, 5, 134, 0, 0, 823, 826, 5, 114, 0, 0, 824, 826, 5, 113, 0, 0, 825, 822, 1, 0, 0, 0, 825, 823, 1, 0, 0, 0, 825, 824, 1, 0, 0, 0, 826, 827, 1, 0, 0, 0, 827, 906, 3, 106, 53, 18, 828, 853, 10, 16, 0, 0, 829, 854, 5, 117, 0, 0, 830, 854, 5, 118, 0, 0, 831, 854, 5, 129, 0, 0, 832, 854, 5, 127, 0, 0, 833, 854, 5, 128, 0, 0, 834, 854, 5, 119, 0, 0, 835, 854, 5, 120, 0, 0, 836, 838, 5, 56, 0, 0, 837, 836, 1, 0, 0, 0, 837, 838, 1, 0, 0, 0, 838, 839, 1, 0, 0, 0, 839, 841, 5, 40, 0, 0, 840, 842, 5, 14, 0, 0, 841, 840, 1, 0, 0, 0, 841, 842, 1, 0, 0, 0, 842, 854, 1, 0, 0, 0, 843, 845, 5, 56, 0, 0, 844, 843, 1, 0, 0, 0, 844, 845, 1, 0, 0, 0, 845, 846, 1, 0, 0, 0, 846, 854, 7, 10, 0, 0, 847, 854, 5, 140, 0, 0, 848, 854, 5, 141, 0, 0, 849, 854, 5, 131, 0, 0, 850, 854, 5, 122, 0, 0, 851, 854, 5, 123, 0, 0, 852, 854, 5, 130, 0, 0, 853, 829, 1, 0, 0, 0, 853, 830, 1, 0, 0, 0, 853, 831, 1, 0, 0, 0, 853, 832, 1, 0, 0, 0, 853, 833, 1, 0, 0, 0, 853, 834, 1, 0, 0, 0, 853, 835, 1, 0, 0, 0, 853, 837, 1, 0, 0, 0, 853, 844, 1, 0, 0, 0, 853, 847, 1, 0, 0, 0, 853, 848, 1, 0, 0, 0, 853, 849, 1, 0, 0, 0, 853, 850, 1, 0, 0, 0, 853, 851, 1, 0, 0, 0, 853, 852, 1, 0, 0, 0, 854, 855, 1, 0, 0, 0, 855, 906, 3, 106, 53, 17, 856, 857, 10, 14, 0, 0, 857, 858, 5, 132, 0, 0, 858, 906, 3, 106, 53, 15, 859, 860, 10, 12, 0, 0, 860, 861, 5, 2, 0, 0, 861, 906, 3, 106, 53, 13, 862, 863, 10, 11, 0, 0, 863, 864, 5, 61, 0, 0, 864, 906, 3, 106, 53, 12, 865, 867, 10, 10, 0, 0, 866, 868, 5, 56, 0, 0, 867, 866, 1, 0, 0, 0, 867, 868, 1, 0, 0, 0, 868, 869, 1, 0, 0, 0, 869, 870, 5, 9, 0, 0, 870, 871, 3, 106, 53, 0, 871, 872, 5, 2, 0, 0, 872, 873, 3, 106, 53, 11, 873, 906, 1, 0, 0, 0, 874, 875, 10, 9, 0, 0, 875, 876, 5, 135, 0, 0, 876, 877, 3, 106, 53, 0, 877, 878, 5, 111, 0, 0, 878, 879, 3, 106, 53, 9, 879, 906, 1, 0, 0, 0, 880, 881, 10, 22, 0, 0, 881, 882, 5, 125, 0, 0, 882, 883, 3, 106, 53, 0, 883, 884, 5, 143, 0, 0, 884, 906, 1, 0, 0, 0, 885, 886, 10, 21, 0, 0, 886, 887, 5, 116, 0, 0, 887, 906, 5, 104, 0, 0, 888, 889, 10, 20, 0, 0, 889, 890, 5, 116, 0, 0, 890, 906, 3, 150, 75, 0, 891, 892, 10, 15, 0, 0, 892, 894, 5, 44, 0, 0, 893, 895, 5, 56, 0, 0, 894, 893, 1, 0, 0, 0, 894, 895, 1, 0, 0, 0, 895, 896, 1, 0, 0, 0, 896, 906, 5, 57, 0, 0, 897, 903, 10, 8, 0, 0, 898, 904, 3, 148, 74, 0, 899, 900, 5, 6, 0, 0, 900, 904, 3, 150, 75, 0, 901, 902, 5, 6, 0, 0, 902, 904, 5, 106, 0, 0, 903, 898, 1, 0, 0, 0, 903, 899, 1, 0, 0, 0, 903, 901, 1, 0, 0, 0, 904, 906, 1, 0, 0, 0, 905, 814, 1, 0, 0, 0, 905, 821, 1, 0, 0, 0, 905, 828, 1, 0, 0, 0, 905, 856, 1, 0, 0, 0, 905, 859, 1, 0, 0, 0, 905, 862, 1, 0, 0, 0, 905, 865, 1, 0, 0, 0, 905, 874, 1, 0, 0, 0, 905, 880, 1, 0, 0, 0, 905, 885, 1, 0, 0, 0, 905, 888, 1, 0, 0, 0, 905, 891, 1, 0, 0, 0, 905, 897, 1, 0, 0, 0, 906, 909, 1, 0, 0, 0, 907, 905, 1, 0, 0, 0, 907, 908, 1, 0, 0, 0, 908, 107, 1, 0, 0, 0, 909, 907, 1, 0, 0, 0, 910, 915, 3, 110, 55, 0, 911, 912, 5, 112, 0, 0, 912, 914, 3, 110, 55, 0, 913, 911, 1, 0, 0, 0, 914, 917, 1, 0, 0, 0, 915, 913, 1, 0, 0, 0, 915, 916, 1, 0, 0, 0, 916, 109, 1, 0, 0, 0, 917, 915, 1, 0, 0, 0, 918, 921, 3, 112, 56, 0, 919, 921, 3, 106, 53, 0, 920, 918, 1, 0, 0, 0, 920, 919, 1, 0, 0, 0, 921, 111, 1, 0, 0, 0, 922, 923, 5, 126, 0, 0, 923, 928, 3, 150, 75, 0, 924, 925, 5, 112, 0, 0, 925, 927, 3, 150, 75, 0, 926, 924, 1, 0, 0, 0, 927, 930, 1, 0, 0, 0, 928, 926, 1, 0, 0, 0, 928, 929, 1, 0, 0, 0, 929, 931, 1, 0, 0, 0, 930, 928, 1, 0, 0, 0, 931, 932, 5, 144, 0, 0, 932, 942, 1, 0, 0, 0, 933, 938, 3, 150, 75, 0, 934, 935, 5, 112, 0, 0, 935, 937, 3, 150, 75, 0, 936, 934, 1, 0, 0, 0, 937, 940, 1, 0, 0, 0, 938, 936, 1, 0, 0, 0, 938, 939, 1, 0, 0, 0, 939, 942, 1, 0, 0, 0, 940, 938, 1, 0, 0, 0, 941, 922, 1, 0, 0, 0, 941, 933, 1, 0, 0, 0, 942, 943, 1, 0, 0, 0, 943, 944, 5, 107, 0, 0, 944, 945, 3, 106, 53, 0, 945, 113, 1, 0, 0, 0, 946, 947, 5, 128, 0, 0, 947, 951, 3, 150, 75, 0, 948, 950, 3, 116, 58, 0, 949, 948, 1, 0, 0, 0, 950, 953, 1, 0, 0, 0, 951, 949, 1, 0, 0, 0, 951, 952, 1, 0, 0, 0, 952, 954, 1, 0, 0, 0, 953, 951, 1, 0, 0, 0, 954, 955, 5, 146, 0, 0, 955, 956, 5, 120, 0, 0, 956, 975, 1, 0, 0, 0, 957, 958, 5, 128, 0, 0, 958, 962, 3, 150, 75, 0, 959, 961, 3, 116, 58, 0, 960, 959, 1, 0, 0, 0, 961, 964, 1, 0, 0, 0, 962, 960, 1, 0, 0, 0, 962, 963, 1, 0, 0, 0, 963, 965, 1, 0, 0, 0, 964, 962, 1, 0, 0, 0, 965, 967, 5, 120, 0, 0, 966, 968, 3, 114, 57, 0, 967, 966, 1, 0, 0, 0, 967, 968, 1, 0, 0, 0, 968, 969, 1, 0, 0, 0, 969, 970, 5, 128, 0, 0, 970, 971, 5, 146, 0, 0, 971, 972, 3, 150, 75, 0, 972, 973, 5, 120, 0, 0, 973, 975, 1, 0, 0, 0, 974, 946, 1, 0, 0, 0, 974, 957, 1, 0, 0, 0, 975, 115, 1, 0, 0, 0, 976, 977, 3, 150, 75, 0, 977, 978, 5, 118, 0, 0, 978, 979, 3, 156, 78, 0, 979, 988, 1, 0, 0, 0, 980, 981, 3, 150, 75, 0, 981, 982, 5, 118, 0, 0, 982, 983, 5, 124, 0, 0, 983, 984, 3, 106, 53, 0, 984, 985, 5, 142, 0, 0, 985, 988, 1, 0, 0, 0, 986, 988, 3, 150, 75, 0, 987, 976, 1, 0, 0, 0, 987, 980, 1, 0, 0, 0, 987, 986, 1, 0, 0, 0, 988, 117, 1, 0, 0, 0, 989, 994, 3, 120, 60, 0, 990, 991, 5, 112, 0, 0, 991, 993, 3, 120, 60, 0, 992, 990, 1, 0, 0, 0, 993, 996, 1, 0, 0, 0, 994, 992, 1, 0, 0, 0, 994, 995, 1, 0, 0, 0, 995, 119, 1, 0, 0, 0, 996, 994, 1, 0, 0, 0, 997, 998, 3, 150, 75, 0, 998, 999, 5, 6, 0, 0, 999, 1000, 5, 126, 0, 0, 1000, 1001, 3, 34, 17, 0, 1001, 1002, 5, 144, 0, 0, 1002, 1008, 1, 0, 0, 0, 1003, 1004, 3, 106, 53, 0, 1004, 1005, 5, 6, 0, 0, 1005, 1006, 3, 150, 75, 0, 1006, 1008, 1, 0, 0, 0, 1007, 997, 1, 0, 0, 0, 1007, 1003, 1, 0, 0, 0, 1008, 121, 1, 0, 0, 0, 1009, 1017, 3, 154, 77, 0, 1010, 1011, 3, 130, 65, 0, 1011, 1012, 5, 116, 0, 0, 1012, 1014, 1, 0, 0, 0, 1013, 1010, 1, 0, 0, 0, 1013, 1014, 1, 0, 0, 0, 1014, 1015, 1, 0, 0, 0, 1015, 1017, 3, 124, 62, 0, 1016, 1009, 1, 0, 0, 0, 1016, 1013, 1, 0, 0, 0, 1017, 123, 1, 0, 0, 0, 1018, 1023, 3, 150, 75, 0, 1019, 1020, 5, 116, 0, 0, 1020, 1022, 3, 150, 75, 0, 1021, 1019, 1, 0, 0, 0, 1022, 1025, 1, 0, 0, 0, 1023, 1021, 1, 0, 0, 0, 1023, 1024, 1, 0, 0, 0, 1024, 125, 1, 0, 0, 0, 1025, 1023, 1, 0, 0, 0, 1026, 1027, 6, 63, -1, 0, 1027, 1036, 3, 130, 65, 0, 1028, 1036, 3, 128, 64, 0, 1029, 1030, 5, 126, 0, 0, 1030, 1031, 3, 34, 17, 0, 1031, 1032, 5, 144, 0, 0, 1032, 1036, 1, 0, 0, 0, 1033, 1036, 3, 114, 57, 0, 1034, 1036, 3, 154, 77, 0, 1035, 1026, 1, 0, 0, 0, 1035, 1028, 1, 0, 0, 0, 1035, 1029, 1, 0, 0, 0, 1035, 1033, 1, 0, 0, 0, 1035, 1034, 1, 0, 0, 0, 1036, 1045, 1, 0, 0, 0, 1037, 1041, 10, 3, 0, 0, 1038, 1042, 3, 148, 74, 0, 1039, 1040, 5, 6, 0, 0, 1040, 1042, 3, 150, 75, 0, 1041, 1038, 1, 0, 0, 0, 1041, 1039, 1, 0, 0, 0, 1042, 1044, 1, 0, 0, 0, 1043, 1037, 1, 0, 0, 0, 1044, 1047, 1, 0, 0, 0, 1045, 1043, 1, 0, 0, 0, 1045, 1046, 1, 0, 0, 0, 1046, 127, 1, 0, 0, 0, 1047, 1045, 1, 0, 0, 0, 1048, 1049, 3, 150, 75, 0, 1049, 1051, 5, 126, 0, 0, 1050, 1052, 3, 132, 66, 0, 1051, 1050, 1, 0, 0, 0, 1051, 1052, 1, 0, 0, 0, 1052, 1053, 1, 0, 0, 0, 1053, 1054, 5, 144, 0, 0, 1054, 129, 1, 0, 0, 0, 1055, 1056, 3, 134, 67, 0, 1056, 1057, 5, 116, 0, 0, 1057, 1059, 1, 0, 0, 0, 1058, 1055, 1, 0, 0, 0, 1058, 1059, 1, 0, 0, 0, 1059, 1060, 1, 0, 0, 0, 1060, 1061, 3, 150, 75, 0, 1061, 131, 1, 0, 0, 0, 1062, 1067, 3, 106, 53, 0, 1063, 1064, 5, 112, 0, 0, 1064, 1066, 3, 106, 53, 0, 1065, 1063, 1, 0, 0, 0, 1066, 1069, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1067, 1068, 1, 0, 0, 0, 1068, 133, 1, 0, 0, 0, 1069, 1067, 1, 0, 0, 0, 1070, 1071, 3, 150, 75, 0, 1071, 135, 1, 0, 0, 0, 1072, 1081, 5, 102, 0, 0, 1073, 1074, 5, 116, 0, 0, 1074, 1081, 7, 11, 0, 0, 1075, 1076, 5, 104, 0, 0, 1076, 1078, 5, 116, 0, 0, 1077, 1079, 7, 11, 0, 0, 1078, 1077, 1, 0, 0, 0, 1078, 1079, 1, 0, 0, 0, 1079, 1081, 1, 0, 0, 0, 1080, 1072, 1, 0, 0, 0, 1080, 1073, 1, 0, 0, 0, 1080, 1075, 1, 0, 0, 0, 1081, 137, 1, 0, 0, 0, 1082, 1084, 7, 12, 0, 0, 1083, 1082, 1, 0, 0, 0, 1083, 1084, 1, 0, 0, 0, 1084, 1091, 1, 0, 0, 0, 1085, 1092, 3, 136, 68, 0, 1086, 1092, 5, 103, 0, 0, 1087, 1092, 5, 104, 0, 0, 1088, 1092, 5, 105, 0, 0, 1089, 1092, 5, 41, 0, 0, 1090, 1092, 5, 55, 0, 0, 1091, 1085, 1, 0, 0, 0, 1091, 1086, 1, 0, 0, 0, 1091, 1087, 1, 0, 0, 0, 1091, 1088, 1, 0, 0, 0, 1091, 1089, 1, 0, 0, 0, 1091, 1090, 1, 0, 0, 0, 1092, 139, 1, 0, 0, 0, 1093, 1097, 3, 138, 69, 0, 1094, 1097, 5, 106, 0, 0, 1095, 1097, 5, 57, 0, 0, 1096, 1093, 1, 0, 0, 0, 1096, 1094, 1, 0, 0, 0, 1096, 1095, 1, 0, 0, 0, 1097, 141, 1, 0, 0, 0, 1098, 1099, 7, 13, 0, 0, 1099, 143, 1, 0, 0, 0, 1100, 1101, 7, 14, 0, 0, 1101, 145, 1, 0, 0, 0, 1102, 1103, 7, 15, 0, 0, 1103, 147, 1, 0, 0, 0, 1104, 1107, 5, 101, 0, 0, 1105, 1107, 3, 146, 73, 0, 1106, 1104, 1, 0, 0, 0, 1106, 1105, 1, 0, 0, 0, 1107, 149, 1, 0, 0, 0, 1108, 1112, 5, 101, 0, 0, 1109, 1112, 3, 142, 71, 0, 1110, 1112, 3, 144, 72, 0, 1111, 1108, 1, 0, 0, 0, 1111, 1109, 1, 0, 0, 0, 1111, 1110, 1, 0, 0, 0, 1112, 151, 1, 0, 0, 0, 1113, 1114, 3, 156, 78, 0, 1114, 1115, 5, 118, 0, 0, 1115, 1116, 3, 138, 69, 0, 1116, 153, 1, 0, 0, 0, 1117, 1118, 5, 124, 0, 0, 1118, 1119, 3, 150, 75, 0, 1119, 1120, 5, 142, 0, 0, 1120, 155, 1, 0, 0, 0, 1121, 1124, 5, 106, 0, 0, 1122, 1124, 3, 158, 79, 0, 1123, 1121, 1, 0, 0, 0, 1123, 1122, 1, 0, 0, 0, 1124, 157, 1, 0, 0, 0, 1125, 1129, 5, 137, 0, 0, 1126, 1128, 3, 160, 80, 0, 1127, 1126, 1, 0, 0, 0, 1128, 1131, 1, 0, 0, 0, 1129, 1127, 1, 0, 0, 0, 1129, 1130, 1, 0, 0, 0, 1130, 1132, 1, 0, 0, 0, 1131, 1129, 1, 0, 0, 0, 1132, 1133, 5, 139, 0, 0, 1133, 159, 1, 0, 0, 0, 1134, 1135, 5, 152, 0, 0, 1135, 1136, 3, 106, 53, 0, 1136, 1137, 5, 142, 0, 0, 1137, 1140, 1, 0, 0, 0, 1138, 1140, 5, 151, 0, 0, 1139, 1134, 1, 0, 0, 0, 1139, 1138, 1, 0, 0, 0, 1140, 161, 1, 0, 0, 0, 1141, 1145, 5, 138, 0, 0, 1142, 1144, 3, 164, 82, 0, 1143, 1142, 1, 0, 0, 0, 1144, 1147, 1, 0, 0, 0, 1145, 1143, 1, 0, 0, 0, 1145, 1146, 1, 0, 0, 0, 1146, 1148, 1, 0, 0, 0, 1147, 1145, 1, 0, 0, 0, 1148, 1149, 5, 0, 0, 1, 1149, 163, 1, 0, 0, 0, 1150, 1151, 5, 154, 0, 0, 1151, 1152, 3, 106, 53, 0, 1152, 1153, 5, 142, 0, 0, 1153, 1156, 1, 0, 0, 0, 1154, 1156, 5, 153, 0, 0, 1155, 1150, 1, 0, 0, 0, 1155, 1154, 1, 0, 0, 0, 1156, 165, 1, 0, 0, 0, 135, 169, 176, 185, 200, 212, 224, 240, 251, 265, 271, 281, 290, 293, 297, 300, 304, 307, 310, 313, 316, 320, 324, 327, 330, 333, 337, 340, 349, 355, 376, 393, 410, 416, 422, 433, 435, 446, 449, 455, 463, 469, 471, 475, 480, 483, 486, 490, 494, 497, 499, 502, 506, 510, 513, 515, 517, 522, 533, 539, 546, 551, 555, 559, 565, 567, 574, 582, 585, 588, 607, 621, 637, 649, 661, 669, 673, 680, 686, 695, 699, 723, 740, 752, 762, 765, 769, 772, 786, 803, 808, 812, 818, 825, 837, 841, 844, 853, 867, 894, 903, 905, 907, 915, 920, 928, 938, 941, 951, 962, 967, 974, 987, 994, 1007, 1013, 1016, 1023, 1035, 1041, 1045, 1051, 1058, 1067, 1078, 1080, 1083, 1091, 1096, 1106, 1111, 1123, 1129, 1139, 1145, 1155] \ No newline at end of file +[4, 1, 154, 1178, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 199, 8, 5, 10, 5, 12, 5, 202, 9, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 213, 8, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 225, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 241, 8, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 250, 8, 13, 10, 13, 12, 13, 253, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 264, 8, 15, 10, 15, 12, 15, 267, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 272, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 280, 8, 17, 10, 17, 12, 17, 283, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 291, 8, 18, 1, 19, 3, 19, 294, 8, 19, 1, 19, 1, 19, 3, 19, 298, 8, 19, 1, 19, 3, 19, 301, 8, 19, 1, 19, 1, 19, 3, 19, 305, 8, 19, 1, 19, 3, 19, 308, 8, 19, 1, 19, 3, 19, 311, 8, 19, 1, 19, 3, 19, 314, 8, 19, 1, 19, 3, 19, 317, 8, 19, 1, 19, 1, 19, 3, 19, 321, 8, 19, 1, 19, 1, 19, 3, 19, 325, 8, 19, 1, 19, 3, 19, 328, 8, 19, 1, 19, 3, 19, 331, 8, 19, 1, 19, 3, 19, 334, 8, 19, 1, 19, 1, 19, 3, 19, 338, 8, 19, 1, 19, 3, 19, 341, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 350, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 356, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 375, 8, 24, 10, 24, 12, 24, 378, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 394, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 411, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 417, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 423, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 434, 8, 31, 3, 31, 436, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 447, 8, 34, 1, 34, 3, 34, 450, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 456, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 464, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 470, 8, 34, 10, 34, 12, 34, 473, 9, 34, 1, 35, 3, 35, 476, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 481, 8, 35, 1, 35, 3, 35, 484, 8, 35, 1, 35, 3, 35, 487, 8, 35, 1, 35, 1, 35, 3, 35, 491, 8, 35, 1, 35, 1, 35, 3, 35, 495, 8, 35, 1, 35, 3, 35, 498, 8, 35, 3, 35, 500, 8, 35, 1, 35, 3, 35, 503, 8, 35, 1, 35, 1, 35, 3, 35, 507, 8, 35, 1, 35, 1, 35, 3, 35, 511, 8, 35, 1, 35, 3, 35, 514, 8, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 523, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 534, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 540, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 545, 8, 39, 10, 39, 12, 39, 548, 9, 39, 1, 40, 1, 40, 3, 40, 552, 8, 40, 1, 40, 1, 40, 3, 40, 556, 8, 40, 1, 40, 1, 40, 3, 40, 560, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 566, 8, 41, 3, 41, 568, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 573, 8, 42, 10, 42, 12, 42, 576, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 583, 8, 44, 1, 44, 3, 44, 586, 8, 44, 1, 44, 3, 44, 589, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 608, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 622, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 636, 8, 51, 10, 51, 12, 51, 639, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 648, 8, 51, 10, 51, 12, 51, 651, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 660, 8, 51, 10, 51, 12, 51, 663, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 670, 8, 51, 1, 51, 1, 51, 3, 51, 674, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 679, 8, 52, 10, 52, 12, 52, 682, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 687, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 694, 8, 53, 11, 53, 12, 53, 695, 1, 53, 1, 53, 3, 53, 700, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 724, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 741, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 747, 8, 53, 1, 53, 3, 53, 750, 8, 53, 1, 53, 3, 53, 753, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 763, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 769, 8, 53, 1, 53, 3, 53, 772, 8, 53, 1, 53, 3, 53, 775, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 783, 8, 53, 1, 53, 3, 53, 786, 8, 53, 1, 53, 1, 53, 3, 53, 790, 8, 53, 1, 53, 3, 53, 793, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 807, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 824, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 829, 8, 53, 1, 53, 1, 53, 3, 53, 833, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 839, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 846, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 858, 8, 53, 1, 53, 1, 53, 3, 53, 862, 8, 53, 1, 53, 3, 53, 865, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 874, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 888, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 915, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 924, 8, 53, 5, 53, 926, 8, 53, 10, 53, 12, 53, 929, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 934, 8, 54, 10, 54, 12, 54, 937, 9, 54, 1, 55, 1, 55, 3, 55, 941, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 947, 8, 56, 10, 56, 12, 56, 950, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 957, 8, 56, 10, 56, 12, 56, 960, 9, 56, 3, 56, 962, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 970, 8, 57, 10, 57, 12, 57, 973, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 981, 8, 57, 10, 57, 12, 57, 984, 9, 57, 1, 57, 1, 57, 3, 57, 988, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 995, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 1008, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 1013, 8, 59, 10, 59, 12, 59, 1016, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1028, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1034, 8, 61, 1, 61, 3, 61, 1037, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1042, 8, 62, 10, 62, 12, 62, 1045, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1056, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1062, 8, 63, 5, 63, 1064, 8, 63, 10, 63, 12, 63, 1067, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1072, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1079, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1086, 8, 66, 10, 66, 12, 66, 1089, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1099, 8, 68, 3, 68, 1101, 8, 68, 1, 69, 3, 69, 1104, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1112, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1117, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1127, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1132, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1144, 8, 78, 1, 79, 1, 79, 5, 79, 1148, 8, 79, 10, 79, 12, 79, 1151, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1160, 8, 80, 1, 81, 1, 81, 5, 81, 1164, 8, 81, 10, 81, 12, 81, 1167, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1176, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1314, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 189, 1, 0, 0, 0, 10, 195, 1, 0, 0, 0, 12, 212, 1, 0, 0, 0, 14, 214, 1, 0, 0, 0, 16, 217, 1, 0, 0, 0, 18, 226, 1, 0, 0, 0, 20, 232, 1, 0, 0, 0, 22, 236, 1, 0, 0, 0, 24, 245, 1, 0, 0, 0, 26, 247, 1, 0, 0, 0, 28, 256, 1, 0, 0, 0, 30, 260, 1, 0, 0, 0, 32, 271, 1, 0, 0, 0, 34, 275, 1, 0, 0, 0, 36, 290, 1, 0, 0, 0, 38, 293, 1, 0, 0, 0, 40, 342, 1, 0, 0, 0, 42, 345, 1, 0, 0, 0, 44, 351, 1, 0, 0, 0, 46, 355, 1, 0, 0, 0, 48, 361, 1, 0, 0, 0, 50, 379, 1, 0, 0, 0, 52, 382, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 395, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 402, 1, 0, 0, 0, 62, 435, 1, 0, 0, 0, 64, 437, 1, 0, 0, 0, 66, 440, 1, 0, 0, 0, 68, 455, 1, 0, 0, 0, 70, 517, 1, 0, 0, 0, 72, 522, 1, 0, 0, 0, 74, 533, 1, 0, 0, 0, 76, 535, 1, 0, 0, 0, 78, 541, 1, 0, 0, 0, 80, 549, 1, 0, 0, 0, 82, 567, 1, 0, 0, 0, 84, 569, 1, 0, 0, 0, 86, 577, 1, 0, 0, 0, 88, 582, 1, 0, 0, 0, 90, 590, 1, 0, 0, 0, 92, 594, 1, 0, 0, 0, 94, 598, 1, 0, 0, 0, 96, 607, 1, 0, 0, 0, 98, 621, 1, 0, 0, 0, 100, 623, 1, 0, 0, 0, 102, 673, 1, 0, 0, 0, 104, 675, 1, 0, 0, 0, 106, 832, 1, 0, 0, 0, 108, 930, 1, 0, 0, 0, 110, 940, 1, 0, 0, 0, 112, 961, 1, 0, 0, 0, 114, 994, 1, 0, 0, 0, 116, 1007, 1, 0, 0, 0, 118, 1009, 1, 0, 0, 0, 120, 1027, 1, 0, 0, 0, 122, 1036, 1, 0, 0, 0, 124, 1038, 1, 0, 0, 0, 126, 1055, 1, 0, 0, 0, 128, 1068, 1, 0, 0, 0, 130, 1078, 1, 0, 0, 0, 132, 1082, 1, 0, 0, 0, 134, 1090, 1, 0, 0, 0, 136, 1100, 1, 0, 0, 0, 138, 1103, 1, 0, 0, 0, 140, 1116, 1, 0, 0, 0, 142, 1118, 1, 0, 0, 0, 144, 1120, 1, 0, 0, 0, 146, 1122, 1, 0, 0, 0, 148, 1126, 1, 0, 0, 0, 150, 1131, 1, 0, 0, 0, 152, 1133, 1, 0, 0, 0, 154, 1137, 1, 0, 0, 0, 156, 1143, 1, 0, 0, 0, 158, 1145, 1, 0, 0, 0, 160, 1159, 1, 0, 0, 0, 162, 1161, 1, 0, 0, 0, 164, 1175, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 12, 6, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 188, 5, 145, 0, 0, 188, 7, 1, 0, 0, 0, 189, 190, 3, 4, 2, 0, 190, 191, 5, 111, 0, 0, 191, 192, 5, 118, 0, 0, 192, 193, 3, 4, 2, 0, 193, 194, 5, 145, 0, 0, 194, 9, 1, 0, 0, 0, 195, 200, 3, 150, 75, 0, 196, 197, 5, 112, 0, 0, 197, 199, 3, 150, 75, 0, 198, 196, 1, 0, 0, 0, 199, 202, 1, 0, 0, 0, 200, 198, 1, 0, 0, 0, 200, 201, 1, 0, 0, 0, 201, 11, 1, 0, 0, 0, 202, 200, 1, 0, 0, 0, 203, 213, 3, 20, 10, 0, 204, 213, 3, 24, 12, 0, 205, 213, 3, 14, 7, 0, 206, 213, 3, 16, 8, 0, 207, 213, 3, 18, 9, 0, 208, 213, 3, 22, 11, 0, 209, 213, 3, 8, 4, 0, 210, 213, 3, 20, 10, 0, 211, 213, 3, 26, 13, 0, 212, 203, 1, 0, 0, 0, 212, 204, 1, 0, 0, 0, 212, 205, 1, 0, 0, 0, 212, 206, 1, 0, 0, 0, 212, 207, 1, 0, 0, 0, 212, 208, 1, 0, 0, 0, 212, 209, 1, 0, 0, 0, 212, 210, 1, 0, 0, 0, 212, 211, 1, 0, 0, 0, 213, 13, 1, 0, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 145, 0, 0, 216, 15, 1, 0, 0, 0, 217, 218, 5, 38, 0, 0, 218, 219, 5, 126, 0, 0, 219, 220, 3, 4, 2, 0, 220, 221, 5, 144, 0, 0, 221, 224, 3, 12, 6, 0, 222, 223, 5, 24, 0, 0, 223, 225, 3, 12, 6, 0, 224, 222, 1, 0, 0, 0, 224, 225, 1, 0, 0, 0, 225, 17, 1, 0, 0, 0, 226, 227, 5, 96, 0, 0, 227, 228, 5, 126, 0, 0, 228, 229, 3, 4, 2, 0, 229, 230, 5, 144, 0, 0, 230, 231, 3, 12, 6, 0, 231, 19, 1, 0, 0, 0, 232, 233, 5, 70, 0, 0, 233, 234, 3, 4, 2, 0, 234, 235, 5, 145, 0, 0, 235, 21, 1, 0, 0, 0, 236, 237, 5, 29, 0, 0, 237, 238, 3, 150, 75, 0, 238, 240, 5, 126, 0, 0, 239, 241, 3, 10, 5, 0, 240, 239, 1, 0, 0, 0, 240, 241, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 5, 144, 0, 0, 243, 244, 3, 26, 13, 0, 244, 23, 1, 0, 0, 0, 245, 246, 5, 145, 0, 0, 246, 25, 1, 0, 0, 0, 247, 251, 5, 124, 0, 0, 248, 250, 3, 2, 1, 0, 249, 248, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 254, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 255, 5, 142, 0, 0, 255, 27, 1, 0, 0, 0, 256, 257, 3, 4, 2, 0, 257, 258, 5, 111, 0, 0, 258, 259, 3, 4, 2, 0, 259, 29, 1, 0, 0, 0, 260, 265, 3, 28, 14, 0, 261, 262, 5, 112, 0, 0, 262, 264, 3, 28, 14, 0, 263, 261, 1, 0, 0, 0, 264, 267, 1, 0, 0, 0, 265, 263, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 31, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 272, 3, 34, 17, 0, 269, 272, 3, 38, 19, 0, 270, 272, 3, 114, 57, 0, 271, 268, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 271, 270, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 274, 5, 0, 0, 1, 274, 33, 1, 0, 0, 0, 275, 281, 3, 36, 18, 0, 276, 277, 5, 91, 0, 0, 277, 278, 5, 1, 0, 0, 278, 280, 3, 36, 18, 0, 279, 276, 1, 0, 0, 0, 280, 283, 1, 0, 0, 0, 281, 279, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 35, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 284, 291, 3, 38, 19, 0, 285, 286, 5, 126, 0, 0, 286, 287, 3, 34, 17, 0, 287, 288, 5, 144, 0, 0, 288, 291, 1, 0, 0, 0, 289, 291, 3, 154, 77, 0, 290, 284, 1, 0, 0, 0, 290, 285, 1, 0, 0, 0, 290, 289, 1, 0, 0, 0, 291, 37, 1, 0, 0, 0, 292, 294, 3, 40, 20, 0, 293, 292, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 297, 5, 77, 0, 0, 296, 298, 5, 23, 0, 0, 297, 296, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 300, 1, 0, 0, 0, 299, 301, 3, 42, 21, 0, 300, 299, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 304, 3, 104, 52, 0, 303, 305, 3, 44, 22, 0, 304, 303, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 307, 1, 0, 0, 0, 306, 308, 3, 46, 23, 0, 307, 306, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308, 310, 1, 0, 0, 0, 309, 311, 3, 50, 25, 0, 310, 309, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 313, 1, 0, 0, 0, 312, 314, 3, 52, 26, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 316, 1, 0, 0, 0, 315, 317, 3, 54, 27, 0, 316, 315, 1, 0, 0, 0, 316, 317, 1, 0, 0, 0, 317, 320, 1, 0, 0, 0, 318, 319, 5, 98, 0, 0, 319, 321, 7, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 324, 1, 0, 0, 0, 322, 323, 5, 98, 0, 0, 323, 325, 5, 86, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 327, 1, 0, 0, 0, 326, 328, 3, 56, 28, 0, 327, 326, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328, 330, 1, 0, 0, 0, 329, 331, 3, 48, 24, 0, 330, 329, 1, 0, 0, 0, 330, 331, 1, 0, 0, 0, 331, 333, 1, 0, 0, 0, 332, 334, 3, 58, 29, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 337, 1, 0, 0, 0, 335, 338, 3, 62, 31, 0, 336, 338, 3, 64, 32, 0, 337, 335, 1, 0, 0, 0, 337, 336, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 340, 1, 0, 0, 0, 339, 341, 3, 66, 33, 0, 340, 339, 1, 0, 0, 0, 340, 341, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 343, 5, 98, 0, 0, 343, 344, 3, 118, 59, 0, 344, 41, 1, 0, 0, 0, 345, 346, 5, 85, 0, 0, 346, 349, 5, 104, 0, 0, 347, 348, 5, 98, 0, 0, 348, 350, 5, 82, 0, 0, 349, 347, 1, 0, 0, 0, 349, 350, 1, 0, 0, 0, 350, 43, 1, 0, 0, 0, 351, 352, 5, 32, 0, 0, 352, 353, 3, 68, 34, 0, 353, 45, 1, 0, 0, 0, 354, 356, 7, 1, 0, 0, 355, 354, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 357, 1, 0, 0, 0, 357, 358, 5, 5, 0, 0, 358, 359, 5, 45, 0, 0, 359, 360, 3, 104, 52, 0, 360, 47, 1, 0, 0, 0, 361, 362, 5, 97, 0, 0, 362, 363, 3, 150, 75, 0, 363, 364, 5, 6, 0, 0, 364, 365, 5, 126, 0, 0, 365, 366, 3, 88, 44, 0, 366, 376, 5, 144, 0, 0, 367, 368, 5, 112, 0, 0, 368, 369, 3, 150, 75, 0, 369, 370, 5, 6, 0, 0, 370, 371, 5, 126, 0, 0, 371, 372, 3, 88, 44, 0, 372, 373, 5, 144, 0, 0, 373, 375, 1, 0, 0, 0, 374, 367, 1, 0, 0, 0, 375, 378, 1, 0, 0, 0, 376, 374, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 49, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 379, 380, 5, 67, 0, 0, 380, 381, 3, 106, 53, 0, 381, 51, 1, 0, 0, 0, 382, 383, 5, 95, 0, 0, 383, 384, 3, 106, 53, 0, 384, 53, 1, 0, 0, 0, 385, 386, 5, 34, 0, 0, 386, 393, 5, 11, 0, 0, 387, 388, 7, 0, 0, 0, 388, 389, 5, 126, 0, 0, 389, 390, 3, 104, 52, 0, 390, 391, 5, 144, 0, 0, 391, 394, 1, 0, 0, 0, 392, 394, 3, 104, 52, 0, 393, 387, 1, 0, 0, 0, 393, 392, 1, 0, 0, 0, 394, 55, 1, 0, 0, 0, 395, 396, 5, 35, 0, 0, 396, 397, 3, 106, 53, 0, 397, 57, 1, 0, 0, 0, 398, 399, 5, 62, 0, 0, 399, 400, 5, 11, 0, 0, 400, 401, 3, 78, 39, 0, 401, 59, 1, 0, 0, 0, 402, 403, 5, 62, 0, 0, 403, 404, 5, 11, 0, 0, 404, 405, 3, 104, 52, 0, 405, 61, 1, 0, 0, 0, 406, 407, 5, 52, 0, 0, 407, 410, 3, 106, 53, 0, 408, 409, 5, 112, 0, 0, 409, 411, 3, 106, 53, 0, 410, 408, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 416, 1, 0, 0, 0, 412, 413, 5, 98, 0, 0, 413, 417, 5, 82, 0, 0, 414, 415, 5, 11, 0, 0, 415, 417, 3, 104, 52, 0, 416, 412, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 416, 417, 1, 0, 0, 0, 417, 436, 1, 0, 0, 0, 418, 419, 5, 52, 0, 0, 419, 422, 3, 106, 53, 0, 420, 421, 5, 98, 0, 0, 421, 423, 5, 82, 0, 0, 422, 420, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 425, 5, 59, 0, 0, 425, 426, 3, 106, 53, 0, 426, 436, 1, 0, 0, 0, 427, 428, 5, 52, 0, 0, 428, 429, 3, 106, 53, 0, 429, 430, 5, 59, 0, 0, 430, 433, 3, 106, 53, 0, 431, 432, 5, 11, 0, 0, 432, 434, 3, 104, 52, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 436, 1, 0, 0, 0, 435, 406, 1, 0, 0, 0, 435, 418, 1, 0, 0, 0, 435, 427, 1, 0, 0, 0, 436, 63, 1, 0, 0, 0, 437, 438, 5, 59, 0, 0, 438, 439, 3, 106, 53, 0, 439, 65, 1, 0, 0, 0, 440, 441, 5, 79, 0, 0, 441, 442, 3, 84, 42, 0, 442, 67, 1, 0, 0, 0, 443, 444, 6, 34, -1, 0, 444, 446, 3, 126, 63, 0, 445, 447, 5, 27, 0, 0, 446, 445, 1, 0, 0, 0, 446, 447, 1, 0, 0, 0, 447, 449, 1, 0, 0, 0, 448, 450, 3, 76, 38, 0, 449, 448, 1, 0, 0, 0, 449, 450, 1, 0, 0, 0, 450, 456, 1, 0, 0, 0, 451, 452, 5, 126, 0, 0, 452, 453, 3, 68, 34, 0, 453, 454, 5, 144, 0, 0, 454, 456, 1, 0, 0, 0, 455, 443, 1, 0, 0, 0, 455, 451, 1, 0, 0, 0, 456, 471, 1, 0, 0, 0, 457, 458, 10, 3, 0, 0, 458, 459, 3, 72, 36, 0, 459, 460, 3, 68, 34, 4, 460, 470, 1, 0, 0, 0, 461, 463, 10, 4, 0, 0, 462, 464, 3, 70, 35, 0, 463, 462, 1, 0, 0, 0, 463, 464, 1, 0, 0, 0, 464, 465, 1, 0, 0, 0, 465, 466, 5, 45, 0, 0, 466, 467, 3, 68, 34, 0, 467, 468, 3, 74, 37, 0, 468, 470, 1, 0, 0, 0, 469, 457, 1, 0, 0, 0, 469, 461, 1, 0, 0, 0, 470, 473, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 471, 472, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 474, 476, 7, 2, 0, 0, 475, 474, 1, 0, 0, 0, 475, 476, 1, 0, 0, 0, 476, 477, 1, 0, 0, 0, 477, 484, 5, 42, 0, 0, 478, 480, 5, 42, 0, 0, 479, 481, 7, 2, 0, 0, 480, 479, 1, 0, 0, 0, 480, 481, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 484, 7, 2, 0, 0, 483, 475, 1, 0, 0, 0, 483, 478, 1, 0, 0, 0, 483, 482, 1, 0, 0, 0, 484, 518, 1, 0, 0, 0, 485, 487, 7, 3, 0, 0, 486, 485, 1, 0, 0, 0, 486, 487, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 490, 7, 4, 0, 0, 489, 491, 5, 63, 0, 0, 490, 489, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 500, 1, 0, 0, 0, 492, 494, 7, 4, 0, 0, 493, 495, 5, 63, 0, 0, 494, 493, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 497, 1, 0, 0, 0, 496, 498, 7, 3, 0, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 500, 1, 0, 0, 0, 499, 486, 1, 0, 0, 0, 499, 492, 1, 0, 0, 0, 500, 518, 1, 0, 0, 0, 501, 503, 7, 5, 0, 0, 502, 501, 1, 0, 0, 0, 502, 503, 1, 0, 0, 0, 503, 504, 1, 0, 0, 0, 504, 506, 5, 33, 0, 0, 505, 507, 5, 63, 0, 0, 506, 505, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 516, 1, 0, 0, 0, 508, 510, 5, 33, 0, 0, 509, 511, 5, 63, 0, 0, 510, 509, 1, 0, 0, 0, 510, 511, 1, 0, 0, 0, 511, 513, 1, 0, 0, 0, 512, 514, 7, 5, 0, 0, 513, 512, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 516, 1, 0, 0, 0, 515, 502, 1, 0, 0, 0, 515, 508, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 483, 1, 0, 0, 0, 517, 499, 1, 0, 0, 0, 517, 515, 1, 0, 0, 0, 518, 71, 1, 0, 0, 0, 519, 520, 5, 16, 0, 0, 520, 523, 5, 45, 0, 0, 521, 523, 5, 112, 0, 0, 522, 519, 1, 0, 0, 0, 522, 521, 1, 0, 0, 0, 523, 73, 1, 0, 0, 0, 524, 525, 5, 60, 0, 0, 525, 534, 3, 104, 52, 0, 526, 527, 5, 92, 0, 0, 527, 528, 5, 126, 0, 0, 528, 529, 3, 104, 52, 0, 529, 530, 5, 144, 0, 0, 530, 534, 1, 0, 0, 0, 531, 532, 5, 92, 0, 0, 532, 534, 3, 104, 52, 0, 533, 524, 1, 0, 0, 0, 533, 526, 1, 0, 0, 0, 533, 531, 1, 0, 0, 0, 534, 75, 1, 0, 0, 0, 535, 536, 5, 75, 0, 0, 536, 539, 3, 82, 41, 0, 537, 538, 5, 59, 0, 0, 538, 540, 3, 82, 41, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 77, 1, 0, 0, 0, 541, 546, 3, 80, 40, 0, 542, 543, 5, 112, 0, 0, 543, 545, 3, 80, 40, 0, 544, 542, 1, 0, 0, 0, 545, 548, 1, 0, 0, 0, 546, 544, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 79, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 549, 551, 3, 106, 53, 0, 550, 552, 7, 6, 0, 0, 551, 550, 1, 0, 0, 0, 551, 552, 1, 0, 0, 0, 552, 555, 1, 0, 0, 0, 553, 554, 5, 58, 0, 0, 554, 556, 7, 7, 0, 0, 555, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 559, 1, 0, 0, 0, 557, 558, 5, 15, 0, 0, 558, 560, 5, 106, 0, 0, 559, 557, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 81, 1, 0, 0, 0, 561, 568, 3, 154, 77, 0, 562, 565, 3, 138, 69, 0, 563, 564, 5, 146, 0, 0, 564, 566, 3, 138, 69, 0, 565, 563, 1, 0, 0, 0, 565, 566, 1, 0, 0, 0, 566, 568, 1, 0, 0, 0, 567, 561, 1, 0, 0, 0, 567, 562, 1, 0, 0, 0, 568, 83, 1, 0, 0, 0, 569, 574, 3, 86, 43, 0, 570, 571, 5, 112, 0, 0, 571, 573, 3, 86, 43, 0, 572, 570, 1, 0, 0, 0, 573, 576, 1, 0, 0, 0, 574, 572, 1, 0, 0, 0, 574, 575, 1, 0, 0, 0, 575, 85, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 577, 578, 3, 150, 75, 0, 578, 579, 5, 118, 0, 0, 579, 580, 3, 140, 70, 0, 580, 87, 1, 0, 0, 0, 581, 583, 3, 90, 45, 0, 582, 581, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 585, 1, 0, 0, 0, 584, 586, 3, 92, 46, 0, 585, 584, 1, 0, 0, 0, 585, 586, 1, 0, 0, 0, 586, 588, 1, 0, 0, 0, 587, 589, 3, 94, 47, 0, 588, 587, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 89, 1, 0, 0, 0, 590, 591, 5, 65, 0, 0, 591, 592, 5, 11, 0, 0, 592, 593, 3, 104, 52, 0, 593, 91, 1, 0, 0, 0, 594, 595, 5, 62, 0, 0, 595, 596, 5, 11, 0, 0, 596, 597, 3, 78, 39, 0, 597, 93, 1, 0, 0, 0, 598, 599, 7, 8, 0, 0, 599, 600, 3, 96, 48, 0, 600, 95, 1, 0, 0, 0, 601, 608, 3, 98, 49, 0, 602, 603, 5, 9, 0, 0, 603, 604, 3, 98, 49, 0, 604, 605, 5, 2, 0, 0, 605, 606, 3, 98, 49, 0, 606, 608, 1, 0, 0, 0, 607, 601, 1, 0, 0, 0, 607, 602, 1, 0, 0, 0, 608, 97, 1, 0, 0, 0, 609, 610, 5, 18, 0, 0, 610, 622, 5, 73, 0, 0, 611, 612, 5, 90, 0, 0, 612, 622, 5, 66, 0, 0, 613, 614, 5, 90, 0, 0, 614, 622, 5, 30, 0, 0, 615, 616, 3, 138, 69, 0, 616, 617, 5, 66, 0, 0, 617, 622, 1, 0, 0, 0, 618, 619, 3, 138, 69, 0, 619, 620, 5, 30, 0, 0, 620, 622, 1, 0, 0, 0, 621, 609, 1, 0, 0, 0, 621, 611, 1, 0, 0, 0, 621, 613, 1, 0, 0, 0, 621, 615, 1, 0, 0, 0, 621, 618, 1, 0, 0, 0, 622, 99, 1, 0, 0, 0, 623, 624, 3, 106, 53, 0, 624, 625, 5, 0, 0, 1, 625, 101, 1, 0, 0, 0, 626, 674, 3, 150, 75, 0, 627, 628, 3, 150, 75, 0, 628, 629, 5, 126, 0, 0, 629, 630, 3, 150, 75, 0, 630, 637, 3, 102, 51, 0, 631, 632, 5, 112, 0, 0, 632, 633, 3, 150, 75, 0, 633, 634, 3, 102, 51, 0, 634, 636, 1, 0, 0, 0, 635, 631, 1, 0, 0, 0, 636, 639, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 637, 638, 1, 0, 0, 0, 638, 640, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 640, 641, 5, 144, 0, 0, 641, 674, 1, 0, 0, 0, 642, 643, 3, 150, 75, 0, 643, 644, 5, 126, 0, 0, 644, 649, 3, 152, 76, 0, 645, 646, 5, 112, 0, 0, 646, 648, 3, 152, 76, 0, 647, 645, 1, 0, 0, 0, 648, 651, 1, 0, 0, 0, 649, 647, 1, 0, 0, 0, 649, 650, 1, 0, 0, 0, 650, 652, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 652, 653, 5, 144, 0, 0, 653, 674, 1, 0, 0, 0, 654, 655, 3, 150, 75, 0, 655, 656, 5, 126, 0, 0, 656, 661, 3, 102, 51, 0, 657, 658, 5, 112, 0, 0, 658, 660, 3, 102, 51, 0, 659, 657, 1, 0, 0, 0, 660, 663, 1, 0, 0, 0, 661, 659, 1, 0, 0, 0, 661, 662, 1, 0, 0, 0, 662, 664, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 664, 665, 5, 144, 0, 0, 665, 674, 1, 0, 0, 0, 666, 667, 3, 150, 75, 0, 667, 669, 5, 126, 0, 0, 668, 670, 3, 104, 52, 0, 669, 668, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 672, 5, 144, 0, 0, 672, 674, 1, 0, 0, 0, 673, 626, 1, 0, 0, 0, 673, 627, 1, 0, 0, 0, 673, 642, 1, 0, 0, 0, 673, 654, 1, 0, 0, 0, 673, 666, 1, 0, 0, 0, 674, 103, 1, 0, 0, 0, 675, 680, 3, 106, 53, 0, 676, 677, 5, 112, 0, 0, 677, 679, 3, 106, 53, 0, 678, 676, 1, 0, 0, 0, 679, 682, 1, 0, 0, 0, 680, 678, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 681, 105, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 683, 684, 6, 53, -1, 0, 684, 686, 5, 12, 0, 0, 685, 687, 3, 106, 53, 0, 686, 685, 1, 0, 0, 0, 686, 687, 1, 0, 0, 0, 687, 693, 1, 0, 0, 0, 688, 689, 5, 94, 0, 0, 689, 690, 3, 106, 53, 0, 690, 691, 5, 81, 0, 0, 691, 692, 3, 106, 53, 0, 692, 694, 1, 0, 0, 0, 693, 688, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 693, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 699, 1, 0, 0, 0, 697, 698, 5, 24, 0, 0, 698, 700, 3, 106, 53, 0, 699, 697, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 702, 5, 25, 0, 0, 702, 833, 1, 0, 0, 0, 703, 704, 5, 13, 0, 0, 704, 705, 5, 126, 0, 0, 705, 706, 3, 106, 53, 0, 706, 707, 5, 6, 0, 0, 707, 708, 3, 102, 51, 0, 708, 709, 5, 144, 0, 0, 709, 833, 1, 0, 0, 0, 710, 711, 5, 19, 0, 0, 711, 833, 5, 106, 0, 0, 712, 713, 5, 43, 0, 0, 713, 714, 3, 106, 53, 0, 714, 715, 3, 142, 71, 0, 715, 833, 1, 0, 0, 0, 716, 717, 5, 80, 0, 0, 717, 718, 5, 126, 0, 0, 718, 719, 3, 106, 53, 0, 719, 720, 5, 32, 0, 0, 720, 723, 3, 106, 53, 0, 721, 722, 5, 31, 0, 0, 722, 724, 3, 106, 53, 0, 723, 721, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 144, 0, 0, 726, 833, 1, 0, 0, 0, 727, 728, 5, 83, 0, 0, 728, 833, 5, 106, 0, 0, 729, 730, 5, 88, 0, 0, 730, 731, 5, 126, 0, 0, 731, 732, 7, 9, 0, 0, 732, 733, 3, 156, 78, 0, 733, 734, 5, 32, 0, 0, 734, 735, 3, 106, 53, 0, 735, 736, 5, 144, 0, 0, 736, 833, 1, 0, 0, 0, 737, 738, 3, 150, 75, 0, 738, 740, 5, 126, 0, 0, 739, 741, 3, 104, 52, 0, 740, 739, 1, 0, 0, 0, 740, 741, 1, 0, 0, 0, 741, 742, 1, 0, 0, 0, 742, 743, 5, 144, 0, 0, 743, 752, 1, 0, 0, 0, 744, 746, 5, 126, 0, 0, 745, 747, 5, 23, 0, 0, 746, 745, 1, 0, 0, 0, 746, 747, 1, 0, 0, 0, 747, 749, 1, 0, 0, 0, 748, 750, 3, 108, 54, 0, 749, 748, 1, 0, 0, 0, 749, 750, 1, 0, 0, 0, 750, 751, 1, 0, 0, 0, 751, 753, 5, 144, 0, 0, 752, 744, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 754, 1, 0, 0, 0, 754, 755, 5, 64, 0, 0, 755, 756, 5, 126, 0, 0, 756, 757, 3, 88, 44, 0, 757, 758, 5, 144, 0, 0, 758, 833, 1, 0, 0, 0, 759, 760, 3, 150, 75, 0, 760, 762, 5, 126, 0, 0, 761, 763, 3, 104, 52, 0, 762, 761, 1, 0, 0, 0, 762, 763, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 765, 5, 144, 0, 0, 765, 774, 1, 0, 0, 0, 766, 768, 5, 126, 0, 0, 767, 769, 5, 23, 0, 0, 768, 767, 1, 0, 0, 0, 768, 769, 1, 0, 0, 0, 769, 771, 1, 0, 0, 0, 770, 772, 3, 108, 54, 0, 771, 770, 1, 0, 0, 0, 771, 772, 1, 0, 0, 0, 772, 773, 1, 0, 0, 0, 773, 775, 5, 144, 0, 0, 774, 766, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 776, 1, 0, 0, 0, 776, 777, 5, 64, 0, 0, 777, 778, 3, 150, 75, 0, 778, 833, 1, 0, 0, 0, 779, 785, 3, 150, 75, 0, 780, 782, 5, 126, 0, 0, 781, 783, 3, 104, 52, 0, 782, 781, 1, 0, 0, 0, 782, 783, 1, 0, 0, 0, 783, 784, 1, 0, 0, 0, 784, 786, 5, 144, 0, 0, 785, 780, 1, 0, 0, 0, 785, 786, 1, 0, 0, 0, 786, 787, 1, 0, 0, 0, 787, 789, 5, 126, 0, 0, 788, 790, 5, 23, 0, 0, 789, 788, 1, 0, 0, 0, 789, 790, 1, 0, 0, 0, 790, 792, 1, 0, 0, 0, 791, 793, 3, 108, 54, 0, 792, 791, 1, 0, 0, 0, 792, 793, 1, 0, 0, 0, 793, 794, 1, 0, 0, 0, 794, 795, 5, 144, 0, 0, 795, 833, 1, 0, 0, 0, 796, 833, 3, 114, 57, 0, 797, 833, 3, 158, 79, 0, 798, 833, 3, 140, 70, 0, 799, 800, 5, 114, 0, 0, 800, 833, 3, 106, 53, 19, 801, 802, 5, 56, 0, 0, 802, 833, 3, 106, 53, 13, 803, 804, 3, 130, 65, 0, 804, 805, 5, 116, 0, 0, 805, 807, 1, 0, 0, 0, 806, 803, 1, 0, 0, 0, 806, 807, 1, 0, 0, 0, 807, 808, 1, 0, 0, 0, 808, 833, 5, 108, 0, 0, 809, 810, 5, 126, 0, 0, 810, 811, 3, 34, 17, 0, 811, 812, 5, 144, 0, 0, 812, 833, 1, 0, 0, 0, 813, 814, 5, 126, 0, 0, 814, 815, 3, 106, 53, 0, 815, 816, 5, 144, 0, 0, 816, 833, 1, 0, 0, 0, 817, 818, 5, 126, 0, 0, 818, 819, 3, 104, 52, 0, 819, 820, 5, 144, 0, 0, 820, 833, 1, 0, 0, 0, 821, 823, 5, 125, 0, 0, 822, 824, 3, 104, 52, 0, 823, 822, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 825, 1, 0, 0, 0, 825, 833, 5, 143, 0, 0, 826, 828, 5, 124, 0, 0, 827, 829, 3, 30, 15, 0, 828, 827, 1, 0, 0, 0, 828, 829, 1, 0, 0, 0, 829, 830, 1, 0, 0, 0, 830, 833, 5, 142, 0, 0, 831, 833, 3, 122, 61, 0, 832, 683, 1, 0, 0, 0, 832, 703, 1, 0, 0, 0, 832, 710, 1, 0, 0, 0, 832, 712, 1, 0, 0, 0, 832, 716, 1, 0, 0, 0, 832, 727, 1, 0, 0, 0, 832, 729, 1, 0, 0, 0, 832, 737, 1, 0, 0, 0, 832, 759, 1, 0, 0, 0, 832, 779, 1, 0, 0, 0, 832, 796, 1, 0, 0, 0, 832, 797, 1, 0, 0, 0, 832, 798, 1, 0, 0, 0, 832, 799, 1, 0, 0, 0, 832, 801, 1, 0, 0, 0, 832, 806, 1, 0, 0, 0, 832, 809, 1, 0, 0, 0, 832, 813, 1, 0, 0, 0, 832, 817, 1, 0, 0, 0, 832, 821, 1, 0, 0, 0, 832, 826, 1, 0, 0, 0, 832, 831, 1, 0, 0, 0, 833, 927, 1, 0, 0, 0, 834, 838, 10, 18, 0, 0, 835, 839, 5, 108, 0, 0, 836, 839, 5, 146, 0, 0, 837, 839, 5, 133, 0, 0, 838, 835, 1, 0, 0, 0, 838, 836, 1, 0, 0, 0, 838, 837, 1, 0, 0, 0, 839, 840, 1, 0, 0, 0, 840, 926, 3, 106, 53, 19, 841, 845, 10, 17, 0, 0, 842, 846, 5, 134, 0, 0, 843, 846, 5, 114, 0, 0, 844, 846, 5, 113, 0, 0, 845, 842, 1, 0, 0, 0, 845, 843, 1, 0, 0, 0, 845, 844, 1, 0, 0, 0, 846, 847, 1, 0, 0, 0, 847, 926, 3, 106, 53, 18, 848, 873, 10, 16, 0, 0, 849, 874, 5, 117, 0, 0, 850, 874, 5, 118, 0, 0, 851, 874, 5, 129, 0, 0, 852, 874, 5, 127, 0, 0, 853, 874, 5, 128, 0, 0, 854, 874, 5, 119, 0, 0, 855, 874, 5, 120, 0, 0, 856, 858, 5, 56, 0, 0, 857, 856, 1, 0, 0, 0, 857, 858, 1, 0, 0, 0, 858, 859, 1, 0, 0, 0, 859, 861, 5, 40, 0, 0, 860, 862, 5, 14, 0, 0, 861, 860, 1, 0, 0, 0, 861, 862, 1, 0, 0, 0, 862, 874, 1, 0, 0, 0, 863, 865, 5, 56, 0, 0, 864, 863, 1, 0, 0, 0, 864, 865, 1, 0, 0, 0, 865, 866, 1, 0, 0, 0, 866, 874, 7, 10, 0, 0, 867, 874, 5, 140, 0, 0, 868, 874, 5, 141, 0, 0, 869, 874, 5, 131, 0, 0, 870, 874, 5, 122, 0, 0, 871, 874, 5, 123, 0, 0, 872, 874, 5, 130, 0, 0, 873, 849, 1, 0, 0, 0, 873, 850, 1, 0, 0, 0, 873, 851, 1, 0, 0, 0, 873, 852, 1, 0, 0, 0, 873, 853, 1, 0, 0, 0, 873, 854, 1, 0, 0, 0, 873, 855, 1, 0, 0, 0, 873, 857, 1, 0, 0, 0, 873, 864, 1, 0, 0, 0, 873, 867, 1, 0, 0, 0, 873, 868, 1, 0, 0, 0, 873, 869, 1, 0, 0, 0, 873, 870, 1, 0, 0, 0, 873, 871, 1, 0, 0, 0, 873, 872, 1, 0, 0, 0, 874, 875, 1, 0, 0, 0, 875, 926, 3, 106, 53, 17, 876, 877, 10, 14, 0, 0, 877, 878, 5, 132, 0, 0, 878, 926, 3, 106, 53, 15, 879, 880, 10, 12, 0, 0, 880, 881, 5, 2, 0, 0, 881, 926, 3, 106, 53, 13, 882, 883, 10, 11, 0, 0, 883, 884, 5, 61, 0, 0, 884, 926, 3, 106, 53, 12, 885, 887, 10, 10, 0, 0, 886, 888, 5, 56, 0, 0, 887, 886, 1, 0, 0, 0, 887, 888, 1, 0, 0, 0, 888, 889, 1, 0, 0, 0, 889, 890, 5, 9, 0, 0, 890, 891, 3, 106, 53, 0, 891, 892, 5, 2, 0, 0, 892, 893, 3, 106, 53, 11, 893, 926, 1, 0, 0, 0, 894, 895, 10, 9, 0, 0, 895, 896, 5, 135, 0, 0, 896, 897, 3, 106, 53, 0, 897, 898, 5, 111, 0, 0, 898, 899, 3, 106, 53, 9, 899, 926, 1, 0, 0, 0, 900, 901, 10, 22, 0, 0, 901, 902, 5, 125, 0, 0, 902, 903, 3, 106, 53, 0, 903, 904, 5, 143, 0, 0, 904, 926, 1, 0, 0, 0, 905, 906, 10, 21, 0, 0, 906, 907, 5, 116, 0, 0, 907, 926, 5, 104, 0, 0, 908, 909, 10, 20, 0, 0, 909, 910, 5, 116, 0, 0, 910, 926, 3, 150, 75, 0, 911, 912, 10, 15, 0, 0, 912, 914, 5, 44, 0, 0, 913, 915, 5, 56, 0, 0, 914, 913, 1, 0, 0, 0, 914, 915, 1, 0, 0, 0, 915, 916, 1, 0, 0, 0, 916, 926, 5, 57, 0, 0, 917, 923, 10, 8, 0, 0, 918, 924, 3, 148, 74, 0, 919, 920, 5, 6, 0, 0, 920, 924, 3, 150, 75, 0, 921, 922, 5, 6, 0, 0, 922, 924, 5, 106, 0, 0, 923, 918, 1, 0, 0, 0, 923, 919, 1, 0, 0, 0, 923, 921, 1, 0, 0, 0, 924, 926, 1, 0, 0, 0, 925, 834, 1, 0, 0, 0, 925, 841, 1, 0, 0, 0, 925, 848, 1, 0, 0, 0, 925, 876, 1, 0, 0, 0, 925, 879, 1, 0, 0, 0, 925, 882, 1, 0, 0, 0, 925, 885, 1, 0, 0, 0, 925, 894, 1, 0, 0, 0, 925, 900, 1, 0, 0, 0, 925, 905, 1, 0, 0, 0, 925, 908, 1, 0, 0, 0, 925, 911, 1, 0, 0, 0, 925, 917, 1, 0, 0, 0, 926, 929, 1, 0, 0, 0, 927, 925, 1, 0, 0, 0, 927, 928, 1, 0, 0, 0, 928, 107, 1, 0, 0, 0, 929, 927, 1, 0, 0, 0, 930, 935, 3, 110, 55, 0, 931, 932, 5, 112, 0, 0, 932, 934, 3, 110, 55, 0, 933, 931, 1, 0, 0, 0, 934, 937, 1, 0, 0, 0, 935, 933, 1, 0, 0, 0, 935, 936, 1, 0, 0, 0, 936, 109, 1, 0, 0, 0, 937, 935, 1, 0, 0, 0, 938, 941, 3, 112, 56, 0, 939, 941, 3, 106, 53, 0, 940, 938, 1, 0, 0, 0, 940, 939, 1, 0, 0, 0, 941, 111, 1, 0, 0, 0, 942, 943, 5, 126, 0, 0, 943, 948, 3, 150, 75, 0, 944, 945, 5, 112, 0, 0, 945, 947, 3, 150, 75, 0, 946, 944, 1, 0, 0, 0, 947, 950, 1, 0, 0, 0, 948, 946, 1, 0, 0, 0, 948, 949, 1, 0, 0, 0, 949, 951, 1, 0, 0, 0, 950, 948, 1, 0, 0, 0, 951, 952, 5, 144, 0, 0, 952, 962, 1, 0, 0, 0, 953, 958, 3, 150, 75, 0, 954, 955, 5, 112, 0, 0, 955, 957, 3, 150, 75, 0, 956, 954, 1, 0, 0, 0, 957, 960, 1, 0, 0, 0, 958, 956, 1, 0, 0, 0, 958, 959, 1, 0, 0, 0, 959, 962, 1, 0, 0, 0, 960, 958, 1, 0, 0, 0, 961, 942, 1, 0, 0, 0, 961, 953, 1, 0, 0, 0, 962, 963, 1, 0, 0, 0, 963, 964, 5, 107, 0, 0, 964, 965, 3, 106, 53, 0, 965, 113, 1, 0, 0, 0, 966, 967, 5, 128, 0, 0, 967, 971, 3, 150, 75, 0, 968, 970, 3, 116, 58, 0, 969, 968, 1, 0, 0, 0, 970, 973, 1, 0, 0, 0, 971, 969, 1, 0, 0, 0, 971, 972, 1, 0, 0, 0, 972, 974, 1, 0, 0, 0, 973, 971, 1, 0, 0, 0, 974, 975, 5, 146, 0, 0, 975, 976, 5, 120, 0, 0, 976, 995, 1, 0, 0, 0, 977, 978, 5, 128, 0, 0, 978, 982, 3, 150, 75, 0, 979, 981, 3, 116, 58, 0, 980, 979, 1, 0, 0, 0, 981, 984, 1, 0, 0, 0, 982, 980, 1, 0, 0, 0, 982, 983, 1, 0, 0, 0, 983, 985, 1, 0, 0, 0, 984, 982, 1, 0, 0, 0, 985, 987, 5, 120, 0, 0, 986, 988, 3, 114, 57, 0, 987, 986, 1, 0, 0, 0, 987, 988, 1, 0, 0, 0, 988, 989, 1, 0, 0, 0, 989, 990, 5, 128, 0, 0, 990, 991, 5, 146, 0, 0, 991, 992, 3, 150, 75, 0, 992, 993, 5, 120, 0, 0, 993, 995, 1, 0, 0, 0, 994, 966, 1, 0, 0, 0, 994, 977, 1, 0, 0, 0, 995, 115, 1, 0, 0, 0, 996, 997, 3, 150, 75, 0, 997, 998, 5, 118, 0, 0, 998, 999, 3, 156, 78, 0, 999, 1008, 1, 0, 0, 0, 1000, 1001, 3, 150, 75, 0, 1001, 1002, 5, 118, 0, 0, 1002, 1003, 5, 124, 0, 0, 1003, 1004, 3, 106, 53, 0, 1004, 1005, 5, 142, 0, 0, 1005, 1008, 1, 0, 0, 0, 1006, 1008, 3, 150, 75, 0, 1007, 996, 1, 0, 0, 0, 1007, 1000, 1, 0, 0, 0, 1007, 1006, 1, 0, 0, 0, 1008, 117, 1, 0, 0, 0, 1009, 1014, 3, 120, 60, 0, 1010, 1011, 5, 112, 0, 0, 1011, 1013, 3, 120, 60, 0, 1012, 1010, 1, 0, 0, 0, 1013, 1016, 1, 0, 0, 0, 1014, 1012, 1, 0, 0, 0, 1014, 1015, 1, 0, 0, 0, 1015, 119, 1, 0, 0, 0, 1016, 1014, 1, 0, 0, 0, 1017, 1018, 3, 150, 75, 0, 1018, 1019, 5, 6, 0, 0, 1019, 1020, 5, 126, 0, 0, 1020, 1021, 3, 34, 17, 0, 1021, 1022, 5, 144, 0, 0, 1022, 1028, 1, 0, 0, 0, 1023, 1024, 3, 106, 53, 0, 1024, 1025, 5, 6, 0, 0, 1025, 1026, 3, 150, 75, 0, 1026, 1028, 1, 0, 0, 0, 1027, 1017, 1, 0, 0, 0, 1027, 1023, 1, 0, 0, 0, 1028, 121, 1, 0, 0, 0, 1029, 1037, 3, 154, 77, 0, 1030, 1031, 3, 130, 65, 0, 1031, 1032, 5, 116, 0, 0, 1032, 1034, 1, 0, 0, 0, 1033, 1030, 1, 0, 0, 0, 1033, 1034, 1, 0, 0, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1037, 3, 124, 62, 0, 1036, 1029, 1, 0, 0, 0, 1036, 1033, 1, 0, 0, 0, 1037, 123, 1, 0, 0, 0, 1038, 1043, 3, 150, 75, 0, 1039, 1040, 5, 116, 0, 0, 1040, 1042, 3, 150, 75, 0, 1041, 1039, 1, 0, 0, 0, 1042, 1045, 1, 0, 0, 0, 1043, 1041, 1, 0, 0, 0, 1043, 1044, 1, 0, 0, 0, 1044, 125, 1, 0, 0, 0, 1045, 1043, 1, 0, 0, 0, 1046, 1047, 6, 63, -1, 0, 1047, 1056, 3, 130, 65, 0, 1048, 1056, 3, 128, 64, 0, 1049, 1050, 5, 126, 0, 0, 1050, 1051, 3, 34, 17, 0, 1051, 1052, 5, 144, 0, 0, 1052, 1056, 1, 0, 0, 0, 1053, 1056, 3, 114, 57, 0, 1054, 1056, 3, 154, 77, 0, 1055, 1046, 1, 0, 0, 0, 1055, 1048, 1, 0, 0, 0, 1055, 1049, 1, 0, 0, 0, 1055, 1053, 1, 0, 0, 0, 1055, 1054, 1, 0, 0, 0, 1056, 1065, 1, 0, 0, 0, 1057, 1061, 10, 3, 0, 0, 1058, 1062, 3, 148, 74, 0, 1059, 1060, 5, 6, 0, 0, 1060, 1062, 3, 150, 75, 0, 1061, 1058, 1, 0, 0, 0, 1061, 1059, 1, 0, 0, 0, 1062, 1064, 1, 0, 0, 0, 1063, 1057, 1, 0, 0, 0, 1064, 1067, 1, 0, 0, 0, 1065, 1063, 1, 0, 0, 0, 1065, 1066, 1, 0, 0, 0, 1066, 127, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1068, 1069, 3, 150, 75, 0, 1069, 1071, 5, 126, 0, 0, 1070, 1072, 3, 132, 66, 0, 1071, 1070, 1, 0, 0, 0, 1071, 1072, 1, 0, 0, 0, 1072, 1073, 1, 0, 0, 0, 1073, 1074, 5, 144, 0, 0, 1074, 129, 1, 0, 0, 0, 1075, 1076, 3, 134, 67, 0, 1076, 1077, 5, 116, 0, 0, 1077, 1079, 1, 0, 0, 0, 1078, 1075, 1, 0, 0, 0, 1078, 1079, 1, 0, 0, 0, 1079, 1080, 1, 0, 0, 0, 1080, 1081, 3, 150, 75, 0, 1081, 131, 1, 0, 0, 0, 1082, 1087, 3, 106, 53, 0, 1083, 1084, 5, 112, 0, 0, 1084, 1086, 3, 106, 53, 0, 1085, 1083, 1, 0, 0, 0, 1086, 1089, 1, 0, 0, 0, 1087, 1085, 1, 0, 0, 0, 1087, 1088, 1, 0, 0, 0, 1088, 133, 1, 0, 0, 0, 1089, 1087, 1, 0, 0, 0, 1090, 1091, 3, 150, 75, 0, 1091, 135, 1, 0, 0, 0, 1092, 1101, 5, 102, 0, 0, 1093, 1094, 5, 116, 0, 0, 1094, 1101, 7, 11, 0, 0, 1095, 1096, 5, 104, 0, 0, 1096, 1098, 5, 116, 0, 0, 1097, 1099, 7, 11, 0, 0, 1098, 1097, 1, 0, 0, 0, 1098, 1099, 1, 0, 0, 0, 1099, 1101, 1, 0, 0, 0, 1100, 1092, 1, 0, 0, 0, 1100, 1093, 1, 0, 0, 0, 1100, 1095, 1, 0, 0, 0, 1101, 137, 1, 0, 0, 0, 1102, 1104, 7, 12, 0, 0, 1103, 1102, 1, 0, 0, 0, 1103, 1104, 1, 0, 0, 0, 1104, 1111, 1, 0, 0, 0, 1105, 1112, 3, 136, 68, 0, 1106, 1112, 5, 103, 0, 0, 1107, 1112, 5, 104, 0, 0, 1108, 1112, 5, 105, 0, 0, 1109, 1112, 5, 41, 0, 0, 1110, 1112, 5, 55, 0, 0, 1111, 1105, 1, 0, 0, 0, 1111, 1106, 1, 0, 0, 0, 1111, 1107, 1, 0, 0, 0, 1111, 1108, 1, 0, 0, 0, 1111, 1109, 1, 0, 0, 0, 1111, 1110, 1, 0, 0, 0, 1112, 139, 1, 0, 0, 0, 1113, 1117, 3, 138, 69, 0, 1114, 1117, 5, 106, 0, 0, 1115, 1117, 5, 57, 0, 0, 1116, 1113, 1, 0, 0, 0, 1116, 1114, 1, 0, 0, 0, 1116, 1115, 1, 0, 0, 0, 1117, 141, 1, 0, 0, 0, 1118, 1119, 7, 13, 0, 0, 1119, 143, 1, 0, 0, 0, 1120, 1121, 7, 14, 0, 0, 1121, 145, 1, 0, 0, 0, 1122, 1123, 7, 15, 0, 0, 1123, 147, 1, 0, 0, 0, 1124, 1127, 5, 101, 0, 0, 1125, 1127, 3, 146, 73, 0, 1126, 1124, 1, 0, 0, 0, 1126, 1125, 1, 0, 0, 0, 1127, 149, 1, 0, 0, 0, 1128, 1132, 5, 101, 0, 0, 1129, 1132, 3, 142, 71, 0, 1130, 1132, 3, 144, 72, 0, 1131, 1128, 1, 0, 0, 0, 1131, 1129, 1, 0, 0, 0, 1131, 1130, 1, 0, 0, 0, 1132, 151, 1, 0, 0, 0, 1133, 1134, 3, 156, 78, 0, 1134, 1135, 5, 118, 0, 0, 1135, 1136, 3, 138, 69, 0, 1136, 153, 1, 0, 0, 0, 1137, 1138, 5, 124, 0, 0, 1138, 1139, 3, 150, 75, 0, 1139, 1140, 5, 142, 0, 0, 1140, 155, 1, 0, 0, 0, 1141, 1144, 5, 106, 0, 0, 1142, 1144, 3, 158, 79, 0, 1143, 1141, 1, 0, 0, 0, 1143, 1142, 1, 0, 0, 0, 1144, 157, 1, 0, 0, 0, 1145, 1149, 5, 137, 0, 0, 1146, 1148, 3, 160, 80, 0, 1147, 1146, 1, 0, 0, 0, 1148, 1151, 1, 0, 0, 0, 1149, 1147, 1, 0, 0, 0, 1149, 1150, 1, 0, 0, 0, 1150, 1152, 1, 0, 0, 0, 1151, 1149, 1, 0, 0, 0, 1152, 1153, 5, 139, 0, 0, 1153, 159, 1, 0, 0, 0, 1154, 1155, 5, 152, 0, 0, 1155, 1156, 3, 106, 53, 0, 1156, 1157, 5, 142, 0, 0, 1157, 1160, 1, 0, 0, 0, 1158, 1160, 5, 151, 0, 0, 1159, 1154, 1, 0, 0, 0, 1159, 1158, 1, 0, 0, 0, 1160, 161, 1, 0, 0, 0, 1161, 1165, 5, 138, 0, 0, 1162, 1164, 3, 164, 82, 0, 1163, 1162, 1, 0, 0, 0, 1164, 1167, 1, 0, 0, 0, 1165, 1163, 1, 0, 0, 0, 1165, 1166, 1, 0, 0, 0, 1166, 1168, 1, 0, 0, 0, 1167, 1165, 1, 0, 0, 0, 1168, 1169, 5, 0, 0, 1, 1169, 163, 1, 0, 0, 0, 1170, 1171, 5, 154, 0, 0, 1171, 1172, 3, 106, 53, 0, 1172, 1173, 5, 142, 0, 0, 1173, 1176, 1, 0, 0, 0, 1174, 1176, 5, 153, 0, 0, 1175, 1170, 1, 0, 0, 0, 1175, 1174, 1, 0, 0, 0, 1176, 165, 1, 0, 0, 0, 141, 169, 176, 185, 200, 212, 224, 240, 251, 265, 271, 281, 290, 293, 297, 300, 304, 307, 310, 313, 316, 320, 324, 327, 330, 333, 337, 340, 349, 355, 376, 393, 410, 416, 422, 433, 435, 446, 449, 455, 463, 469, 471, 475, 480, 483, 486, 490, 494, 497, 499, 502, 506, 510, 513, 515, 517, 522, 533, 539, 546, 551, 555, 559, 565, 567, 574, 582, 585, 588, 607, 621, 637, 649, 661, 669, 673, 680, 686, 695, 699, 723, 740, 746, 749, 752, 762, 768, 771, 774, 782, 785, 789, 792, 806, 823, 828, 832, 838, 845, 857, 861, 864, 873, 887, 914, 923, 925, 927, 935, 940, 948, 958, 961, 971, 982, 987, 994, 1007, 1014, 1027, 1033, 1036, 1043, 1055, 1061, 1065, 1071, 1078, 1087, 1098, 1100, 1103, 1111, 1116, 1126, 1131, 1143, 1149, 1159, 1165, 1175] \ No newline at end of file diff --git a/posthog/hogql/grammar/HogQLParser.py b/posthog/hogql/grammar/HogQLParser.py index 50e81765f514d..9c0c50c0835da 100644 --- a/posthog/hogql/grammar/HogQLParser.py +++ b/posthog/hogql/grammar/HogQLParser.py @@ -10,7 +10,7 @@ def serializedATN(): return [ - 4,1,154,1158,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, + 4,1,154,1178,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, 7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7, 13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2, 20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7, @@ -72,74 +72,76 @@ def serializedATN(): 53,700,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,724, 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,3,53,741,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,3,53,753,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,3,53,763,8,53,1,53,3,53,766,8,53,1,53,1,53,3,53,770,8,53,1, - 53,3,53,773,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, - 53,1,53,1,53,3,53,787,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, - 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,804,8,53,1,53,1,53,1, - 53,3,53,809,8,53,1,53,1,53,3,53,813,8,53,1,53,1,53,1,53,1,53,3,53, - 819,8,53,1,53,1,53,1,53,1,53,1,53,3,53,826,8,53,1,53,1,53,1,53,1, - 53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,838,8,53,1,53,1,53,3,53,842, - 8,53,1,53,3,53,845,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53, - 854,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,3,53,868,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,3,53,741,8,53,1,53,1,53,1,53,1,53,3,53,747,8,53,1, + 53,3,53,750,8,53,1,53,3,53,753,8,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,3,53,763,8,53,1,53,1,53,1,53,1,53,3,53,769,8,53,1,53,3, + 53,772,8,53,1,53,3,53,775,8,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53, + 783,8,53,1,53,3,53,786,8,53,1,53,1,53,3,53,790,8,53,1,53,3,53,793, + 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 3,53,807,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,3,53,824,8,53,1,53,1,53,1,53,3,53,829,8, + 53,1,53,1,53,3,53,833,8,53,1,53,1,53,1,53,1,53,3,53,839,8,53,1,53, + 1,53,1,53,1,53,1,53,3,53,846,8,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,3,53,858,8,53,1,53,1,53,3,53,862,8,53,1,53,3, + 53,865,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,874,8,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,888, + 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,3,53,895,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 3,53,904,8,53,5,53,906,8,53,10,53,12,53,909,9,53,1,54,1,54,1,54, - 5,54,914,8,54,10,54,12,54,917,9,54,1,55,1,55,3,55,921,8,55,1,56, - 1,56,1,56,1,56,5,56,927,8,56,10,56,12,56,930,9,56,1,56,1,56,1,56, - 1,56,1,56,5,56,937,8,56,10,56,12,56,940,9,56,3,56,942,8,56,1,56, - 1,56,1,56,1,57,1,57,1,57,5,57,950,8,57,10,57,12,57,953,9,57,1,57, - 1,57,1,57,1,57,1,57,1,57,5,57,961,8,57,10,57,12,57,964,9,57,1,57, - 1,57,3,57,968,8,57,1,57,1,57,1,57,1,57,1,57,3,57,975,8,57,1,58,1, - 58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,3,58,988,8,58,1, - 59,1,59,1,59,5,59,993,8,59,10,59,12,59,996,9,59,1,60,1,60,1,60,1, - 60,1,60,1,60,1,60,1,60,1,60,1,60,3,60,1008,8,60,1,61,1,61,1,61,1, - 61,3,61,1014,8,61,1,61,3,61,1017,8,61,1,62,1,62,1,62,5,62,1022,8, - 62,10,62,12,62,1025,9,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63, - 1,63,3,63,1036,8,63,1,63,1,63,1,63,1,63,3,63,1042,8,63,5,63,1044, - 8,63,10,63,12,63,1047,9,63,1,64,1,64,1,64,3,64,1052,8,64,1,64,1, - 64,1,65,1,65,1,65,3,65,1059,8,65,1,65,1,65,1,66,1,66,1,66,5,66,1066, - 8,66,10,66,12,66,1069,9,66,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1, - 68,3,68,1079,8,68,3,68,1081,8,68,1,69,3,69,1084,8,69,1,69,1,69,1, - 69,1,69,1,69,1,69,3,69,1092,8,69,1,70,1,70,1,70,3,70,1097,8,70,1, - 71,1,71,1,72,1,72,1,73,1,73,1,74,1,74,3,74,1107,8,74,1,75,1,75,1, - 75,3,75,1112,8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,78,1, - 78,3,78,1124,8,78,1,79,1,79,5,79,1128,8,79,10,79,12,79,1131,9,79, - 1,79,1,79,1,80,1,80,1,80,1,80,1,80,3,80,1140,8,80,1,81,1,81,5,81, - 1144,8,81,10,81,12,81,1147,9,81,1,81,1,81,1,82,1,82,1,82,1,82,1, - 82,3,82,1156,8,82,1,82,0,3,68,106,126,83,0,2,4,6,8,10,12,14,16,18, - 20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62, - 64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104, - 106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136, - 138,140,142,144,146,148,150,152,154,156,158,160,162,164,0,16,2,0, - 17,17,72,72,2,0,42,42,49,49,3,0,1,1,4,4,8,8,4,0,1,1,3,4,8,8,78,78, - 2,0,49,49,71,71,2,0,1,1,4,4,2,0,7,7,21,22,2,0,28,28,47,47,2,0,69, - 69,74,74,3,0,10,10,48,48,87,87,2,0,39,39,51,51,1,0,103,104,2,0,114, - 114,134,134,7,0,20,20,36,36,53,54,68,68,76,76,93,93,99,99,12,0,1, - 19,21,28,30,35,37,40,42,49,51,52,56,56,58,67,69,75,77,92,94,95,97, - 98,4,0,19,19,28,28,37,37,46,46,1288,0,169,1,0,0,0,2,176,1,0,0,0, - 4,178,1,0,0,0,6,180,1,0,0,0,8,189,1,0,0,0,10,195,1,0,0,0,12,212, - 1,0,0,0,14,214,1,0,0,0,16,217,1,0,0,0,18,226,1,0,0,0,20,232,1,0, - 0,0,22,236,1,0,0,0,24,245,1,0,0,0,26,247,1,0,0,0,28,256,1,0,0,0, - 30,260,1,0,0,0,32,271,1,0,0,0,34,275,1,0,0,0,36,290,1,0,0,0,38,293, - 1,0,0,0,40,342,1,0,0,0,42,345,1,0,0,0,44,351,1,0,0,0,46,355,1,0, - 0,0,48,361,1,0,0,0,50,379,1,0,0,0,52,382,1,0,0,0,54,385,1,0,0,0, - 56,395,1,0,0,0,58,398,1,0,0,0,60,402,1,0,0,0,62,435,1,0,0,0,64,437, - 1,0,0,0,66,440,1,0,0,0,68,455,1,0,0,0,70,517,1,0,0,0,72,522,1,0, - 0,0,74,533,1,0,0,0,76,535,1,0,0,0,78,541,1,0,0,0,80,549,1,0,0,0, - 82,567,1,0,0,0,84,569,1,0,0,0,86,577,1,0,0,0,88,582,1,0,0,0,90,590, - 1,0,0,0,92,594,1,0,0,0,94,598,1,0,0,0,96,607,1,0,0,0,98,621,1,0, - 0,0,100,623,1,0,0,0,102,673,1,0,0,0,104,675,1,0,0,0,106,812,1,0, - 0,0,108,910,1,0,0,0,110,920,1,0,0,0,112,941,1,0,0,0,114,974,1,0, - 0,0,116,987,1,0,0,0,118,989,1,0,0,0,120,1007,1,0,0,0,122,1016,1, - 0,0,0,124,1018,1,0,0,0,126,1035,1,0,0,0,128,1048,1,0,0,0,130,1058, - 1,0,0,0,132,1062,1,0,0,0,134,1070,1,0,0,0,136,1080,1,0,0,0,138,1083, - 1,0,0,0,140,1096,1,0,0,0,142,1098,1,0,0,0,144,1100,1,0,0,0,146,1102, - 1,0,0,0,148,1106,1,0,0,0,150,1111,1,0,0,0,152,1113,1,0,0,0,154,1117, - 1,0,0,0,156,1123,1,0,0,0,158,1125,1,0,0,0,160,1139,1,0,0,0,162,1141, - 1,0,0,0,164,1155,1,0,0,0,166,168,3,2,1,0,167,166,1,0,0,0,168,171, + 3,53,915,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,924,8,53,5, + 53,926,8,53,10,53,12,53,929,9,53,1,54,1,54,1,54,5,54,934,8,54,10, + 54,12,54,937,9,54,1,55,1,55,3,55,941,8,55,1,56,1,56,1,56,1,56,5, + 56,947,8,56,10,56,12,56,950,9,56,1,56,1,56,1,56,1,56,1,56,5,56,957, + 8,56,10,56,12,56,960,9,56,3,56,962,8,56,1,56,1,56,1,56,1,57,1,57, + 1,57,5,57,970,8,57,10,57,12,57,973,9,57,1,57,1,57,1,57,1,57,1,57, + 1,57,5,57,981,8,57,10,57,12,57,984,9,57,1,57,1,57,3,57,988,8,57, + 1,57,1,57,1,57,1,57,1,57,3,57,995,8,57,1,58,1,58,1,58,1,58,1,58, + 1,58,1,58,1,58,1,58,1,58,1,58,3,58,1008,8,58,1,59,1,59,1,59,5,59, + 1013,8,59,10,59,12,59,1016,9,59,1,60,1,60,1,60,1,60,1,60,1,60,1, + 60,1,60,1,60,1,60,3,60,1028,8,60,1,61,1,61,1,61,1,61,3,61,1034,8, + 61,1,61,3,61,1037,8,61,1,62,1,62,1,62,5,62,1042,8,62,10,62,12,62, + 1045,9,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,3,63,1056, + 8,63,1,63,1,63,1,63,1,63,3,63,1062,8,63,5,63,1064,8,63,10,63,12, + 63,1067,9,63,1,64,1,64,1,64,3,64,1072,8,64,1,64,1,64,1,65,1,65,1, + 65,3,65,1079,8,65,1,65,1,65,1,66,1,66,1,66,5,66,1086,8,66,10,66, + 12,66,1089,9,66,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68,3,68,1099, + 8,68,3,68,1101,8,68,1,69,3,69,1104,8,69,1,69,1,69,1,69,1,69,1,69, + 1,69,3,69,1112,8,69,1,70,1,70,1,70,3,70,1117,8,70,1,71,1,71,1,72, + 1,72,1,73,1,73,1,74,1,74,3,74,1127,8,74,1,75,1,75,1,75,3,75,1132, + 8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,78,1,78,3,78,1144, + 8,78,1,79,1,79,5,79,1148,8,79,10,79,12,79,1151,9,79,1,79,1,79,1, + 80,1,80,1,80,1,80,1,80,3,80,1160,8,80,1,81,1,81,5,81,1164,8,81,10, + 81,12,81,1167,9,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,3,82,1176, + 8,82,1,82,0,3,68,106,126,83,0,2,4,6,8,10,12,14,16,18,20,22,24,26, + 28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70, + 72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110, + 112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142, + 144,146,148,150,152,154,156,158,160,162,164,0,16,2,0,17,17,72,72, + 2,0,42,42,49,49,3,0,1,1,4,4,8,8,4,0,1,1,3,4,8,8,78,78,2,0,49,49, + 71,71,2,0,1,1,4,4,2,0,7,7,21,22,2,0,28,28,47,47,2,0,69,69,74,74, + 3,0,10,10,48,48,87,87,2,0,39,39,51,51,1,0,103,104,2,0,114,114,134, + 134,7,0,20,20,36,36,53,54,68,68,76,76,93,93,99,99,12,0,1,19,21,28, + 30,35,37,40,42,49,51,52,56,56,58,67,69,75,77,92,94,95,97,98,4,0, + 19,19,28,28,37,37,46,46,1314,0,169,1,0,0,0,2,176,1,0,0,0,4,178,1, + 0,0,0,6,180,1,0,0,0,8,189,1,0,0,0,10,195,1,0,0,0,12,212,1,0,0,0, + 14,214,1,0,0,0,16,217,1,0,0,0,18,226,1,0,0,0,20,232,1,0,0,0,22,236, + 1,0,0,0,24,245,1,0,0,0,26,247,1,0,0,0,28,256,1,0,0,0,30,260,1,0, + 0,0,32,271,1,0,0,0,34,275,1,0,0,0,36,290,1,0,0,0,38,293,1,0,0,0, + 40,342,1,0,0,0,42,345,1,0,0,0,44,351,1,0,0,0,46,355,1,0,0,0,48,361, + 1,0,0,0,50,379,1,0,0,0,52,382,1,0,0,0,54,385,1,0,0,0,56,395,1,0, + 0,0,58,398,1,0,0,0,60,402,1,0,0,0,62,435,1,0,0,0,64,437,1,0,0,0, + 66,440,1,0,0,0,68,455,1,0,0,0,70,517,1,0,0,0,72,522,1,0,0,0,74,533, + 1,0,0,0,76,535,1,0,0,0,78,541,1,0,0,0,80,549,1,0,0,0,82,567,1,0, + 0,0,84,569,1,0,0,0,86,577,1,0,0,0,88,582,1,0,0,0,90,590,1,0,0,0, + 92,594,1,0,0,0,94,598,1,0,0,0,96,607,1,0,0,0,98,621,1,0,0,0,100, + 623,1,0,0,0,102,673,1,0,0,0,104,675,1,0,0,0,106,832,1,0,0,0,108, + 930,1,0,0,0,110,940,1,0,0,0,112,961,1,0,0,0,114,994,1,0,0,0,116, + 1007,1,0,0,0,118,1009,1,0,0,0,120,1027,1,0,0,0,122,1036,1,0,0,0, + 124,1038,1,0,0,0,126,1055,1,0,0,0,128,1068,1,0,0,0,130,1078,1,0, + 0,0,132,1082,1,0,0,0,134,1090,1,0,0,0,136,1100,1,0,0,0,138,1103, + 1,0,0,0,140,1116,1,0,0,0,142,1118,1,0,0,0,144,1120,1,0,0,0,146,1122, + 1,0,0,0,148,1126,1,0,0,0,150,1131,1,0,0,0,152,1133,1,0,0,0,154,1137, + 1,0,0,0,156,1143,1,0,0,0,158,1145,1,0,0,0,160,1159,1,0,0,0,162,1161, + 1,0,0,0,164,1175,1,0,0,0,166,168,3,2,1,0,167,166,1,0,0,0,168,171, 1,0,0,0,169,167,1,0,0,0,169,170,1,0,0,0,170,172,1,0,0,0,171,169, 1,0,0,0,172,173,5,0,0,1,173,1,1,0,0,0,174,177,3,6,3,0,175,177,3, 12,6,0,176,174,1,0,0,0,176,175,1,0,0,0,177,3,1,0,0,0,178,179,3,106, @@ -302,170 +304,178 @@ def serializedATN(): 690,3,106,53,0,690,691,5,81,0,0,691,692,3,106,53,0,692,694,1,0,0, 0,693,688,1,0,0,0,694,695,1,0,0,0,695,693,1,0,0,0,695,696,1,0,0, 0,696,699,1,0,0,0,697,698,5,24,0,0,698,700,3,106,53,0,699,697,1, - 0,0,0,699,700,1,0,0,0,700,701,1,0,0,0,701,702,5,25,0,0,702,813,1, + 0,0,0,699,700,1,0,0,0,700,701,1,0,0,0,701,702,5,25,0,0,702,833,1, 0,0,0,703,704,5,13,0,0,704,705,5,126,0,0,705,706,3,106,53,0,706, - 707,5,6,0,0,707,708,3,102,51,0,708,709,5,144,0,0,709,813,1,0,0,0, - 710,711,5,19,0,0,711,813,5,106,0,0,712,713,5,43,0,0,713,714,3,106, - 53,0,714,715,3,142,71,0,715,813,1,0,0,0,716,717,5,80,0,0,717,718, + 707,5,6,0,0,707,708,3,102,51,0,708,709,5,144,0,0,709,833,1,0,0,0, + 710,711,5,19,0,0,711,833,5,106,0,0,712,713,5,43,0,0,713,714,3,106, + 53,0,714,715,3,142,71,0,715,833,1,0,0,0,716,717,5,80,0,0,717,718, 5,126,0,0,718,719,3,106,53,0,719,720,5,32,0,0,720,723,3,106,53,0, 721,722,5,31,0,0,722,724,3,106,53,0,723,721,1,0,0,0,723,724,1,0, - 0,0,724,725,1,0,0,0,725,726,5,144,0,0,726,813,1,0,0,0,727,728,5, - 83,0,0,728,813,5,106,0,0,729,730,5,88,0,0,730,731,5,126,0,0,731, + 0,0,724,725,1,0,0,0,725,726,5,144,0,0,726,833,1,0,0,0,727,728,5, + 83,0,0,728,833,5,106,0,0,729,730,5,88,0,0,730,731,5,126,0,0,731, 732,7,9,0,0,732,733,3,156,78,0,733,734,5,32,0,0,734,735,3,106,53, - 0,735,736,5,144,0,0,736,813,1,0,0,0,737,738,3,150,75,0,738,740,5, + 0,735,736,5,144,0,0,736,833,1,0,0,0,737,738,3,150,75,0,738,740,5, 126,0,0,739,741,3,104,52,0,740,739,1,0,0,0,740,741,1,0,0,0,741,742, - 1,0,0,0,742,743,5,144,0,0,743,744,1,0,0,0,744,745,5,64,0,0,745,746, - 5,126,0,0,746,747,3,88,44,0,747,748,5,144,0,0,748,813,1,0,0,0,749, - 750,3,150,75,0,750,752,5,126,0,0,751,753,3,104,52,0,752,751,1,0, - 0,0,752,753,1,0,0,0,753,754,1,0,0,0,754,755,5,144,0,0,755,756,1, - 0,0,0,756,757,5,64,0,0,757,758,3,150,75,0,758,813,1,0,0,0,759,765, - 3,150,75,0,760,762,5,126,0,0,761,763,3,104,52,0,762,761,1,0,0,0, - 762,763,1,0,0,0,763,764,1,0,0,0,764,766,5,144,0,0,765,760,1,0,0, - 0,765,766,1,0,0,0,766,767,1,0,0,0,767,769,5,126,0,0,768,770,5,23, - 0,0,769,768,1,0,0,0,769,770,1,0,0,0,770,772,1,0,0,0,771,773,3,108, - 54,0,772,771,1,0,0,0,772,773,1,0,0,0,773,774,1,0,0,0,774,775,5,144, - 0,0,775,813,1,0,0,0,776,813,3,114,57,0,777,813,3,158,79,0,778,813, - 3,140,70,0,779,780,5,114,0,0,780,813,3,106,53,19,781,782,5,56,0, - 0,782,813,3,106,53,13,783,784,3,130,65,0,784,785,5,116,0,0,785,787, - 1,0,0,0,786,783,1,0,0,0,786,787,1,0,0,0,787,788,1,0,0,0,788,813, - 5,108,0,0,789,790,5,126,0,0,790,791,3,34,17,0,791,792,5,144,0,0, - 792,813,1,0,0,0,793,794,5,126,0,0,794,795,3,106,53,0,795,796,5,144, - 0,0,796,813,1,0,0,0,797,798,5,126,0,0,798,799,3,104,52,0,799,800, - 5,144,0,0,800,813,1,0,0,0,801,803,5,125,0,0,802,804,3,104,52,0,803, - 802,1,0,0,0,803,804,1,0,0,0,804,805,1,0,0,0,805,813,5,143,0,0,806, - 808,5,124,0,0,807,809,3,30,15,0,808,807,1,0,0,0,808,809,1,0,0,0, - 809,810,1,0,0,0,810,813,5,142,0,0,811,813,3,122,61,0,812,683,1,0, - 0,0,812,703,1,0,0,0,812,710,1,0,0,0,812,712,1,0,0,0,812,716,1,0, - 0,0,812,727,1,0,0,0,812,729,1,0,0,0,812,737,1,0,0,0,812,749,1,0, - 0,0,812,759,1,0,0,0,812,776,1,0,0,0,812,777,1,0,0,0,812,778,1,0, - 0,0,812,779,1,0,0,0,812,781,1,0,0,0,812,786,1,0,0,0,812,789,1,0, - 0,0,812,793,1,0,0,0,812,797,1,0,0,0,812,801,1,0,0,0,812,806,1,0, - 0,0,812,811,1,0,0,0,813,907,1,0,0,0,814,818,10,18,0,0,815,819,5, - 108,0,0,816,819,5,146,0,0,817,819,5,133,0,0,818,815,1,0,0,0,818, - 816,1,0,0,0,818,817,1,0,0,0,819,820,1,0,0,0,820,906,3,106,53,19, - 821,825,10,17,0,0,822,826,5,134,0,0,823,826,5,114,0,0,824,826,5, - 113,0,0,825,822,1,0,0,0,825,823,1,0,0,0,825,824,1,0,0,0,826,827, - 1,0,0,0,827,906,3,106,53,18,828,853,10,16,0,0,829,854,5,117,0,0, - 830,854,5,118,0,0,831,854,5,129,0,0,832,854,5,127,0,0,833,854,5, - 128,0,0,834,854,5,119,0,0,835,854,5,120,0,0,836,838,5,56,0,0,837, - 836,1,0,0,0,837,838,1,0,0,0,838,839,1,0,0,0,839,841,5,40,0,0,840, - 842,5,14,0,0,841,840,1,0,0,0,841,842,1,0,0,0,842,854,1,0,0,0,843, - 845,5,56,0,0,844,843,1,0,0,0,844,845,1,0,0,0,845,846,1,0,0,0,846, - 854,7,10,0,0,847,854,5,140,0,0,848,854,5,141,0,0,849,854,5,131,0, - 0,850,854,5,122,0,0,851,854,5,123,0,0,852,854,5,130,0,0,853,829, - 1,0,0,0,853,830,1,0,0,0,853,831,1,0,0,0,853,832,1,0,0,0,853,833, - 1,0,0,0,853,834,1,0,0,0,853,835,1,0,0,0,853,837,1,0,0,0,853,844, - 1,0,0,0,853,847,1,0,0,0,853,848,1,0,0,0,853,849,1,0,0,0,853,850, - 1,0,0,0,853,851,1,0,0,0,853,852,1,0,0,0,854,855,1,0,0,0,855,906, - 3,106,53,17,856,857,10,14,0,0,857,858,5,132,0,0,858,906,3,106,53, - 15,859,860,10,12,0,0,860,861,5,2,0,0,861,906,3,106,53,13,862,863, - 10,11,0,0,863,864,5,61,0,0,864,906,3,106,53,12,865,867,10,10,0,0, - 866,868,5,56,0,0,867,866,1,0,0,0,867,868,1,0,0,0,868,869,1,0,0,0, - 869,870,5,9,0,0,870,871,3,106,53,0,871,872,5,2,0,0,872,873,3,106, - 53,11,873,906,1,0,0,0,874,875,10,9,0,0,875,876,5,135,0,0,876,877, - 3,106,53,0,877,878,5,111,0,0,878,879,3,106,53,9,879,906,1,0,0,0, - 880,881,10,22,0,0,881,882,5,125,0,0,882,883,3,106,53,0,883,884,5, - 143,0,0,884,906,1,0,0,0,885,886,10,21,0,0,886,887,5,116,0,0,887, - 906,5,104,0,0,888,889,10,20,0,0,889,890,5,116,0,0,890,906,3,150, - 75,0,891,892,10,15,0,0,892,894,5,44,0,0,893,895,5,56,0,0,894,893, - 1,0,0,0,894,895,1,0,0,0,895,896,1,0,0,0,896,906,5,57,0,0,897,903, - 10,8,0,0,898,904,3,148,74,0,899,900,5,6,0,0,900,904,3,150,75,0,901, - 902,5,6,0,0,902,904,5,106,0,0,903,898,1,0,0,0,903,899,1,0,0,0,903, - 901,1,0,0,0,904,906,1,0,0,0,905,814,1,0,0,0,905,821,1,0,0,0,905, - 828,1,0,0,0,905,856,1,0,0,0,905,859,1,0,0,0,905,862,1,0,0,0,905, - 865,1,0,0,0,905,874,1,0,0,0,905,880,1,0,0,0,905,885,1,0,0,0,905, - 888,1,0,0,0,905,891,1,0,0,0,905,897,1,0,0,0,906,909,1,0,0,0,907, - 905,1,0,0,0,907,908,1,0,0,0,908,107,1,0,0,0,909,907,1,0,0,0,910, - 915,3,110,55,0,911,912,5,112,0,0,912,914,3,110,55,0,913,911,1,0, - 0,0,914,917,1,0,0,0,915,913,1,0,0,0,915,916,1,0,0,0,916,109,1,0, - 0,0,917,915,1,0,0,0,918,921,3,112,56,0,919,921,3,106,53,0,920,918, - 1,0,0,0,920,919,1,0,0,0,921,111,1,0,0,0,922,923,5,126,0,0,923,928, - 3,150,75,0,924,925,5,112,0,0,925,927,3,150,75,0,926,924,1,0,0,0, - 927,930,1,0,0,0,928,926,1,0,0,0,928,929,1,0,0,0,929,931,1,0,0,0, - 930,928,1,0,0,0,931,932,5,144,0,0,932,942,1,0,0,0,933,938,3,150, - 75,0,934,935,5,112,0,0,935,937,3,150,75,0,936,934,1,0,0,0,937,940, - 1,0,0,0,938,936,1,0,0,0,938,939,1,0,0,0,939,942,1,0,0,0,940,938, - 1,0,0,0,941,922,1,0,0,0,941,933,1,0,0,0,942,943,1,0,0,0,943,944, - 5,107,0,0,944,945,3,106,53,0,945,113,1,0,0,0,946,947,5,128,0,0,947, - 951,3,150,75,0,948,950,3,116,58,0,949,948,1,0,0,0,950,953,1,0,0, - 0,951,949,1,0,0,0,951,952,1,0,0,0,952,954,1,0,0,0,953,951,1,0,0, - 0,954,955,5,146,0,0,955,956,5,120,0,0,956,975,1,0,0,0,957,958,5, - 128,0,0,958,962,3,150,75,0,959,961,3,116,58,0,960,959,1,0,0,0,961, - 964,1,0,0,0,962,960,1,0,0,0,962,963,1,0,0,0,963,965,1,0,0,0,964, - 962,1,0,0,0,965,967,5,120,0,0,966,968,3,114,57,0,967,966,1,0,0,0, - 967,968,1,0,0,0,968,969,1,0,0,0,969,970,5,128,0,0,970,971,5,146, - 0,0,971,972,3,150,75,0,972,973,5,120,0,0,973,975,1,0,0,0,974,946, - 1,0,0,0,974,957,1,0,0,0,975,115,1,0,0,0,976,977,3,150,75,0,977,978, - 5,118,0,0,978,979,3,156,78,0,979,988,1,0,0,0,980,981,3,150,75,0, - 981,982,5,118,0,0,982,983,5,124,0,0,983,984,3,106,53,0,984,985,5, - 142,0,0,985,988,1,0,0,0,986,988,3,150,75,0,987,976,1,0,0,0,987,980, - 1,0,0,0,987,986,1,0,0,0,988,117,1,0,0,0,989,994,3,120,60,0,990,991, - 5,112,0,0,991,993,3,120,60,0,992,990,1,0,0,0,993,996,1,0,0,0,994, - 992,1,0,0,0,994,995,1,0,0,0,995,119,1,0,0,0,996,994,1,0,0,0,997, - 998,3,150,75,0,998,999,5,6,0,0,999,1000,5,126,0,0,1000,1001,3,34, - 17,0,1001,1002,5,144,0,0,1002,1008,1,0,0,0,1003,1004,3,106,53,0, - 1004,1005,5,6,0,0,1005,1006,3,150,75,0,1006,1008,1,0,0,0,1007,997, - 1,0,0,0,1007,1003,1,0,0,0,1008,121,1,0,0,0,1009,1017,3,154,77,0, - 1010,1011,3,130,65,0,1011,1012,5,116,0,0,1012,1014,1,0,0,0,1013, - 1010,1,0,0,0,1013,1014,1,0,0,0,1014,1015,1,0,0,0,1015,1017,3,124, - 62,0,1016,1009,1,0,0,0,1016,1013,1,0,0,0,1017,123,1,0,0,0,1018,1023, - 3,150,75,0,1019,1020,5,116,0,0,1020,1022,3,150,75,0,1021,1019,1, - 0,0,0,1022,1025,1,0,0,0,1023,1021,1,0,0,0,1023,1024,1,0,0,0,1024, - 125,1,0,0,0,1025,1023,1,0,0,0,1026,1027,6,63,-1,0,1027,1036,3,130, - 65,0,1028,1036,3,128,64,0,1029,1030,5,126,0,0,1030,1031,3,34,17, - 0,1031,1032,5,144,0,0,1032,1036,1,0,0,0,1033,1036,3,114,57,0,1034, - 1036,3,154,77,0,1035,1026,1,0,0,0,1035,1028,1,0,0,0,1035,1029,1, - 0,0,0,1035,1033,1,0,0,0,1035,1034,1,0,0,0,1036,1045,1,0,0,0,1037, - 1041,10,3,0,0,1038,1042,3,148,74,0,1039,1040,5,6,0,0,1040,1042,3, - 150,75,0,1041,1038,1,0,0,0,1041,1039,1,0,0,0,1042,1044,1,0,0,0,1043, - 1037,1,0,0,0,1044,1047,1,0,0,0,1045,1043,1,0,0,0,1045,1046,1,0,0, - 0,1046,127,1,0,0,0,1047,1045,1,0,0,0,1048,1049,3,150,75,0,1049,1051, - 5,126,0,0,1050,1052,3,132,66,0,1051,1050,1,0,0,0,1051,1052,1,0,0, - 0,1052,1053,1,0,0,0,1053,1054,5,144,0,0,1054,129,1,0,0,0,1055,1056, - 3,134,67,0,1056,1057,5,116,0,0,1057,1059,1,0,0,0,1058,1055,1,0,0, - 0,1058,1059,1,0,0,0,1059,1060,1,0,0,0,1060,1061,3,150,75,0,1061, - 131,1,0,0,0,1062,1067,3,106,53,0,1063,1064,5,112,0,0,1064,1066,3, - 106,53,0,1065,1063,1,0,0,0,1066,1069,1,0,0,0,1067,1065,1,0,0,0,1067, - 1068,1,0,0,0,1068,133,1,0,0,0,1069,1067,1,0,0,0,1070,1071,3,150, - 75,0,1071,135,1,0,0,0,1072,1081,5,102,0,0,1073,1074,5,116,0,0,1074, - 1081,7,11,0,0,1075,1076,5,104,0,0,1076,1078,5,116,0,0,1077,1079, - 7,11,0,0,1078,1077,1,0,0,0,1078,1079,1,0,0,0,1079,1081,1,0,0,0,1080, - 1072,1,0,0,0,1080,1073,1,0,0,0,1080,1075,1,0,0,0,1081,137,1,0,0, - 0,1082,1084,7,12,0,0,1083,1082,1,0,0,0,1083,1084,1,0,0,0,1084,1091, - 1,0,0,0,1085,1092,3,136,68,0,1086,1092,5,103,0,0,1087,1092,5,104, - 0,0,1088,1092,5,105,0,0,1089,1092,5,41,0,0,1090,1092,5,55,0,0,1091, - 1085,1,0,0,0,1091,1086,1,0,0,0,1091,1087,1,0,0,0,1091,1088,1,0,0, - 0,1091,1089,1,0,0,0,1091,1090,1,0,0,0,1092,139,1,0,0,0,1093,1097, - 3,138,69,0,1094,1097,5,106,0,0,1095,1097,5,57,0,0,1096,1093,1,0, - 0,0,1096,1094,1,0,0,0,1096,1095,1,0,0,0,1097,141,1,0,0,0,1098,1099, - 7,13,0,0,1099,143,1,0,0,0,1100,1101,7,14,0,0,1101,145,1,0,0,0,1102, - 1103,7,15,0,0,1103,147,1,0,0,0,1104,1107,5,101,0,0,1105,1107,3,146, - 73,0,1106,1104,1,0,0,0,1106,1105,1,0,0,0,1107,149,1,0,0,0,1108,1112, - 5,101,0,0,1109,1112,3,142,71,0,1110,1112,3,144,72,0,1111,1108,1, - 0,0,0,1111,1109,1,0,0,0,1111,1110,1,0,0,0,1112,151,1,0,0,0,1113, - 1114,3,156,78,0,1114,1115,5,118,0,0,1115,1116,3,138,69,0,1116,153, - 1,0,0,0,1117,1118,5,124,0,0,1118,1119,3,150,75,0,1119,1120,5,142, - 0,0,1120,155,1,0,0,0,1121,1124,5,106,0,0,1122,1124,3,158,79,0,1123, - 1121,1,0,0,0,1123,1122,1,0,0,0,1124,157,1,0,0,0,1125,1129,5,137, - 0,0,1126,1128,3,160,80,0,1127,1126,1,0,0,0,1128,1131,1,0,0,0,1129, - 1127,1,0,0,0,1129,1130,1,0,0,0,1130,1132,1,0,0,0,1131,1129,1,0,0, - 0,1132,1133,5,139,0,0,1133,159,1,0,0,0,1134,1135,5,152,0,0,1135, - 1136,3,106,53,0,1136,1137,5,142,0,0,1137,1140,1,0,0,0,1138,1140, - 5,151,0,0,1139,1134,1,0,0,0,1139,1138,1,0,0,0,1140,161,1,0,0,0,1141, - 1145,5,138,0,0,1142,1144,3,164,82,0,1143,1142,1,0,0,0,1144,1147, - 1,0,0,0,1145,1143,1,0,0,0,1145,1146,1,0,0,0,1146,1148,1,0,0,0,1147, - 1145,1,0,0,0,1148,1149,5,0,0,1,1149,163,1,0,0,0,1150,1151,5,154, - 0,0,1151,1152,3,106,53,0,1152,1153,5,142,0,0,1153,1156,1,0,0,0,1154, - 1156,5,153,0,0,1155,1150,1,0,0,0,1155,1154,1,0,0,0,1156,165,1,0, - 0,0,135,169,176,185,200,212,224,240,251,265,271,281,290,293,297, + 1,0,0,0,742,743,5,144,0,0,743,752,1,0,0,0,744,746,5,126,0,0,745, + 747,5,23,0,0,746,745,1,0,0,0,746,747,1,0,0,0,747,749,1,0,0,0,748, + 750,3,108,54,0,749,748,1,0,0,0,749,750,1,0,0,0,750,751,1,0,0,0,751, + 753,5,144,0,0,752,744,1,0,0,0,752,753,1,0,0,0,753,754,1,0,0,0,754, + 755,5,64,0,0,755,756,5,126,0,0,756,757,3,88,44,0,757,758,5,144,0, + 0,758,833,1,0,0,0,759,760,3,150,75,0,760,762,5,126,0,0,761,763,3, + 104,52,0,762,761,1,0,0,0,762,763,1,0,0,0,763,764,1,0,0,0,764,765, + 5,144,0,0,765,774,1,0,0,0,766,768,5,126,0,0,767,769,5,23,0,0,768, + 767,1,0,0,0,768,769,1,0,0,0,769,771,1,0,0,0,770,772,3,108,54,0,771, + 770,1,0,0,0,771,772,1,0,0,0,772,773,1,0,0,0,773,775,5,144,0,0,774, + 766,1,0,0,0,774,775,1,0,0,0,775,776,1,0,0,0,776,777,5,64,0,0,777, + 778,3,150,75,0,778,833,1,0,0,0,779,785,3,150,75,0,780,782,5,126, + 0,0,781,783,3,104,52,0,782,781,1,0,0,0,782,783,1,0,0,0,783,784,1, + 0,0,0,784,786,5,144,0,0,785,780,1,0,0,0,785,786,1,0,0,0,786,787, + 1,0,0,0,787,789,5,126,0,0,788,790,5,23,0,0,789,788,1,0,0,0,789,790, + 1,0,0,0,790,792,1,0,0,0,791,793,3,108,54,0,792,791,1,0,0,0,792,793, + 1,0,0,0,793,794,1,0,0,0,794,795,5,144,0,0,795,833,1,0,0,0,796,833, + 3,114,57,0,797,833,3,158,79,0,798,833,3,140,70,0,799,800,5,114,0, + 0,800,833,3,106,53,19,801,802,5,56,0,0,802,833,3,106,53,13,803,804, + 3,130,65,0,804,805,5,116,0,0,805,807,1,0,0,0,806,803,1,0,0,0,806, + 807,1,0,0,0,807,808,1,0,0,0,808,833,5,108,0,0,809,810,5,126,0,0, + 810,811,3,34,17,0,811,812,5,144,0,0,812,833,1,0,0,0,813,814,5,126, + 0,0,814,815,3,106,53,0,815,816,5,144,0,0,816,833,1,0,0,0,817,818, + 5,126,0,0,818,819,3,104,52,0,819,820,5,144,0,0,820,833,1,0,0,0,821, + 823,5,125,0,0,822,824,3,104,52,0,823,822,1,0,0,0,823,824,1,0,0,0, + 824,825,1,0,0,0,825,833,5,143,0,0,826,828,5,124,0,0,827,829,3,30, + 15,0,828,827,1,0,0,0,828,829,1,0,0,0,829,830,1,0,0,0,830,833,5,142, + 0,0,831,833,3,122,61,0,832,683,1,0,0,0,832,703,1,0,0,0,832,710,1, + 0,0,0,832,712,1,0,0,0,832,716,1,0,0,0,832,727,1,0,0,0,832,729,1, + 0,0,0,832,737,1,0,0,0,832,759,1,0,0,0,832,779,1,0,0,0,832,796,1, + 0,0,0,832,797,1,0,0,0,832,798,1,0,0,0,832,799,1,0,0,0,832,801,1, + 0,0,0,832,806,1,0,0,0,832,809,1,0,0,0,832,813,1,0,0,0,832,817,1, + 0,0,0,832,821,1,0,0,0,832,826,1,0,0,0,832,831,1,0,0,0,833,927,1, + 0,0,0,834,838,10,18,0,0,835,839,5,108,0,0,836,839,5,146,0,0,837, + 839,5,133,0,0,838,835,1,0,0,0,838,836,1,0,0,0,838,837,1,0,0,0,839, + 840,1,0,0,0,840,926,3,106,53,19,841,845,10,17,0,0,842,846,5,134, + 0,0,843,846,5,114,0,0,844,846,5,113,0,0,845,842,1,0,0,0,845,843, + 1,0,0,0,845,844,1,0,0,0,846,847,1,0,0,0,847,926,3,106,53,18,848, + 873,10,16,0,0,849,874,5,117,0,0,850,874,5,118,0,0,851,874,5,129, + 0,0,852,874,5,127,0,0,853,874,5,128,0,0,854,874,5,119,0,0,855,874, + 5,120,0,0,856,858,5,56,0,0,857,856,1,0,0,0,857,858,1,0,0,0,858,859, + 1,0,0,0,859,861,5,40,0,0,860,862,5,14,0,0,861,860,1,0,0,0,861,862, + 1,0,0,0,862,874,1,0,0,0,863,865,5,56,0,0,864,863,1,0,0,0,864,865, + 1,0,0,0,865,866,1,0,0,0,866,874,7,10,0,0,867,874,5,140,0,0,868,874, + 5,141,0,0,869,874,5,131,0,0,870,874,5,122,0,0,871,874,5,123,0,0, + 872,874,5,130,0,0,873,849,1,0,0,0,873,850,1,0,0,0,873,851,1,0,0, + 0,873,852,1,0,0,0,873,853,1,0,0,0,873,854,1,0,0,0,873,855,1,0,0, + 0,873,857,1,0,0,0,873,864,1,0,0,0,873,867,1,0,0,0,873,868,1,0,0, + 0,873,869,1,0,0,0,873,870,1,0,0,0,873,871,1,0,0,0,873,872,1,0,0, + 0,874,875,1,0,0,0,875,926,3,106,53,17,876,877,10,14,0,0,877,878, + 5,132,0,0,878,926,3,106,53,15,879,880,10,12,0,0,880,881,5,2,0,0, + 881,926,3,106,53,13,882,883,10,11,0,0,883,884,5,61,0,0,884,926,3, + 106,53,12,885,887,10,10,0,0,886,888,5,56,0,0,887,886,1,0,0,0,887, + 888,1,0,0,0,888,889,1,0,0,0,889,890,5,9,0,0,890,891,3,106,53,0,891, + 892,5,2,0,0,892,893,3,106,53,11,893,926,1,0,0,0,894,895,10,9,0,0, + 895,896,5,135,0,0,896,897,3,106,53,0,897,898,5,111,0,0,898,899,3, + 106,53,9,899,926,1,0,0,0,900,901,10,22,0,0,901,902,5,125,0,0,902, + 903,3,106,53,0,903,904,5,143,0,0,904,926,1,0,0,0,905,906,10,21,0, + 0,906,907,5,116,0,0,907,926,5,104,0,0,908,909,10,20,0,0,909,910, + 5,116,0,0,910,926,3,150,75,0,911,912,10,15,0,0,912,914,5,44,0,0, + 913,915,5,56,0,0,914,913,1,0,0,0,914,915,1,0,0,0,915,916,1,0,0,0, + 916,926,5,57,0,0,917,923,10,8,0,0,918,924,3,148,74,0,919,920,5,6, + 0,0,920,924,3,150,75,0,921,922,5,6,0,0,922,924,5,106,0,0,923,918, + 1,0,0,0,923,919,1,0,0,0,923,921,1,0,0,0,924,926,1,0,0,0,925,834, + 1,0,0,0,925,841,1,0,0,0,925,848,1,0,0,0,925,876,1,0,0,0,925,879, + 1,0,0,0,925,882,1,0,0,0,925,885,1,0,0,0,925,894,1,0,0,0,925,900, + 1,0,0,0,925,905,1,0,0,0,925,908,1,0,0,0,925,911,1,0,0,0,925,917, + 1,0,0,0,926,929,1,0,0,0,927,925,1,0,0,0,927,928,1,0,0,0,928,107, + 1,0,0,0,929,927,1,0,0,0,930,935,3,110,55,0,931,932,5,112,0,0,932, + 934,3,110,55,0,933,931,1,0,0,0,934,937,1,0,0,0,935,933,1,0,0,0,935, + 936,1,0,0,0,936,109,1,0,0,0,937,935,1,0,0,0,938,941,3,112,56,0,939, + 941,3,106,53,0,940,938,1,0,0,0,940,939,1,0,0,0,941,111,1,0,0,0,942, + 943,5,126,0,0,943,948,3,150,75,0,944,945,5,112,0,0,945,947,3,150, + 75,0,946,944,1,0,0,0,947,950,1,0,0,0,948,946,1,0,0,0,948,949,1,0, + 0,0,949,951,1,0,0,0,950,948,1,0,0,0,951,952,5,144,0,0,952,962,1, + 0,0,0,953,958,3,150,75,0,954,955,5,112,0,0,955,957,3,150,75,0,956, + 954,1,0,0,0,957,960,1,0,0,0,958,956,1,0,0,0,958,959,1,0,0,0,959, + 962,1,0,0,0,960,958,1,0,0,0,961,942,1,0,0,0,961,953,1,0,0,0,962, + 963,1,0,0,0,963,964,5,107,0,0,964,965,3,106,53,0,965,113,1,0,0,0, + 966,967,5,128,0,0,967,971,3,150,75,0,968,970,3,116,58,0,969,968, + 1,0,0,0,970,973,1,0,0,0,971,969,1,0,0,0,971,972,1,0,0,0,972,974, + 1,0,0,0,973,971,1,0,0,0,974,975,5,146,0,0,975,976,5,120,0,0,976, + 995,1,0,0,0,977,978,5,128,0,0,978,982,3,150,75,0,979,981,3,116,58, + 0,980,979,1,0,0,0,981,984,1,0,0,0,982,980,1,0,0,0,982,983,1,0,0, + 0,983,985,1,0,0,0,984,982,1,0,0,0,985,987,5,120,0,0,986,988,3,114, + 57,0,987,986,1,0,0,0,987,988,1,0,0,0,988,989,1,0,0,0,989,990,5,128, + 0,0,990,991,5,146,0,0,991,992,3,150,75,0,992,993,5,120,0,0,993,995, + 1,0,0,0,994,966,1,0,0,0,994,977,1,0,0,0,995,115,1,0,0,0,996,997, + 3,150,75,0,997,998,5,118,0,0,998,999,3,156,78,0,999,1008,1,0,0,0, + 1000,1001,3,150,75,0,1001,1002,5,118,0,0,1002,1003,5,124,0,0,1003, + 1004,3,106,53,0,1004,1005,5,142,0,0,1005,1008,1,0,0,0,1006,1008, + 3,150,75,0,1007,996,1,0,0,0,1007,1000,1,0,0,0,1007,1006,1,0,0,0, + 1008,117,1,0,0,0,1009,1014,3,120,60,0,1010,1011,5,112,0,0,1011,1013, + 3,120,60,0,1012,1010,1,0,0,0,1013,1016,1,0,0,0,1014,1012,1,0,0,0, + 1014,1015,1,0,0,0,1015,119,1,0,0,0,1016,1014,1,0,0,0,1017,1018,3, + 150,75,0,1018,1019,5,6,0,0,1019,1020,5,126,0,0,1020,1021,3,34,17, + 0,1021,1022,5,144,0,0,1022,1028,1,0,0,0,1023,1024,3,106,53,0,1024, + 1025,5,6,0,0,1025,1026,3,150,75,0,1026,1028,1,0,0,0,1027,1017,1, + 0,0,0,1027,1023,1,0,0,0,1028,121,1,0,0,0,1029,1037,3,154,77,0,1030, + 1031,3,130,65,0,1031,1032,5,116,0,0,1032,1034,1,0,0,0,1033,1030, + 1,0,0,0,1033,1034,1,0,0,0,1034,1035,1,0,0,0,1035,1037,3,124,62,0, + 1036,1029,1,0,0,0,1036,1033,1,0,0,0,1037,123,1,0,0,0,1038,1043,3, + 150,75,0,1039,1040,5,116,0,0,1040,1042,3,150,75,0,1041,1039,1,0, + 0,0,1042,1045,1,0,0,0,1043,1041,1,0,0,0,1043,1044,1,0,0,0,1044,125, + 1,0,0,0,1045,1043,1,0,0,0,1046,1047,6,63,-1,0,1047,1056,3,130,65, + 0,1048,1056,3,128,64,0,1049,1050,5,126,0,0,1050,1051,3,34,17,0,1051, + 1052,5,144,0,0,1052,1056,1,0,0,0,1053,1056,3,114,57,0,1054,1056, + 3,154,77,0,1055,1046,1,0,0,0,1055,1048,1,0,0,0,1055,1049,1,0,0,0, + 1055,1053,1,0,0,0,1055,1054,1,0,0,0,1056,1065,1,0,0,0,1057,1061, + 10,3,0,0,1058,1062,3,148,74,0,1059,1060,5,6,0,0,1060,1062,3,150, + 75,0,1061,1058,1,0,0,0,1061,1059,1,0,0,0,1062,1064,1,0,0,0,1063, + 1057,1,0,0,0,1064,1067,1,0,0,0,1065,1063,1,0,0,0,1065,1066,1,0,0, + 0,1066,127,1,0,0,0,1067,1065,1,0,0,0,1068,1069,3,150,75,0,1069,1071, + 5,126,0,0,1070,1072,3,132,66,0,1071,1070,1,0,0,0,1071,1072,1,0,0, + 0,1072,1073,1,0,0,0,1073,1074,5,144,0,0,1074,129,1,0,0,0,1075,1076, + 3,134,67,0,1076,1077,5,116,0,0,1077,1079,1,0,0,0,1078,1075,1,0,0, + 0,1078,1079,1,0,0,0,1079,1080,1,0,0,0,1080,1081,3,150,75,0,1081, + 131,1,0,0,0,1082,1087,3,106,53,0,1083,1084,5,112,0,0,1084,1086,3, + 106,53,0,1085,1083,1,0,0,0,1086,1089,1,0,0,0,1087,1085,1,0,0,0,1087, + 1088,1,0,0,0,1088,133,1,0,0,0,1089,1087,1,0,0,0,1090,1091,3,150, + 75,0,1091,135,1,0,0,0,1092,1101,5,102,0,0,1093,1094,5,116,0,0,1094, + 1101,7,11,0,0,1095,1096,5,104,0,0,1096,1098,5,116,0,0,1097,1099, + 7,11,0,0,1098,1097,1,0,0,0,1098,1099,1,0,0,0,1099,1101,1,0,0,0,1100, + 1092,1,0,0,0,1100,1093,1,0,0,0,1100,1095,1,0,0,0,1101,137,1,0,0, + 0,1102,1104,7,12,0,0,1103,1102,1,0,0,0,1103,1104,1,0,0,0,1104,1111, + 1,0,0,0,1105,1112,3,136,68,0,1106,1112,5,103,0,0,1107,1112,5,104, + 0,0,1108,1112,5,105,0,0,1109,1112,5,41,0,0,1110,1112,5,55,0,0,1111, + 1105,1,0,0,0,1111,1106,1,0,0,0,1111,1107,1,0,0,0,1111,1108,1,0,0, + 0,1111,1109,1,0,0,0,1111,1110,1,0,0,0,1112,139,1,0,0,0,1113,1117, + 3,138,69,0,1114,1117,5,106,0,0,1115,1117,5,57,0,0,1116,1113,1,0, + 0,0,1116,1114,1,0,0,0,1116,1115,1,0,0,0,1117,141,1,0,0,0,1118,1119, + 7,13,0,0,1119,143,1,0,0,0,1120,1121,7,14,0,0,1121,145,1,0,0,0,1122, + 1123,7,15,0,0,1123,147,1,0,0,0,1124,1127,5,101,0,0,1125,1127,3,146, + 73,0,1126,1124,1,0,0,0,1126,1125,1,0,0,0,1127,149,1,0,0,0,1128,1132, + 5,101,0,0,1129,1132,3,142,71,0,1130,1132,3,144,72,0,1131,1128,1, + 0,0,0,1131,1129,1,0,0,0,1131,1130,1,0,0,0,1132,151,1,0,0,0,1133, + 1134,3,156,78,0,1134,1135,5,118,0,0,1135,1136,3,138,69,0,1136,153, + 1,0,0,0,1137,1138,5,124,0,0,1138,1139,3,150,75,0,1139,1140,5,142, + 0,0,1140,155,1,0,0,0,1141,1144,5,106,0,0,1142,1144,3,158,79,0,1143, + 1141,1,0,0,0,1143,1142,1,0,0,0,1144,157,1,0,0,0,1145,1149,5,137, + 0,0,1146,1148,3,160,80,0,1147,1146,1,0,0,0,1148,1151,1,0,0,0,1149, + 1147,1,0,0,0,1149,1150,1,0,0,0,1150,1152,1,0,0,0,1151,1149,1,0,0, + 0,1152,1153,5,139,0,0,1153,159,1,0,0,0,1154,1155,5,152,0,0,1155, + 1156,3,106,53,0,1156,1157,5,142,0,0,1157,1160,1,0,0,0,1158,1160, + 5,151,0,0,1159,1154,1,0,0,0,1159,1158,1,0,0,0,1160,161,1,0,0,0,1161, + 1165,5,138,0,0,1162,1164,3,164,82,0,1163,1162,1,0,0,0,1164,1167, + 1,0,0,0,1165,1163,1,0,0,0,1165,1166,1,0,0,0,1166,1168,1,0,0,0,1167, + 1165,1,0,0,0,1168,1169,5,0,0,1,1169,163,1,0,0,0,1170,1171,5,154, + 0,0,1171,1172,3,106,53,0,1172,1173,5,142,0,0,1173,1176,1,0,0,0,1174, + 1176,5,153,0,0,1175,1170,1,0,0,0,1175,1174,1,0,0,0,1176,165,1,0, + 0,0,141,169,176,185,200,212,224,240,251,265,271,281,290,293,297, 300,304,307,310,313,316,320,324,327,330,333,337,340,349,355,376, 393,410,416,422,433,435,446,449,455,463,469,471,475,480,483,486, 490,494,497,499,502,506,510,513,515,517,522,533,539,546,551,555, 559,565,567,574,582,585,588,607,621,637,649,661,669,673,680,686, - 695,699,723,740,752,762,765,769,772,786,803,808,812,818,825,837, - 841,844,853,867,894,903,905,907,915,920,928,938,941,951,962,967, - 974,987,994,1007,1013,1016,1023,1035,1041,1045,1051,1058,1067,1078, - 1080,1083,1091,1096,1106,1111,1123,1129,1139,1145,1155 + 695,699,723,740,746,749,752,762,768,771,774,782,785,789,792,806, + 823,828,832,838,845,857,861,864,873,887,914,923,925,927,935,940, + 948,958,961,971,982,987,994,1007,1014,1027,1033,1036,1043,1055,1061, + 1065,1071,1078,1087,1098,1100,1103,1111,1116,1126,1131,1143,1149, + 1159,1165,1175 ] class HogQLParser ( Parser ): @@ -5467,13 +5477,24 @@ def identifier(self, i:int=None): def OVER(self): return self.getToken(HogQLParser.OVER, 0) - def LPAREN(self): - return self.getToken(HogQLParser.LPAREN, 0) - def RPAREN(self): - return self.getToken(HogQLParser.RPAREN, 0) + def LPAREN(self, i:int=None): + if i is None: + return self.getTokens(HogQLParser.LPAREN) + else: + return self.getToken(HogQLParser.LPAREN, i) + def RPAREN(self, i:int=None): + if i is None: + return self.getTokens(HogQLParser.RPAREN) + else: + return self.getToken(HogQLParser.RPAREN, i) def columnExprList(self): return self.getTypedRuleContext(HogQLParser.ColumnExprListContext,0) + def DISTINCT(self): + return self.getToken(HogQLParser.DISTINCT, 0) + def columnArgList(self): + return self.getTypedRuleContext(HogQLParser.ColumnArgListContext,0) + def accept(self, visitor:ParseTreeVisitor): if hasattr( visitor, "visitColumnExprWinFunctionTarget" ): @@ -5851,6 +5872,11 @@ def RPAREN(self, i:int=None): def columnExprList(self): return self.getTypedRuleContext(HogQLParser.ColumnExprListContext,0) + def DISTINCT(self): + return self.getToken(HogQLParser.DISTINCT, 0) + def columnArgList(self): + return self.getTypedRuleContext(HogQLParser.ColumnArgListContext,0) + def accept(self, visitor:ParseTreeVisitor): if hasattr( visitor, "visitColumnExprWinFunction" ): @@ -5943,9 +5969,9 @@ def columnExpr(self, _p:int=0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 812 + self.state = 832 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,90,self._ctx) + la_ = self._interp.adaptivePredict(self._input,96,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnExprCaseContext(self, localctx) self._ctx = localctx @@ -6115,13 +6141,39 @@ def columnExpr(self, _p:int=0): self.state = 742 self.match(HogQLParser.RPAREN) - self.state = 744 + self.state = 752 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==126: + self.state = 744 + self.match(HogQLParser.LPAREN) + self.state = 746 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,82,self._ctx) + if la_ == 1: + self.state = 745 + self.match(HogQLParser.DISTINCT) + + + self.state = 749 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): + self.state = 748 + self.columnArgList() + + + self.state = 751 + self.match(HogQLParser.RPAREN) + + + self.state = 754 self.match(HogQLParser.OVER) - self.state = 745 + self.state = 755 self.match(HogQLParser.LPAREN) - self.state = 746 + self.state = 756 self.windowExpr() - self.state = 747 + self.state = 757 self.match(HogQLParser.RPAREN) pass @@ -6129,24 +6181,50 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprWinFunctionTargetContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 749 + self.state = 759 self.identifier() - self.state = 750 + self.state = 760 self.match(HogQLParser.LPAREN) - self.state = 752 + self.state = 762 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 751 + self.state = 761 self.columnExprList() - self.state = 754 + self.state = 764 self.match(HogQLParser.RPAREN) - self.state = 756 + self.state = 774 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==126: + self.state = 766 + self.match(HogQLParser.LPAREN) + self.state = 768 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,86,self._ctx) + if la_ == 1: + self.state = 767 + self.match(HogQLParser.DISTINCT) + + + self.state = 771 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): + self.state = 770 + self.columnArgList() + + + self.state = 773 + self.match(HogQLParser.RPAREN) + + + self.state = 776 self.match(HogQLParser.OVER) - self.state = 757 + self.state = 777 self.identifier() pass @@ -6154,45 +6232,45 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 759 + self.state = 779 self.identifier() - self.state = 765 + self.state = 785 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,84,self._ctx) + la_ = self._interp.adaptivePredict(self._input,90,self._ctx) if la_ == 1: - self.state = 760 + self.state = 780 self.match(HogQLParser.LPAREN) - self.state = 762 + self.state = 782 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 761 + self.state = 781 self.columnExprList() - self.state = 764 + self.state = 784 self.match(HogQLParser.RPAREN) - self.state = 767 + self.state = 787 self.match(HogQLParser.LPAREN) - self.state = 769 + self.state = 789 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,85,self._ctx) + la_ = self._interp.adaptivePredict(self._input,91,self._ctx) if la_ == 1: - self.state = 768 + self.state = 788 self.match(HogQLParser.DISTINCT) - self.state = 772 + self.state = 792 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 771 + self.state = 791 self.columnArgList() - self.state = 774 + self.state = 794 self.match(HogQLParser.RPAREN) pass @@ -6200,7 +6278,7 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTagElementContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 776 + self.state = 796 self.hogqlxTagElement() pass @@ -6208,7 +6286,7 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTemplateStringContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 777 + self.state = 797 self.templateString() pass @@ -6216,7 +6294,7 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprLiteralContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 778 + self.state = 798 self.literal() pass @@ -6224,9 +6302,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprNegateContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 779 + self.state = 799 self.match(HogQLParser.DASH) - self.state = 780 + self.state = 800 self.columnExpr(19) pass @@ -6234,9 +6312,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprNotContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 781 + self.state = 801 self.match(HogQLParser.NOT) - self.state = 782 + self.state = 802 self.columnExpr(13) pass @@ -6244,17 +6322,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprAsteriskContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 786 + self.state = 806 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): - self.state = 783 + self.state = 803 self.tableIdentifier() - self.state = 784 + self.state = 804 self.match(HogQLParser.DOT) - self.state = 788 + self.state = 808 self.match(HogQLParser.ASTERISK) pass @@ -6262,11 +6340,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprSubqueryContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 789 + self.state = 809 self.match(HogQLParser.LPAREN) - self.state = 790 + self.state = 810 self.selectUnionStmt() - self.state = 791 + self.state = 811 self.match(HogQLParser.RPAREN) pass @@ -6274,11 +6352,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprParensContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 793 + self.state = 813 self.match(HogQLParser.LPAREN) - self.state = 794 + self.state = 814 self.columnExpr(0) - self.state = 795 + self.state = 815 self.match(HogQLParser.RPAREN) pass @@ -6286,11 +6364,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTupleContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 797 + self.state = 817 self.match(HogQLParser.LPAREN) - self.state = 798 + self.state = 818 self.columnExprList() - self.state = 799 + self.state = 819 self.match(HogQLParser.RPAREN) pass @@ -6298,17 +6376,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprArrayContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 801 + self.state = 821 self.match(HogQLParser.LBRACKET) - self.state = 803 + self.state = 823 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 802 + self.state = 822 self.columnExprList() - self.state = 805 + self.state = 825 self.match(HogQLParser.RBRACKET) pass @@ -6316,17 +6394,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprDictContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 806 + self.state = 826 self.match(HogQLParser.LBRACE) - self.state = 808 + self.state = 828 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 807 + self.state = 827 self.kvPairList() - self.state = 810 + self.state = 830 self.match(HogQLParser.RBRACE) pass @@ -6334,50 +6412,50 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprIdentifierContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 811 + self.state = 831 self.columnIdentifier() pass self._ctx.stop = self._input.LT(-1) - self.state = 907 + self.state = 927 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,101,self._ctx) + _alt = self._interp.adaptivePredict(self._input,107,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 905 + self.state = 925 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,100,self._ctx) + la_ = self._interp.adaptivePredict(self._input,106,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnExprPrecedence1Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 814 + self.state = 834 if not self.precpred(self._ctx, 18): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 18)") - self.state = 818 + self.state = 838 self._errHandler.sync(self) token = self._input.LA(1) if token in [108]: - self.state = 815 + self.state = 835 localctx.operator = self.match(HogQLParser.ASTERISK) pass elif token in [146]: - self.state = 816 + self.state = 836 localctx.operator = self.match(HogQLParser.SLASH) pass elif token in [133]: - self.state = 817 + self.state = 837 localctx.operator = self.match(HogQLParser.PERCENT) pass else: raise NoViableAltException(self) - self.state = 820 + self.state = 840 localctx.right = self.columnExpr(19) pass @@ -6385,29 +6463,29 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprPrecedence2Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 821 + self.state = 841 if not self.precpred(self._ctx, 17): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 17)") - self.state = 825 + self.state = 845 self._errHandler.sync(self) token = self._input.LA(1) if token in [134]: - self.state = 822 + self.state = 842 localctx.operator = self.match(HogQLParser.PLUS) pass elif token in [114]: - self.state = 823 + self.state = 843 localctx.operator = self.match(HogQLParser.DASH) pass elif token in [113]: - self.state = 824 + self.state = 844 localctx.operator = self.match(HogQLParser.CONCAT) pass else: raise NoViableAltException(self) - self.state = 827 + self.state = 847 localctx.right = self.columnExpr(18) pass @@ -6415,79 +6493,79 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprPrecedence3Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 828 + self.state = 848 if not self.precpred(self._ctx, 16): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 16)") - self.state = 853 + self.state = 873 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,96,self._ctx) + la_ = self._interp.adaptivePredict(self._input,102,self._ctx) if la_ == 1: - self.state = 829 + self.state = 849 localctx.operator = self.match(HogQLParser.EQ_DOUBLE) pass elif la_ == 2: - self.state = 830 + self.state = 850 localctx.operator = self.match(HogQLParser.EQ_SINGLE) pass elif la_ == 3: - self.state = 831 + self.state = 851 localctx.operator = self.match(HogQLParser.NOT_EQ) pass elif la_ == 4: - self.state = 832 + self.state = 852 localctx.operator = self.match(HogQLParser.LT_EQ) pass elif la_ == 5: - self.state = 833 + self.state = 853 localctx.operator = self.match(HogQLParser.LT) pass elif la_ == 6: - self.state = 834 + self.state = 854 localctx.operator = self.match(HogQLParser.GT_EQ) pass elif la_ == 7: - self.state = 835 + self.state = 855 localctx.operator = self.match(HogQLParser.GT) pass elif la_ == 8: - self.state = 837 + self.state = 857 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 836 + self.state = 856 localctx.operator = self.match(HogQLParser.NOT) - self.state = 839 + self.state = 859 self.match(HogQLParser.IN) - self.state = 841 + self.state = 861 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,94,self._ctx) + la_ = self._interp.adaptivePredict(self._input,100,self._ctx) if la_ == 1: - self.state = 840 + self.state = 860 self.match(HogQLParser.COHORT) pass elif la_ == 9: - self.state = 844 + self.state = 864 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 843 + self.state = 863 localctx.operator = self.match(HogQLParser.NOT) - self.state = 846 + self.state = 866 _la = self._input.LA(1) if not(_la==39 or _la==51): self._errHandler.recoverInline(self) @@ -6497,209 +6575,209 @@ def columnExpr(self, _p:int=0): pass elif la_ == 10: - self.state = 847 + self.state = 867 localctx.operator = self.match(HogQLParser.REGEX_SINGLE) pass elif la_ == 11: - self.state = 848 + self.state = 868 localctx.operator = self.match(HogQLParser.REGEX_DOUBLE) pass elif la_ == 12: - self.state = 849 + self.state = 869 localctx.operator = self.match(HogQLParser.NOT_REGEX) pass elif la_ == 13: - self.state = 850 + self.state = 870 localctx.operator = self.match(HogQLParser.IREGEX_SINGLE) pass elif la_ == 14: - self.state = 851 + self.state = 871 localctx.operator = self.match(HogQLParser.IREGEX_DOUBLE) pass elif la_ == 15: - self.state = 852 + self.state = 872 localctx.operator = self.match(HogQLParser.NOT_IREGEX) pass - self.state = 855 + self.state = 875 localctx.right = self.columnExpr(17) pass elif la_ == 4: localctx = HogQLParser.ColumnExprNullishContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 856 + self.state = 876 if not self.precpred(self._ctx, 14): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 14)") - self.state = 857 + self.state = 877 self.match(HogQLParser.NULLISH) - self.state = 858 + self.state = 878 self.columnExpr(15) pass elif la_ == 5: localctx = HogQLParser.ColumnExprAndContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 859 + self.state = 879 if not self.precpred(self._ctx, 12): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 12)") - self.state = 860 + self.state = 880 self.match(HogQLParser.AND) - self.state = 861 + self.state = 881 self.columnExpr(13) pass elif la_ == 6: localctx = HogQLParser.ColumnExprOrContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 862 + self.state = 882 if not self.precpred(self._ctx, 11): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 11)") - self.state = 863 + self.state = 883 self.match(HogQLParser.OR) - self.state = 864 + self.state = 884 self.columnExpr(12) pass elif la_ == 7: localctx = HogQLParser.ColumnExprBetweenContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 865 + self.state = 885 if not self.precpred(self._ctx, 10): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 10)") - self.state = 867 + self.state = 887 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 866 + self.state = 886 self.match(HogQLParser.NOT) - self.state = 869 + self.state = 889 self.match(HogQLParser.BETWEEN) - self.state = 870 + self.state = 890 self.columnExpr(0) - self.state = 871 + self.state = 891 self.match(HogQLParser.AND) - self.state = 872 + self.state = 892 self.columnExpr(11) pass elif la_ == 8: localctx = HogQLParser.ColumnExprTernaryOpContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 874 + self.state = 894 if not self.precpred(self._ctx, 9): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 9)") - self.state = 875 + self.state = 895 self.match(HogQLParser.QUERY) - self.state = 876 + self.state = 896 self.columnExpr(0) - self.state = 877 + self.state = 897 self.match(HogQLParser.COLON) - self.state = 878 + self.state = 898 self.columnExpr(9) pass elif la_ == 9: localctx = HogQLParser.ColumnExprArrayAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 880 + self.state = 900 if not self.precpred(self._ctx, 22): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 22)") - self.state = 881 + self.state = 901 self.match(HogQLParser.LBRACKET) - self.state = 882 + self.state = 902 self.columnExpr(0) - self.state = 883 + self.state = 903 self.match(HogQLParser.RBRACKET) pass elif la_ == 10: localctx = HogQLParser.ColumnExprTupleAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 885 + self.state = 905 if not self.precpred(self._ctx, 21): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 21)") - self.state = 886 + self.state = 906 self.match(HogQLParser.DOT) - self.state = 887 + self.state = 907 self.match(HogQLParser.DECIMAL_LITERAL) pass elif la_ == 11: localctx = HogQLParser.ColumnExprPropertyAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 888 + self.state = 908 if not self.precpred(self._ctx, 20): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 20)") - self.state = 889 + self.state = 909 self.match(HogQLParser.DOT) - self.state = 890 + self.state = 910 self.identifier() pass elif la_ == 12: localctx = HogQLParser.ColumnExprIsNullContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 891 + self.state = 911 if not self.precpred(self._ctx, 15): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 15)") - self.state = 892 + self.state = 912 self.match(HogQLParser.IS) - self.state = 894 + self.state = 914 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 893 + self.state = 913 self.match(HogQLParser.NOT) - self.state = 896 + self.state = 916 self.match(HogQLParser.NULL_SQL) pass elif la_ == 13: localctx = HogQLParser.ColumnExprAliasContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 897 + self.state = 917 if not self.precpred(self._ctx, 8): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 8)") - self.state = 903 + self.state = 923 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,99,self._ctx) + la_ = self._interp.adaptivePredict(self._input,105,self._ctx) if la_ == 1: - self.state = 898 + self.state = 918 self.alias() pass elif la_ == 2: - self.state = 899 + self.state = 919 self.match(HogQLParser.AS) - self.state = 900 + self.state = 920 self.identifier() pass elif la_ == 3: - self.state = 901 + self.state = 921 self.match(HogQLParser.AS) - self.state = 902 + self.state = 922 self.match(HogQLParser.STRING_LITERAL) pass @@ -6707,9 +6785,9 @@ def columnExpr(self, _p:int=0): pass - self.state = 909 + self.state = 929 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,101,self._ctx) + _alt = self._interp.adaptivePredict(self._input,107,self._ctx) except RecognitionException as re: localctx.exception = re @@ -6759,17 +6837,17 @@ def columnArgList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 910 + self.state = 930 self.columnArgExpr() - self.state = 915 + self.state = 935 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 911 + self.state = 931 self.match(HogQLParser.COMMA) - self.state = 912 + self.state = 932 self.columnArgExpr() - self.state = 917 + self.state = 937 self._errHandler.sync(self) _la = self._input.LA(1) @@ -6814,18 +6892,18 @@ def columnArgExpr(self): localctx = HogQLParser.ColumnArgExprContext(self, self._ctx, self.state) self.enterRule(localctx, 110, self.RULE_columnArgExpr) try: - self.state = 920 + self.state = 940 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,103,self._ctx) + la_ = self._interp.adaptivePredict(self._input,109,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 918 + self.state = 938 self.columnLambdaExpr() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 919 + self.state = 939 self.columnExpr(0) pass @@ -6891,41 +6969,41 @@ def columnLambdaExpr(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 941 + self.state = 961 self._errHandler.sync(self) token = self._input.LA(1) if token in [126]: - self.state = 922 + self.state = 942 self.match(HogQLParser.LPAREN) - self.state = 923 + self.state = 943 self.identifier() - self.state = 928 + self.state = 948 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 924 + self.state = 944 self.match(HogQLParser.COMMA) - self.state = 925 + self.state = 945 self.identifier() - self.state = 930 + self.state = 950 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 931 + self.state = 951 self.match(HogQLParser.RPAREN) pass elif token in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 97, 98, 99, 101]: - self.state = 933 + self.state = 953 self.identifier() - self.state = 938 + self.state = 958 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 934 + self.state = 954 self.match(HogQLParser.COMMA) - self.state = 935 + self.state = 955 self.identifier() - self.state = 940 + self.state = 960 self._errHandler.sync(self) _la = self._input.LA(1) @@ -6933,9 +7011,9 @@ def columnLambdaExpr(self): else: raise NoViableAltException(self) - self.state = 943 + self.state = 963 self.match(HogQLParser.ARROW) - self.state = 944 + self.state = 964 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -7040,66 +7118,66 @@ def hogqlxTagElement(self): self.enterRule(localctx, 114, self.RULE_hogqlxTagElement) self._la = 0 # Token type try: - self.state = 974 + self.state = 994 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,110,self._ctx) + la_ = self._interp.adaptivePredict(self._input,116,self._ctx) if la_ == 1: localctx = HogQLParser.HogqlxTagElementClosedContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 946 + self.state = 966 self.match(HogQLParser.LT) - self.state = 947 + self.state = 967 self.identifier() - self.state = 951 + self.state = 971 self._errHandler.sync(self) _la = self._input.LA(1) while (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): - self.state = 948 + self.state = 968 self.hogqlxTagAttribute() - self.state = 953 + self.state = 973 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 954 + self.state = 974 self.match(HogQLParser.SLASH) - self.state = 955 + self.state = 975 self.match(HogQLParser.GT) pass elif la_ == 2: localctx = HogQLParser.HogqlxTagElementNestedContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 957 + self.state = 977 self.match(HogQLParser.LT) - self.state = 958 + self.state = 978 self.identifier() - self.state = 962 + self.state = 982 self._errHandler.sync(self) _la = self._input.LA(1) while (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): - self.state = 959 + self.state = 979 self.hogqlxTagAttribute() - self.state = 964 + self.state = 984 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 965 + self.state = 985 self.match(HogQLParser.GT) - self.state = 967 + self.state = 987 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,109,self._ctx) + la_ = self._interp.adaptivePredict(self._input,115,self._ctx) if la_ == 1: - self.state = 966 + self.state = 986 self.hogqlxTagElement() - self.state = 969 + self.state = 989 self.match(HogQLParser.LT) - self.state = 970 + self.state = 990 self.match(HogQLParser.SLASH) - self.state = 971 + self.state = 991 self.identifier() - self.state = 972 + self.state = 992 self.match(HogQLParser.GT) pass @@ -7158,36 +7236,36 @@ def hogqlxTagAttribute(self): localctx = HogQLParser.HogqlxTagAttributeContext(self, self._ctx, self.state) self.enterRule(localctx, 116, self.RULE_hogqlxTagAttribute) try: - self.state = 987 + self.state = 1007 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,111,self._ctx) + la_ = self._interp.adaptivePredict(self._input,117,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 976 + self.state = 996 self.identifier() - self.state = 977 + self.state = 997 self.match(HogQLParser.EQ_SINGLE) - self.state = 978 + self.state = 998 self.string() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 980 + self.state = 1000 self.identifier() - self.state = 981 + self.state = 1001 self.match(HogQLParser.EQ_SINGLE) - self.state = 982 + self.state = 1002 self.match(HogQLParser.LBRACE) - self.state = 983 + self.state = 1003 self.columnExpr(0) - self.state = 984 + self.state = 1004 self.match(HogQLParser.RBRACE) pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 986 + self.state = 1006 self.identifier() pass @@ -7240,17 +7318,17 @@ def withExprList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 989 + self.state = 1009 self.withExpr() - self.state = 994 + self.state = 1014 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 990 + self.state = 1010 self.match(HogQLParser.COMMA) - self.state = 991 + self.state = 1011 self.withExpr() - self.state = 996 + self.state = 1016 self._errHandler.sync(self) _la = self._input.LA(1) @@ -7334,32 +7412,32 @@ def withExpr(self): localctx = HogQLParser.WithExprContext(self, self._ctx, self.state) self.enterRule(localctx, 120, self.RULE_withExpr) try: - self.state = 1007 + self.state = 1027 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,113,self._ctx) + la_ = self._interp.adaptivePredict(self._input,119,self._ctx) if la_ == 1: localctx = HogQLParser.WithExprSubqueryContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 997 + self.state = 1017 self.identifier() - self.state = 998 + self.state = 1018 self.match(HogQLParser.AS) - self.state = 999 + self.state = 1019 self.match(HogQLParser.LPAREN) - self.state = 1000 + self.state = 1020 self.selectUnionStmt() - self.state = 1001 + self.state = 1021 self.match(HogQLParser.RPAREN) pass elif la_ == 2: localctx = HogQLParser.WithExprColumnContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 1003 + self.state = 1023 self.columnExpr(0) - self.state = 1004 + self.state = 1024 self.match(HogQLParser.AS) - self.state = 1005 + self.state = 1025 self.identifier() pass @@ -7412,27 +7490,27 @@ def columnIdentifier(self): localctx = HogQLParser.ColumnIdentifierContext(self, self._ctx, self.state) self.enterRule(localctx, 122, self.RULE_columnIdentifier) try: - self.state = 1016 + self.state = 1036 self._errHandler.sync(self) token = self._input.LA(1) if token in [124]: self.enterOuterAlt(localctx, 1) - self.state = 1009 + self.state = 1029 self.placeholder() pass elif token in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 97, 98, 99, 101]: self.enterOuterAlt(localctx, 2) - self.state = 1013 + self.state = 1033 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,114,self._ctx) + la_ = self._interp.adaptivePredict(self._input,120,self._ctx) if la_ == 1: - self.state = 1010 + self.state = 1030 self.tableIdentifier() - self.state = 1011 + self.state = 1031 self.match(HogQLParser.DOT) - self.state = 1015 + self.state = 1035 self.nestedIdentifier() pass else: @@ -7485,20 +7563,20 @@ def nestedIdentifier(self): self.enterRule(localctx, 124, self.RULE_nestedIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 1018 + self.state = 1038 self.identifier() - self.state = 1023 + self.state = 1043 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,116,self._ctx) + _alt = self._interp.adaptivePredict(self._input,122,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: - self.state = 1019 + self.state = 1039 self.match(HogQLParser.DOT) - self.state = 1020 + self.state = 1040 self.identifier() - self.state = 1025 + self.state = 1045 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,116,self._ctx) + _alt = self._interp.adaptivePredict(self._input,122,self._ctx) except RecognitionException as re: localctx.exception = re @@ -7649,15 +7727,15 @@ def tableExpr(self, _p:int=0): self.enterRecursionRule(localctx, 126, self.RULE_tableExpr, _p) try: self.enterOuterAlt(localctx, 1) - self.state = 1035 + self.state = 1055 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,117,self._ctx) + la_ = self._interp.adaptivePredict(self._input,123,self._ctx) if la_ == 1: localctx = HogQLParser.TableExprIdentifierContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1027 + self.state = 1047 self.tableIdentifier() pass @@ -7665,7 +7743,7 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1028 + self.state = 1048 self.tableFunctionExpr() pass @@ -7673,11 +7751,11 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprSubqueryContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1029 + self.state = 1049 self.match(HogQLParser.LPAREN) - self.state = 1030 + self.state = 1050 self.selectUnionStmt() - self.state = 1031 + self.state = 1051 self.match(HogQLParser.RPAREN) pass @@ -7685,7 +7763,7 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprTagContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1033 + self.state = 1053 self.hogqlxTagElement() pass @@ -7693,15 +7771,15 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprPlaceholderContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1034 + self.state = 1054 self.placeholder() pass self._ctx.stop = self._input.LT(-1) - self.state = 1045 + self.state = 1065 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,119,self._ctx) + _alt = self._interp.adaptivePredict(self._input,125,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: @@ -7709,29 +7787,29 @@ def tableExpr(self, _p:int=0): _prevctx = localctx localctx = HogQLParser.TableExprAliasContext(self, HogQLParser.TableExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_tableExpr) - self.state = 1037 + self.state = 1057 if not self.precpred(self._ctx, 3): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 3)") - self.state = 1041 + self.state = 1061 self._errHandler.sync(self) token = self._input.LA(1) if token in [19, 28, 37, 46, 101]: - self.state = 1038 + self.state = 1058 self.alias() pass elif token in [6]: - self.state = 1039 + self.state = 1059 self.match(HogQLParser.AS) - self.state = 1040 + self.state = 1060 self.identifier() pass else: raise NoViableAltException(self) - self.state = 1047 + self.state = 1067 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,119,self._ctx) + _alt = self._interp.adaptivePredict(self._input,125,self._ctx) except RecognitionException as re: localctx.exception = re @@ -7782,19 +7860,19 @@ def tableFunctionExpr(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1048 + self.state = 1068 self.identifier() - self.state = 1049 + self.state = 1069 self.match(HogQLParser.LPAREN) - self.state = 1051 + self.state = 1071 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 1050 + self.state = 1070 self.tableArgList() - self.state = 1053 + self.state = 1073 self.match(HogQLParser.RPAREN) except RecognitionException as re: localctx.exception = re @@ -7841,17 +7919,17 @@ def tableIdentifier(self): self.enterRule(localctx, 130, self.RULE_tableIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 1058 + self.state = 1078 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,121,self._ctx) + la_ = self._interp.adaptivePredict(self._input,127,self._ctx) if la_ == 1: - self.state = 1055 + self.state = 1075 self.databaseIdentifier() - self.state = 1056 + self.state = 1076 self.match(HogQLParser.DOT) - self.state = 1060 + self.state = 1080 self.identifier() except RecognitionException as re: localctx.exception = re @@ -7901,17 +7979,17 @@ def tableArgList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1062 + self.state = 1082 self.columnExpr(0) - self.state = 1067 + self.state = 1087 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 1063 + self.state = 1083 self.match(HogQLParser.COMMA) - self.state = 1064 + self.state = 1084 self.columnExpr(0) - self.state = 1069 + self.state = 1089 self._errHandler.sync(self) _la = self._input.LA(1) @@ -7953,7 +8031,7 @@ def databaseIdentifier(self): self.enterRule(localctx, 134, self.RULE_databaseIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 1070 + self.state = 1090 self.identifier() except RecognitionException as re: localctx.exception = re @@ -8004,19 +8082,19 @@ def floatingLiteral(self): self.enterRule(localctx, 136, self.RULE_floatingLiteral) self._la = 0 # Token type try: - self.state = 1080 + self.state = 1100 self._errHandler.sync(self) token = self._input.LA(1) if token in [102]: self.enterOuterAlt(localctx, 1) - self.state = 1072 + self.state = 1092 self.match(HogQLParser.FLOATING_LITERAL) pass elif token in [116]: self.enterOuterAlt(localctx, 2) - self.state = 1073 + self.state = 1093 self.match(HogQLParser.DOT) - self.state = 1074 + self.state = 1094 _la = self._input.LA(1) if not(_la==103 or _la==104): self._errHandler.recoverInline(self) @@ -8026,15 +8104,15 @@ def floatingLiteral(self): pass elif token in [104]: self.enterOuterAlt(localctx, 3) - self.state = 1075 + self.state = 1095 self.match(HogQLParser.DECIMAL_LITERAL) - self.state = 1076 + self.state = 1096 self.match(HogQLParser.DOT) - self.state = 1078 + self.state = 1098 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,123,self._ctx) + la_ = self._interp.adaptivePredict(self._input,129,self._ctx) if la_ == 1: - self.state = 1077 + self.state = 1097 _la = self._input.LA(1) if not(_la==103 or _la==104): self._errHandler.recoverInline(self) @@ -8107,11 +8185,11 @@ def numberLiteral(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1083 + self.state = 1103 self._errHandler.sync(self) _la = self._input.LA(1) if _la==114 or _la==134: - self.state = 1082 + self.state = 1102 _la = self._input.LA(1) if not(_la==114 or _la==134): self._errHandler.recoverInline(self) @@ -8120,36 +8198,36 @@ def numberLiteral(self): self.consume() - self.state = 1091 + self.state = 1111 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,126,self._ctx) + la_ = self._interp.adaptivePredict(self._input,132,self._ctx) if la_ == 1: - self.state = 1085 + self.state = 1105 self.floatingLiteral() pass elif la_ == 2: - self.state = 1086 + self.state = 1106 self.match(HogQLParser.OCTAL_LITERAL) pass elif la_ == 3: - self.state = 1087 + self.state = 1107 self.match(HogQLParser.DECIMAL_LITERAL) pass elif la_ == 4: - self.state = 1088 + self.state = 1108 self.match(HogQLParser.HEXADECIMAL_LITERAL) pass elif la_ == 5: - self.state = 1089 + self.state = 1109 self.match(HogQLParser.INF) pass elif la_ == 6: - self.state = 1090 + self.state = 1110 self.match(HogQLParser.NAN_SQL) pass @@ -8197,22 +8275,22 @@ def literal(self): localctx = HogQLParser.LiteralContext(self, self._ctx, self.state) self.enterRule(localctx, 140, self.RULE_literal) try: - self.state = 1096 + self.state = 1116 self._errHandler.sync(self) token = self._input.LA(1) if token in [41, 55, 102, 103, 104, 105, 114, 116, 134]: self.enterOuterAlt(localctx, 1) - self.state = 1093 + self.state = 1113 self.numberLiteral() pass elif token in [106]: self.enterOuterAlt(localctx, 2) - self.state = 1094 + self.state = 1114 self.match(HogQLParser.STRING_LITERAL) pass elif token in [57]: self.enterOuterAlt(localctx, 3) - self.state = 1095 + self.state = 1115 self.match(HogQLParser.NULL_SQL) pass else: @@ -8277,7 +8355,7 @@ def interval(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1098 + self.state = 1118 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 27021666484748288) != 0) or ((((_la - 68)) & ~0x3f) == 0 and ((1 << (_la - 68)) & 2181038337) != 0)): self._errHandler.recoverInline(self) @@ -8574,7 +8652,7 @@ def keyword(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1100 + self.state = 1120 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & -208293751046537218) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 29527896047) != 0)): self._errHandler.recoverInline(self) @@ -8628,7 +8706,7 @@ def keywordForAlias(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1102 + self.state = 1122 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 70506452090880) != 0)): self._errHandler.recoverInline(self) @@ -8675,17 +8753,17 @@ def alias(self): localctx = HogQLParser.AliasContext(self, self._ctx, self.state) self.enterRule(localctx, 148, self.RULE_alias) try: - self.state = 1106 + self.state = 1126 self._errHandler.sync(self) token = self._input.LA(1) if token in [101]: self.enterOuterAlt(localctx, 1) - self.state = 1104 + self.state = 1124 self.match(HogQLParser.IDENTIFIER) pass elif token in [19, 28, 37, 46]: self.enterOuterAlt(localctx, 2) - self.state = 1105 + self.state = 1125 self.keywordForAlias() pass else: @@ -8735,22 +8813,22 @@ def identifier(self): localctx = HogQLParser.IdentifierContext(self, self._ctx, self.state) self.enterRule(localctx, 150, self.RULE_identifier) try: - self.state = 1111 + self.state = 1131 self._errHandler.sync(self) token = self._input.LA(1) if token in [101]: self.enterOuterAlt(localctx, 1) - self.state = 1108 + self.state = 1128 self.match(HogQLParser.IDENTIFIER) pass elif token in [20, 36, 53, 54, 68, 76, 93, 99]: self.enterOuterAlt(localctx, 2) - self.state = 1109 + self.state = 1129 self.interval() pass elif token in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 69, 70, 71, 72, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 95, 97, 98]: self.enterOuterAlt(localctx, 3) - self.state = 1110 + self.state = 1130 self.keyword() pass else: @@ -8801,11 +8879,11 @@ def enumValue(self): self.enterRule(localctx, 152, self.RULE_enumValue) try: self.enterOuterAlt(localctx, 1) - self.state = 1113 + self.state = 1133 self.string() - self.state = 1114 + self.state = 1134 self.match(HogQLParser.EQ_SINGLE) - self.state = 1115 + self.state = 1135 self.numberLiteral() except RecognitionException as re: localctx.exception = re @@ -8851,11 +8929,11 @@ def placeholder(self): self.enterRule(localctx, 154, self.RULE_placeholder) try: self.enterOuterAlt(localctx, 1) - self.state = 1117 + self.state = 1137 self.match(HogQLParser.LBRACE) - self.state = 1118 + self.state = 1138 self.identifier() - self.state = 1119 + self.state = 1139 self.match(HogQLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -8897,17 +8975,17 @@ def string(self): localctx = HogQLParser.StringContext(self, self._ctx, self.state) self.enterRule(localctx, 156, self.RULE_string) try: - self.state = 1123 + self.state = 1143 self._errHandler.sync(self) token = self._input.LA(1) if token in [106]: self.enterOuterAlt(localctx, 1) - self.state = 1121 + self.state = 1141 self.match(HogQLParser.STRING_LITERAL) pass elif token in [137]: self.enterOuterAlt(localctx, 2) - self.state = 1122 + self.state = 1142 self.templateString() pass else: @@ -8961,19 +9039,19 @@ def templateString(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1125 + self.state = 1145 self.match(HogQLParser.QUOTE_SINGLE_TEMPLATE) - self.state = 1129 + self.state = 1149 self._errHandler.sync(self) _la = self._input.LA(1) while _la==151 or _la==152: - self.state = 1126 + self.state = 1146 self.stringContents() - self.state = 1131 + self.state = 1151 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 1132 + self.state = 1152 self.match(HogQLParser.QUOTE_SINGLE) except RecognitionException as re: localctx.exception = re @@ -9021,21 +9099,21 @@ def stringContents(self): localctx = HogQLParser.StringContentsContext(self, self._ctx, self.state) self.enterRule(localctx, 160, self.RULE_stringContents) try: - self.state = 1139 + self.state = 1159 self._errHandler.sync(self) token = self._input.LA(1) if token in [152]: self.enterOuterAlt(localctx, 1) - self.state = 1134 + self.state = 1154 self.match(HogQLParser.STRING_ESCAPE_TRIGGER) - self.state = 1135 + self.state = 1155 self.columnExpr(0) - self.state = 1136 + self.state = 1156 self.match(HogQLParser.RBRACE) pass elif token in [151]: self.enterOuterAlt(localctx, 2) - self.state = 1138 + self.state = 1158 self.match(HogQLParser.STRING_TEXT) pass else: @@ -9089,19 +9167,19 @@ def fullTemplateString(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1141 + self.state = 1161 self.match(HogQLParser.QUOTE_SINGLE_TEMPLATE_FULL) - self.state = 1145 + self.state = 1165 self._errHandler.sync(self) _la = self._input.LA(1) while _la==153 or _la==154: - self.state = 1142 + self.state = 1162 self.stringContentsFull() - self.state = 1147 + self.state = 1167 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 1148 + self.state = 1168 self.match(HogQLParser.EOF) except RecognitionException as re: localctx.exception = re @@ -9149,21 +9227,21 @@ def stringContentsFull(self): localctx = HogQLParser.StringContentsFullContext(self, self._ctx, self.state) self.enterRule(localctx, 164, self.RULE_stringContentsFull) try: - self.state = 1155 + self.state = 1175 self._errHandler.sync(self) token = self._input.LA(1) if token in [154]: self.enterOuterAlt(localctx, 1) - self.state = 1150 + self.state = 1170 self.match(HogQLParser.FULL_STRING_ESCAPE_TRIGGER) - self.state = 1151 + self.state = 1171 self.columnExpr(0) - self.state = 1152 + self.state = 1172 self.match(HogQLParser.RBRACE) pass elif token in [153]: self.enterOuterAlt(localctx, 2) - self.state = 1154 + self.state = 1174 self.match(HogQLParser.FULL_STRING_TEXT) pass else: diff --git a/posthog/hogql/modifiers.py b/posthog/hogql/modifiers.py index ce17684f47f3d..3aedc9572a4b8 100644 --- a/posthog/hogql/modifiers.py +++ b/posthog/hogql/modifiers.py @@ -34,26 +34,26 @@ def create_default_modifiers_for_team( def set_default_modifier_values(modifiers: HogQLQueryModifiers, team: "Team"): if modifiers.personsOnEventsMode is None: - modifiers.personsOnEventsMode = team.person_on_events_mode or PersonsOnEventsMode.disabled + modifiers.personsOnEventsMode = team.person_on_events_mode or PersonsOnEventsMode.DISABLED if modifiers.personsArgMaxVersion is None: - modifiers.personsArgMaxVersion = PersonsArgMaxVersion.auto + modifiers.personsArgMaxVersion = PersonsArgMaxVersion.AUTO if modifiers.inCohortVia is None: - modifiers.inCohortVia = InCohortVia.auto + modifiers.inCohortVia = InCohortVia.AUTO - if modifiers.materializationMode is None or modifiers.materializationMode == MaterializationMode.auto: - modifiers.materializationMode = MaterializationMode.legacy_null_as_null + if modifiers.materializationMode is None or modifiers.materializationMode == MaterializationMode.AUTO: + modifiers.materializationMode = MaterializationMode.LEGACY_NULL_AS_NULL if modifiers.optimizeJoinedFilters is None: modifiers.optimizeJoinedFilters = False if modifiers.bounceRatePageViewMode is None: - modifiers.bounceRatePageViewMode = BounceRatePageViewMode.count_pageviews + modifiers.bounceRatePageViewMode = BounceRatePageViewMode.COUNT_PAGEVIEWS def set_default_in_cohort_via(modifiers: HogQLQueryModifiers) -> HogQLQueryModifiers: - if modifiers.inCohortVia is None or modifiers.inCohortVia == InCohortVia.auto: - modifiers.inCohortVia = InCohortVia.subquery + if modifiers.inCohortVia is None or modifiers.inCohortVia == InCohortVia.AUTO: + modifiers.inCohortVia = InCohortVia.SUBQUERY return modifiers diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index bab0e9486d003..bf08f5c122635 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -822,14 +822,16 @@ def visitColumnExprNot(self, ctx: HogQLParser.ColumnExprNotContext): def visitColumnExprWinFunctionTarget(self, ctx: HogQLParser.ColumnExprWinFunctionTargetContext): return ast.WindowFunction( name=self.visit(ctx.identifier(0)), - args=self.visit(ctx.columnExprList()) if ctx.columnExprList() else [], + exprs=self.visit(ctx.columnExprList()) if ctx.columnExprList() else [], + args=self.visit(ctx.columnArgList()) if ctx.columnArgList() else [], over_identifier=self.visit(ctx.identifier(1)), ) def visitColumnExprWinFunction(self, ctx: HogQLParser.ColumnExprWinFunctionContext): return ast.WindowFunction( name=self.visit(ctx.identifier()), - args=self.visit(ctx.columnExprList()) if ctx.columnExprList() else [], + exprs=self.visit(ctx.columnExprList()) if ctx.columnExprList() else [], + args=self.visit(ctx.columnArgList()) if ctx.columnArgList() else [], over_expr=self.visit(ctx.windowExpr()) if ctx.windowExpr() else None, ) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 69b6ae1ef342a..3104d121112de 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -100,12 +100,12 @@ def prepare_ast_for_printing( context.modifiers = set_default_in_cohort_via(context.modifiers) - if context.modifiers.inCohortVia == InCohortVia.leftjoin_conjoined: + if context.modifiers.inCohortVia == InCohortVia.LEFTJOIN_CONJOINED: with context.timings.measure("resolve_in_cohorts_conjoined"): resolve_in_cohorts_conjoined(node, dialect, context, stack) with context.timings.measure("resolve_types"): node = resolve_types(node, context, dialect=dialect, scopes=[node.type for node in stack] if stack else None) - if context.modifiers.inCohortVia == InCohortVia.leftjoin: + if context.modifiers.inCohortVia == InCohortVia.LEFTJOIN: with context.timings.measure("resolve_in_cohorts"): resolve_in_cohorts(node, dialect, stack, context) if dialect == "clickhouse": @@ -573,23 +573,23 @@ def visit_compare_operation(self, node: ast.CompareOperation): value_if_one_side_is_null = True elif node.op == ast.CompareOperationOp.Gt: op = f"greater({left}, {right})" - constant_lambda = ( - lambda left_op, right_op: left_op > right_op if left_op is not None and right_op is not None else False + constant_lambda = lambda left_op, right_op: ( + left_op > right_op if left_op is not None and right_op is not None else False ) elif node.op == ast.CompareOperationOp.GtEq: op = f"greaterOrEquals({left}, {right})" - constant_lambda = ( - lambda left_op, right_op: left_op >= right_op if left_op is not None and right_op is not None else False + constant_lambda = lambda left_op, right_op: ( + left_op >= right_op if left_op is not None and right_op is not None else False ) elif node.op == ast.CompareOperationOp.Lt: op = f"less({left}, {right})" - constant_lambda = ( - lambda left_op, right_op: left_op < right_op if left_op is not None and right_op is not None else False + constant_lambda = lambda left_op, right_op: ( + left_op < right_op if left_op is not None and right_op is not None else False ) elif node.op == ast.CompareOperationOp.LtEq: op = f"lessOrEquals({left}, {right})" - constant_lambda = ( - lambda left_op, right_op: left_op <= right_op if left_op is not None and right_op is not None else False + constant_lambda = lambda left_op, right_op: ( + left_op <= right_op if left_op is not None and right_op is not None else False ) else: raise ImpossibleASTError(f"Unknown CompareOperationOp: {node.op.name}") @@ -956,7 +956,7 @@ def visit_field_type(self, type: ast.FieldType): and type.name == "properties" and type.table_type.field == "poe" ): - if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.DISABLED: field_sql = "person_properties" else: field_sql = "person_props" @@ -980,7 +980,7 @@ def visit_field_type(self, type: ast.FieldType): # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. if self.context.within_non_hogql_query and field_sql == "events__pdi__person.properties": - if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.DISABLED: field_sql = "person_properties" else: field_sql = "person_props" @@ -1030,7 +1030,7 @@ def visit_property_type(self, type: ast.PropertyType): or (isinstance(table, ast.VirtualTableType) and table.field == "poe") ): # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. - if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.DISABLED: materialized_column = self._get_materialized_column( "events", str(type.chain[0]), "person_properties" ) @@ -1041,9 +1041,9 @@ def visit_property_type(self, type: ast.PropertyType): if materialized_property_sql is not None: # TODO: rematerialize all columns to properly support empty strings and "null" string values. - if self.context.modifiers.materializationMode == MaterializationMode.legacy_null_as_string: + if self.context.modifiers.materializationMode == MaterializationMode.LEGACY_NULL_AS_STRING: materialized_property_sql = f"nullIf({materialized_property_sql}, '')" - else: # MaterializationMode.auto.legacy_null_as_null + else: # MaterializationMode AUTO or LEGACY_NULL_AS_NULL materialized_property_sql = f"nullIf(nullIf({materialized_property_sql}, ''), 'null')" if len(type.chain) == 1: @@ -1140,8 +1140,11 @@ def visit_window_expr(self, node: ast.WindowExpr): return " ".join(strings) def visit_window_function(self, node: ast.WindowFunction): + identifier = self._print_identifier(node.name) + exprs = ", ".join(self.visit(expr) for expr in node.exprs or []) + args = "(" + (", ".join(self.visit(arg) for arg in node.args or [])) + ")" if node.args else "" over = f"({self.visit(node.over_expr)})" if node.over_expr else self._print_identifier(node.over_identifier) - return f"{self._print_identifier(node.name)}({', '.join(self.visit(expr) for expr in node.args or [])}) OVER {over}" + return f"{identifier}({exprs}){args} OVER {over}" def visit_window_frame_expr(self, node: ast.WindowFrameExpr): if node.frame_type == "PRECEDING": diff --git a/posthog/hogql/property.py b/posthog/hogql/property.py index 056334812f8f5..652a46fee9141 100644 --- a/posthog/hogql/property.py +++ b/posthog/hogql/property.py @@ -105,8 +105,8 @@ def property_to_expr( raise NotImplementedError(f'PropertyGroup of unknown type "{property.type}"') if ( (isinstance(property, PropertyGroupFilter) or isinstance(property, PropertyGroupFilterValue)) - and property.type != FilterLogicalOperator.AND - and property.type != FilterLogicalOperator.OR + and property.type != FilterLogicalOperator.AND_ + and property.type != FilterLogicalOperator.OR_ ): raise NotImplementedError(f'PropertyGroupFilter of unknown type "{property.type}"') @@ -115,7 +115,7 @@ def property_to_expr( if len(property.values) == 1: return property_to_expr(property.values[0], team, scope) - if property.type == PropertyOperatorType.AND or property.type == FilterLogicalOperator.AND: + if property.type == PropertyOperatorType.AND or property.type == FilterLogicalOperator.AND_: return ast.And(exprs=[property_to_expr(p, team, scope) for p in property.values]) else: return ast.Or(exprs=[property_to_expr(p, team, scope) for p in property.values]) @@ -143,7 +143,7 @@ def property_to_expr( ): if (scope == "person" and property.type != "person") or (scope == "session" and property.type != "session"): raise NotImplementedError(f"The '{property.type}' property filter does not work in '{scope}' scope") - operator = cast(Optional[PropertyOperator], property.operator) or PropertyOperator.exact + operator = cast(Optional[PropertyOperator], property.operator) or PropertyOperator.EXACT value = property.value if property.type == "person" and scope != "person": @@ -195,20 +195,20 @@ def property_to_expr( for v in value ] if ( - operator == PropertyOperator.not_icontains - or operator == PropertyOperator.not_regex - or operator == PropertyOperator.is_not + operator == PropertyOperator.NOT_ICONTAINS + or operator == PropertyOperator.NOT_REGEX + or operator == PropertyOperator.IS_NOT ): return ast.And(exprs=exprs) return ast.Or(exprs=exprs) - if operator == PropertyOperator.is_set: + if operator == PropertyOperator.IS_SET: return ast.CompareOperation( op=ast.CompareOperationOp.NotEq, left=field, right=ast.Constant(value=None), ) - elif operator == PropertyOperator.is_not_set: + elif operator == PropertyOperator.IS_NOT_SET: return ast.Or( exprs=[ ast.CompareOperation( @@ -230,19 +230,19 @@ def property_to_expr( ] ) ) - elif operator == PropertyOperator.icontains: + elif operator == PropertyOperator.ICONTAINS: return ast.CompareOperation( op=ast.CompareOperationOp.ILike, left=field, right=ast.Constant(value=f"%{value}%"), ) - elif operator == PropertyOperator.not_icontains: + elif operator == PropertyOperator.NOT_ICONTAINS: return ast.CompareOperation( op=ast.CompareOperationOp.NotILike, left=field, right=ast.Constant(value=f"%{value}%"), ) - elif operator == PropertyOperator.regex: + elif operator == PropertyOperator.REGEX: return ast.Call( name="ifNull", args=[ @@ -250,7 +250,7 @@ def property_to_expr( ast.Constant(value=False), ], ) - elif operator == PropertyOperator.not_regex: + elif operator == PropertyOperator.NOT_REGEX: return ast.Call( name="ifNull", args=[ @@ -265,17 +265,17 @@ def property_to_expr( ast.Constant(value=True), ], ) - elif operator == PropertyOperator.exact or operator == PropertyOperator.is_date_exact: + elif operator == PropertyOperator.EXACT or operator == PropertyOperator.IS_DATE_EXACT: op = ast.CompareOperationOp.Eq - elif operator == PropertyOperator.is_not: + elif operator == PropertyOperator.IS_NOT: op = ast.CompareOperationOp.NotEq - elif operator == PropertyOperator.lt or operator == PropertyOperator.is_date_before: + elif operator == PropertyOperator.LT or operator == PropertyOperator.IS_DATE_BEFORE: op = ast.CompareOperationOp.Lt - elif operator == PropertyOperator.gt or operator == PropertyOperator.is_date_after: + elif operator == PropertyOperator.GT or operator == PropertyOperator.IS_DATE_AFTER: op = ast.CompareOperationOp.Gt - elif operator == PropertyOperator.lte: + elif operator == PropertyOperator.LTE: op = ast.CompareOperationOp.LtEq - elif operator == PropertyOperator.gte: + elif operator == PropertyOperator.GTE: op = ast.CompareOperationOp.GtEq else: raise NotImplementedError(f"PropertyOperator {operator} not implemented") @@ -365,7 +365,7 @@ def property_to_expr( if scope == "person": raise NotImplementedError(f"property_to_expr for scope {scope} not implemented for type '{property.type}'") value = property.value - operator = cast(Optional[PropertyOperator], property.operator) or PropertyOperator.exact + operator = cast(Optional[PropertyOperator], property.operator) or PropertyOperator.EXACT if isinstance(value, list): if len(value) == 1: value = value[0] @@ -385,20 +385,20 @@ def property_to_expr( for v in value ] if ( - operator == PropertyOperator.is_not - or operator == PropertyOperator.not_icontains - or operator == PropertyOperator.not_regex + operator == PropertyOperator.IS_NOT + or operator == PropertyOperator.NOT_ICONTAINS + or operator == PropertyOperator.NOT_REGEX ): return ast.And(exprs=exprs) return ast.Or(exprs=exprs) if property.key == "selector" or property.key == "tag_name": - if operator != PropertyOperator.exact and operator != PropertyOperator.is_not: + if operator != PropertyOperator.EXACT and operator != PropertyOperator.IS_NOT: raise NotImplementedError( f"property_to_expr for element {property.key} only supports exact and is_not operators, not {operator}" ) expr = selector_to_expr(str(value)) if property.key == "selector" else tag_name_to_expr(str(value)) - if operator == PropertyOperator.is_not: + if operator == PropertyOperator.IS_NOT: return ast.Call(name="not", args=[expr]) return expr @@ -445,19 +445,19 @@ def action_to_expr(action: Action) -> ast.Expr: exprs.append(tag_name_to_expr(step.tag_name)) if step.href is not None: if step.href_matching == "regex": - operator = PropertyOperator.regex + operator = PropertyOperator.REGEX elif step.href_matching == "contains": - operator = PropertyOperator.icontains + operator = PropertyOperator.ICONTAINS else: - operator = PropertyOperator.exact + operator = PropertyOperator.EXACT exprs.append(element_chain_key_filter("href", step.href, operator)) if step.text is not None: if step.text_matching == "regex": - operator = PropertyOperator.regex + operator = PropertyOperator.REGEX elif step.text_matching == "contains": - operator = PropertyOperator.icontains + operator = PropertyOperator.ICONTAINS else: - operator = PropertyOperator.exact + operator = PropertyOperator.EXACT exprs.append(element_chain_key_filter("text", step.text, operator)) if step.url: @@ -510,28 +510,28 @@ def entity_to_expr(entity: RetentionEntity) -> ast.Expr: def element_chain_key_filter(key: str, text: str, operator: PropertyOperator): escaped = text.replace('"', r"\"") - if operator == PropertyOperator.is_set or operator == PropertyOperator.is_not_set: + if operator == PropertyOperator.IS_SET or operator == PropertyOperator.IS_NOT_SET: value = r'[^"]+' - elif operator == PropertyOperator.icontains or operator == PropertyOperator.not_icontains: + elif operator == PropertyOperator.ICONTAINS or operator == PropertyOperator.NOT_ICONTAINS: value = rf'[^"]*{re.escape(escaped)}[^"]*' - elif operator == PropertyOperator.regex or operator == PropertyOperator.not_regex: + elif operator == PropertyOperator.REGEX or operator == PropertyOperator.NOT_REGEX: value = escaped - elif operator == PropertyOperator.exact or operator == PropertyOperator.is_not: + elif operator == PropertyOperator.EXACT or operator == PropertyOperator.IS_NOT: value = re.escape(escaped) else: raise NotImplementedError(f"element_href_to_expr not implemented for operator {operator}") regex = f'({key}="{value}")' - if operator == PropertyOperator.icontains or operator == PropertyOperator.not_icontains: + if operator == PropertyOperator.ICONTAINS or operator == PropertyOperator.NOT_ICONTAINS: expr = parse_expr("elements_chain =~* {regex}", {"regex": ast.Constant(value=str(regex))}) else: expr = parse_expr("elements_chain =~ {regex}", {"regex": ast.Constant(value=str(regex))}) if ( - operator == PropertyOperator.is_not_set - or operator == PropertyOperator.not_icontains - or operator == PropertyOperator.is_not - or operator == PropertyOperator.not_regex + operator == PropertyOperator.IS_NOT_SET + or operator == PropertyOperator.NOT_ICONTAINS + or operator == PropertyOperator.IS_NOT + or operator == PropertyOperator.NOT_REGEX ): expr = ast.Call(name="not", args=[expr]) return expr diff --git a/posthog/hogql/test/_test_parser.py b/posthog/hogql/test/_test_parser.py index 4a76240ffdc90..d1bff88ebfcff 100644 --- a/posthog/hogql/test/_test_parser.py +++ b/posthog/hogql/test/_test_parser.py @@ -1409,7 +1409,7 @@ def test_window_functions(self): alias="timestamp", expr=ast.WindowFunction( name="min", - args=[ast.Field(chain=["timestamp"])], + exprs=[ast.Field(chain=["timestamp"])], over_expr=ast.WindowExpr( partition_by=[ast.Field(chain=["person", "id"])], order_by=[ @@ -1429,6 +1429,32 @@ def test_window_functions(self): ) self.assertEqual(expr, expected) + def test_window_functions_call_arg(self): + query = "SELECT quantiles(0.0, 0.25, 0.5, 0.75, 1.0)(distinct distinct_id) over () as values FROM events" + expr = self._select(query) + expected = ast.SelectQuery( + select=[ + ast.Alias( + alias="values", + expr=ast.WindowFunction( + name="quantiles", + args=[ast.Field(chain=["distinct_id"])], + exprs=[ + ast.Constant(value=0.0), + ast.Constant(value=0.25), + ast.Constant(value=0.5), + ast.Constant(value=0.75), + ast.Constant(value=1.0), + ], + over_expr=ast.WindowExpr(), + ), + hidden=False, + ) + ], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), + ) + self.assertEqual(expr, expected) + def test_window_functions_with_window(self): query = "SELECT person.id, min(timestamp) over win1 AS timestamp FROM events WINDOW win1 as (PARTITION by person.id ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)" expr = self._select(query) @@ -1439,7 +1465,7 @@ def test_window_functions_with_window(self): alias="timestamp", expr=ast.WindowFunction( name="min", - args=[ast.Field(chain=["timestamp"])], + exprs=[ast.Field(chain=["timestamp"])], over_identifier="win1", ), ), diff --git a/posthog/hogql/test/test_modifiers.py b/posthog/hogql/test/test_modifiers.py index b4619cacfbc21..d118f4e32355d 100644 --- a/posthog/hogql/test/test_modifiers.py +++ b/posthog/hogql/test/test_modifiers.py @@ -17,34 +17,34 @@ class TestModifiers(BaseTest): def test_create_default_modifiers_for_team_init(self): assert self.team.person_on_events_mode == "disabled" modifiers = create_default_modifiers_for_team(self.team) - assert modifiers.personsOnEventsMode == PersonsOnEventsMode.disabled # NB! not a None + assert modifiers.personsOnEventsMode == PersonsOnEventsMode.DISABLED # NB! not a None modifiers = create_default_modifiers_for_team( self.team, - HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.person_id_no_override_properties_on_events), + HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS), ) - assert modifiers.personsOnEventsMode == PersonsOnEventsMode.person_id_no_override_properties_on_events + assert modifiers.personsOnEventsMode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS modifiers = create_default_modifiers_for_team( self.team, - HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.person_id_override_properties_on_events), + HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS), ) - assert modifiers.personsOnEventsMode == PersonsOnEventsMode.person_id_override_properties_on_events + assert modifiers.personsOnEventsMode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS def test_team_modifiers_override(self): assert self.team.modifiers is None modifiers = create_default_modifiers_for_team(self.team) assert modifiers.personsOnEventsMode == self.team.default_modifiers["personsOnEventsMode"] - assert modifiers.personsOnEventsMode == PersonsOnEventsMode.disabled # the default mode + assert modifiers.personsOnEventsMode == PersonsOnEventsMode.DISABLED # the default mode - self.team.modifiers = {"personsOnEventsMode": PersonsOnEventsMode.person_id_override_properties_on_events} + self.team.modifiers = {"personsOnEventsMode": PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS} self.team.save() modifiers = create_default_modifiers_for_team(self.team) - assert modifiers.personsOnEventsMode == PersonsOnEventsMode.person_id_override_properties_on_events - assert self.team.default_modifiers["personsOnEventsMode"] == PersonsOnEventsMode.disabled # no change here + assert modifiers.personsOnEventsMode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + assert self.team.default_modifiers["personsOnEventsMode"] == PersonsOnEventsMode.DISABLED # no change here - self.team.modifiers = {"personsOnEventsMode": PersonsOnEventsMode.person_id_no_override_properties_on_events} + self.team.modifiers = {"personsOnEventsMode": PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS} self.team.save() modifiers = create_default_modifiers_for_team(self.team) - assert modifiers.personsOnEventsMode == PersonsOnEventsMode.person_id_no_override_properties_on_events + assert modifiers.personsOnEventsMode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS def test_modifiers_persons_on_events_mode_person_id_override_properties_on_events(self): query = "SELECT event, person_id FROM events" @@ -53,7 +53,7 @@ def test_modifiers_persons_on_events_mode_person_id_override_properties_on_event response = execute_hogql_query( query, team=self.team, - modifiers=HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.disabled), + modifiers=HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.DISABLED), ) assert " JOIN " in response.clickhouse @@ -62,7 +62,7 @@ def test_modifiers_persons_on_events_mode_person_id_override_properties_on_event query, team=self.team, modifiers=HogQLQueryModifiers( - personsOnEventsMode=PersonsOnEventsMode.person_id_no_override_properties_on_events + personsOnEventsMode=PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS ), ) assert " JOIN " not in response.clickhouse @@ -77,7 +77,7 @@ class TestCase(NamedTuple): test_cases: list[TestCase] = [ TestCase( - PersonsOnEventsMode.disabled, + PersonsOnEventsMode.DISABLED, [ "events.event AS event", "events__pdi__person.id AS id", @@ -86,7 +86,7 @@ class TestCase(NamedTuple): ], ), TestCase( - PersonsOnEventsMode.person_id_no_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, [ "events.event AS event", "events.person_id AS id", @@ -95,7 +95,7 @@ class TestCase(NamedTuple): ], ), TestCase( - PersonsOnEventsMode.person_id_override_properties_on_events, + PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS, [ "events.event AS event", "if(not(empty(events__override.distinct_id)), events__override.person_id, events.person_id) AS id", @@ -107,7 +107,7 @@ class TestCase(NamedTuple): ], ), TestCase( - PersonsOnEventsMode.person_id_override_properties_joined, + PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED, [ "events.event AS event", "events__person.id AS id", @@ -142,7 +142,7 @@ def test_modifiers_persons_argmax_version_v2(self): response = execute_hogql_query( query, team=self.team, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.v1), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.V1), ) assert "in(tuple(person.id, person.version)" not in response.clickhouse @@ -150,7 +150,7 @@ def test_modifiers_persons_argmax_version_v2(self): response = execute_hogql_query( query, team=self.team, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.v2), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.V2), ) assert "in(tuple(person.id, person.version)" in response.clickhouse @@ -159,7 +159,7 @@ def test_modifiers_persons_argmax_version_auto(self): response = execute_hogql_query( "SELECT id, properties.$browser, is_identified FROM persons", team=self.team, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.auto), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.AUTO), ) assert "in(tuple(person.id, person.version)" in response.clickhouse @@ -167,7 +167,7 @@ def test_modifiers_persons_argmax_version_auto(self): response = execute_hogql_query( "SELECT id, properties FROM persons", team=self.team, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.auto), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.AUTO), ) assert "in(tuple(person.id, person.version)" in response.clickhouse @@ -175,7 +175,7 @@ def test_modifiers_persons_argmax_version_auto(self): response = execute_hogql_query( "SELECT id, is_identified FROM persons", team=self.team, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.auto), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.AUTO), ) assert "in(tuple(person.id, person.version)" not in response.clickhouse @@ -208,7 +208,7 @@ def test_modifiers_materialization_mode(self): response = execute_hogql_query( "SELECT properties.$browser FROM events", team=self.team, - modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.auto), + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.AUTO), pretty=False, ) assert ( @@ -218,7 +218,7 @@ def test_modifiers_materialization_mode(self): response = execute_hogql_query( "SELECT properties.$browser FROM events", team=self.team, - modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.legacy_null_as_null), + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.LEGACY_NULL_AS_NULL), pretty=False, ) assert ( @@ -228,7 +228,7 @@ def test_modifiers_materialization_mode(self): response = execute_hogql_query( "SELECT properties.$browser FROM events", team=self.team, - modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.legacy_null_as_string), + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.LEGACY_NULL_AS_STRING), pretty=False, ) assert "SELECT nullIf(events.`mat_$browser`, '') AS `$browser` FROM events" in response.clickhouse @@ -236,7 +236,7 @@ def test_modifiers_materialization_mode(self): response = execute_hogql_query( "SELECT properties.$browser FROM events", team=self.team, - modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.disabled), + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.DISABLED), pretty=False, ) assert ( diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index c8466c4f40387..5bf05573e671f 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -141,7 +141,7 @@ def test_fields_and_properties(self): context = HogQLContext( team_id=self.team.pk, within_non_hogql_query=True, - modifiers=HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.disabled), + modifiers=HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.DISABLED), ) self.assertEqual( self._expr("person.properties.bla", context), @@ -158,7 +158,7 @@ def test_fields_and_properties(self): team_id=self.team.pk, within_non_hogql_query=True, modifiers=HogQLQueryModifiers( - personsOnEventsMode=PersonsOnEventsMode.person_id_no_override_properties_on_events + personsOnEventsMode=PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS ), ) self.assertEqual( @@ -749,7 +749,7 @@ def test_select_sample(self): context = HogQLContext( team_id=self.team.pk, enable_select_queries=True, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.v2), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.V2), ) query = self._select( "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons ON persons.id=events.person_id", @@ -772,7 +772,7 @@ def test_select_sample(self): context = HogQLContext( team_id=self.team.pk, enable_select_queries=True, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.v2), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.V2), ) self.assertEqual( self._select( @@ -795,7 +795,7 @@ def test_select_sample(self): context = HogQLContext( team_id=self.team.pk, enable_select_queries=True, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.v2), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.V2), ) expected = self._select( "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons ON persons.id=events.person_id", @@ -814,7 +814,7 @@ def test_select_sample(self): context = HogQLContext( team_id=self.team.pk, enable_select_queries=True, - modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.v2), + modifiers=HogQLQueryModifiers(personsArgMaxVersion=PersonsArgMaxVersion.V2), ) expected = self._select( "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons SAMPLE 0.1 ON persons.id=events.person_id", @@ -925,6 +925,14 @@ def test_window_functions_with_window(self): f"SELECT events.distinct_id AS distinct_id, min(toTimeZone(events.timestamp, %(hogql_val_0)s)) OVER win1 AS timestamp FROM events WHERE equals(events.team_id, {self.team.pk}) WINDOW win1 AS (PARTITION BY events.distinct_id ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) LIMIT {MAX_SELECT_RETURNED_ROWS}", ) + def test_window_functions_with_arg(self): + self.assertEqual( + self._select( + "SELECT quantiles(0.0, 0.25, 0.5, 0.75, 1.0)(distinct distinct_id) over () as values FROM events" + ), + f"SELECT quantiles(0.0, 0.25, 0.5, 0.75, 1.0)(events.distinct_id) OVER () AS values FROM events WHERE equals(events.team_id, {self.team.pk}) LIMIT 50000", + ) + def test_nullish_concat(self): self.assertEqual( self._expr("concat(null, 'a', 3, toString(4), toString(NULL))"), diff --git a/posthog/hogql/test/test_property.py b/posthog/hogql/test/test_property.py index 03c42efb3673b..7a4edb8a3db23 100644 --- a/posthog/hogql/test/test_property.py +++ b/posthog/hogql/test/test_property.py @@ -336,7 +336,7 @@ def test_property_to_expr_element(self): "operator": "exact", } ), - clear_locations(element_chain_key_filter("href", "href-text.", PropertyOperator.exact)), + clear_locations(element_chain_key_filter("href", "href-text.", PropertyOperator.EXACT)), ) self.assertEqual( self._property_to_expr( @@ -347,7 +347,7 @@ def test_property_to_expr_element(self): "operator": "regex", } ), - clear_locations(element_chain_key_filter("text", "text-text.", PropertyOperator.regex)), + clear_locations(element_chain_key_filter("text", "text-text.", PropertyOperator.REGEX)), ) def test_property_groups(self): @@ -503,35 +503,35 @@ def test_selector_to_expr(self): def test_elements_chain_key_filter(self): self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.is_set)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.IS_SET)), clear_locations(elements_chain_match('(href="[^"]+")')), ) self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.is_not_set)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.IS_NOT_SET)), clear_locations(not_call(elements_chain_match('(href="[^"]+")'))), ) self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.icontains)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.ICONTAINS)), clear_locations(elements_chain_imatch('(href="[^"]*boo\\.\\.[^"]*")')), ) self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.not_icontains)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.NOT_ICONTAINS)), clear_locations(not_call(elements_chain_imatch('(href="[^"]*boo\\.\\.[^"]*")'))), ) self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.regex)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.REGEX)), clear_locations(elements_chain_match('(href="boo..")')), ) self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.not_regex)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.NOT_REGEX)), clear_locations(not_call(elements_chain_match('(href="boo..")'))), ) self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.exact)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.EXACT)), clear_locations(elements_chain_match('(href="boo\\.\\.")')), ) self.assertEqual( - clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.is_not)), + clear_locations(element_chain_key_filter("href", "boo..", PropertyOperator.IS_NOT)), clear_locations(not_call(elements_chain_match('(href="boo\\.\\.")'))), ) diff --git a/posthog/hogql/transforms/property_types.py b/posthog/hogql/transforms/property_types.py index 58c8539533acd..26ab9fc8b3656 100644 --- a/posthog/hogql/transforms/property_types.py +++ b/posthog/hogql/transforms/property_types.py @@ -236,7 +236,7 @@ def _add_property_notice( ): property_name = str(node.chain[-1]) if property_type == "person": - if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.disabled: + if self.context.modifiers.personsOnEventsMode != PersonsOnEventsMode.DISABLED: materialized_column = self._get_materialized_column("events", property_name, "person_properties") else: materialized_column = self._get_materialized_column("person", property_name, "properties") diff --git a/posthog/hogql/transforms/test/test_in_cohort.py b/posthog/hogql/transforms/test/test_in_cohort.py index cde9e291c43c0..bdceb43df7932 100644 --- a/posthog/hogql/transforms/test/test_in_cohort.py +++ b/posthog/hogql/transforms/test/test_in_cohort.py @@ -48,7 +48,7 @@ def test_in_cohort_dynamic(self): response = execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT {cohort.pk} AND event='{random_uuid}'", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN), pretty=False, ) assert pretty_print_response_in_tests(response, self.team.pk) == self.snapshot # type: ignore @@ -65,7 +65,7 @@ def test_in_cohort_static(self): response = execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT {cohort.pk}", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN), pretty=False, ) assert pretty_print_response_in_tests(response, self.team.pk) == self.snapshot # type: ignore @@ -81,7 +81,7 @@ def test_in_cohort_strings(self): response = execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT 'my cohort'", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN), pretty=False, ) assert pretty_print_response_in_tests(response, self.team.pk) == self.snapshot # type: ignore @@ -93,7 +93,7 @@ def test_in_cohort_error(self): execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT true", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.subquery), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.SUBQUERY), pretty=False, ) self.assertEqual(str(e.exception), "cohort() takes exactly one string or integer argument") @@ -102,7 +102,7 @@ def test_in_cohort_error(self): execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT 'blabla'", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.subquery), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.SUBQUERY), pretty=False, ) self.assertEqual(str(e.exception), "Could not find a cohort with the name 'blabla'") @@ -118,7 +118,7 @@ def test_in_cohort_conjoined_string(self): response = execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT 'my cohort'", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin_conjoined), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN_CONJOINED), pretty=False, ) assert pretty_print_response_in_tests(response, self.team.pk) == self.snapshot # type: ignore @@ -133,7 +133,7 @@ def test_in_cohort_conjoined_int(self): response = execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT {cohort.pk}", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin_conjoined), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN_CONJOINED), pretty=False, ) assert pretty_print_response_in_tests(response, self.team.pk) == self.snapshot # type: ignore @@ -150,7 +150,7 @@ def test_in_cohort_conjoined_dynamic(self): response = execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT {cohort.pk} AND event='{random_uuid}'", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin_conjoined), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN_CONJOINED), pretty=False, ) assert pretty_print_response_in_tests(response, self.team.pk) == self.snapshot # type: ignore @@ -164,7 +164,7 @@ def test_in_cohort_conjoined_error(self): execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT true", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin_conjoined), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN_CONJOINED), pretty=False, ) self.assertEqual(str(e.exception), "cohort() takes exactly one string or integer argument") @@ -173,7 +173,7 @@ def test_in_cohort_conjoined_error(self): execute_hogql_query( f"SELECT event FROM events WHERE person_id IN COHORT 'blabla'", self.team, - modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.leftjoin_conjoined), + modifiers=HogQLQueryModifiers(inCohortVia=InCohortVia.LEFTJOIN_CONJOINED), pretty=False, ) self.assertEqual(str(e.exception), "Could not find a cohort with the name 'blabla'") diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 3ea762b786983..2b5cabb7d0d4e 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -142,7 +142,7 @@ def test_resolve_lazy_table_indirectly_referenced(self): # of a lazy join. printed = self._print_select( "select person.id from events", - HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.person_id_override_properties_joined), + HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED), ) assert printed == self.snapshot @@ -152,6 +152,6 @@ def test_resolve_lazy_table_indirect_duplicate_references(self): # is referenced via two different selected columns. printed = self._print_select( "select person_id, person.properties from events", - HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.person_id_override_properties_joined), + HogQLQueryModifiers(personsOnEventsMode=PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED), ) assert printed == self.snapshot diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index 84b34bddbf8c4..f4d8c6f308c20 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -246,8 +246,10 @@ def visit_window_expr(self, node: ast.WindowExpr): self.visit(node.frame_end) def visit_window_function(self, node: ast.WindowFunction): - for expr in node.args or []: + for expr in node.exprs or []: self.visit(expr) + for arg in node.args or []: + self.visit(arg) self.visit(node.over_expr) def visit_window_frame_expr(self, node: ast.WindowFrameExpr): @@ -553,7 +555,8 @@ def visit_window_function(self, node: ast.WindowFunction): end=None if self.clear_locations else node.end, type=None if self.clear_types else node.type, name=node.name, - args=[self.visit(expr) for expr in node.args] if node.args else None, + exprs=[self.visit(expr) for expr in node.exprs] if node.exprs else None, + args=[self.visit(arg) for arg in node.args] if node.args else None, over_expr=self.visit(node.over_expr) if node.over_expr else None, over_identifier=node.over_identifier, ) diff --git a/posthog/hogql_queries/apply_dashboard_filters.py b/posthog/hogql_queries/apply_dashboard_filters.py index 64302522586ea..6d8e74f0fb588 100644 --- a/posthog/hogql_queries/apply_dashboard_filters.py +++ b/posthog/hogql_queries/apply_dashboard_filters.py @@ -3,7 +3,7 @@ from posthog.models import Team from posthog.schema import DashboardFilter, NodeKind -WRAPPER_NODE_KINDS = [NodeKind.DataTableNode, NodeKind.DataVisualizationNode, NodeKind.InsightVizNode] +WRAPPER_NODE_KINDS = [NodeKind.DATA_TABLE_NODE, NodeKind.DATA_VISUALIZATION_NODE, NodeKind.INSIGHT_VIZ_NODE] # Apply the filters from the django-style Dashboard object diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index 104a2aa87d580..e673f59c467ef 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -119,7 +119,7 @@ def _get_breakdown_select_prop(self) -> list[ast.Expr]: prop_basic = ast.Alias(alias="prop_basic", expr=self._get_breakdown_expr()) # breakdown attribution - if breakdownAttributionType == BreakdownAttributionType.step: + if breakdownAttributionType == BreakdownAttributionType.STEP: select_columns = [] default_breakdown_selector = "[]" if self._query_has_array_breakdown() else "NULL" # get prop value from each step @@ -133,8 +133,8 @@ def _get_breakdown_select_prop(self) -> list[ast.Expr]: return [prop_basic, *select_columns, final_select, prop_window] elif breakdownAttributionType in [ - BreakdownAttributionType.first_touch, - BreakdownAttributionType.last_touch, + BreakdownAttributionType.FIRST_TOUCH, + BreakdownAttributionType.LAST_TOUCH, ]: prop_conditional = ( "notEmpty(arrayFilter(x -> notEmpty(x), prop))" @@ -143,7 +143,7 @@ def _get_breakdown_select_prop(self) -> list[ast.Expr]: ) aggregate_operation = ( - "argMinIf" if breakdownAttributionType == BreakdownAttributionType.first_touch else "argMaxIf" + "argMinIf" if breakdownAttributionType == BreakdownAttributionType.FIRST_TOUCH else "argMaxIf" ) breakdown_window_selector = f"{aggregate_operation}(prop, timestamp, {prop_conditional})" @@ -368,7 +368,7 @@ def _get_inner_event_query( funnel_events_query.select = [*funnel_events_query.select, *all_step_cols] - if breakdown and breakdownType == BreakdownType.cohort: + if breakdown and breakdownType == BreakdownType.COHORT: if funnel_events_query.select_from is None: raise ValidationError("Apologies, there was an error adding cohort breakdowns to the query.") funnel_events_query.select_from.next_join = self._get_cohort_breakdown_join() @@ -378,7 +378,7 @@ def _get_inner_event_query( steps_conditions = self._get_steps_conditions(length=len(entities_to_use)) funnel_events_query.where = ast.And(exprs=[funnel_events_query.where, steps_conditions]) - if breakdown and breakdownAttributionType != BreakdownAttributionType.all_events: + if breakdown and breakdownAttributionType != BreakdownAttributionType.ALL_EVENTS: # ALL_EVENTS attribution is the old default, which doesn't need the subquery return self._add_breakdown_attribution_subquery(funnel_events_query) @@ -425,8 +425,8 @@ def _add_breakdown_attribution_subquery(self, inner_query: ast.SelectQuery) -> a ) if breakdownAttributionType in [ - BreakdownAttributionType.first_touch, - BreakdownAttributionType.last_touch, + BreakdownAttributionType.FIRST_TOUCH, + BreakdownAttributionType.LAST_TOUCH, ]: # When breaking down by first/last touch, each person can only have one prop value # so just select that. Except for the empty case, where we select the default. @@ -987,9 +987,9 @@ def _get_step_counts_query(self, outer_select: list[ast.Expr], inner_select: lis *person_and_group_properties, ] if breakdown and breakdownType in [ - BreakdownType.person, - BreakdownType.event, - BreakdownType.group, + BreakdownType.PERSON, + BreakdownType.EVENT, + BreakdownType.GROUP, ]: time_fields = [ parse_expr(f"min(step_{i}_conversion_time) as step_{i}_conversion_time") for i in range(1, max_steps) diff --git a/posthog/hogql_queries/insights/funnels/funnel.py b/posthog/hogql_queries/insights/funnels/funnel.py index c44108dbc61ea..470979d32b26d 100644 --- a/posthog/hogql_queries/insights/funnels/funnel.py +++ b/posthog/hogql_queries/insights/funnels/funnel.py @@ -33,9 +33,9 @@ def get_query(self): max_steps = self.context.max_steps if self.context.breakdown and self.context.breakdownType in [ - BreakdownType.person, - BreakdownType.event, - BreakdownType.group, + BreakdownType.PERSON, + BreakdownType.EVENT, + BreakdownType.GROUP, ]: return self._breakdown_other_subquery() diff --git a/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py b/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py index fc6bc0cc5657c..33026970c1e2e 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py +++ b/posthog/hogql_queries/insights/funnels/funnel_correlation_query_runner.py @@ -303,7 +303,7 @@ def serialize_event_odds_ratio(self, odds_ratio: EventOddsRatio) -> EventOddsRat failure_count=odds_ratio["failure_count"], odds_ratio=odds_ratio["odds_ratio"], correlation_type=( - CorrelationType.success if odds_ratio["correlation_type"] == "success" else CorrelationType.failure + CorrelationType.SUCCESS if odds_ratio["correlation_type"] == "success" else CorrelationType.FAILURE ), event=event_definition, ) @@ -334,10 +334,10 @@ def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: Returns a query string and params, which are used to generate the contingency table. The query returns success and failure count for event / property values, along with total success and failure counts. """ - if self.query.funnelCorrelationType == FunnelCorrelationResultsType.properties: + if self.query.funnelCorrelationType == FunnelCorrelationResultsType.PROPERTIES: return self.get_properties_query() - if self.query.funnelCorrelationType == FunnelCorrelationResultsType.event_with_properties: + if self.query.funnelCorrelationType == FunnelCorrelationResultsType.EVENT_WITH_PROPERTIES: return self.get_event_property_query() return self.get_event_query() @@ -345,7 +345,7 @@ def to_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: def to_actors_query(self) -> ast.SelectQuery | ast.SelectUnionQuery: assert self.correlation_actors_query is not None - if self.query.funnelCorrelationType == FunnelCorrelationResultsType.properties: + if self.query.funnelCorrelationType == FunnelCorrelationResultsType.PROPERTIES: # Filtering on persons / groups properties can be pushed down to funnel events query if ( self.correlation_actors_query.funnelCorrelationPropertyValues @@ -841,7 +841,7 @@ def _get_funnel_step_names(self) -> list[str]: def properties_to_include(self) -> list[str]: props_to_include: list[str] = [] # TODO: implement or remove - # if self.query.funnelCorrelationType == FunnelCorrelationResultsType.properties: + # if self.query.funnelCorrelationType == FunnelCorrelationResultsType.PROPERTIES: # assert self.query.funnelCorrelationNames is not None # # When dealing with properties, make sure funnel response comes with properties @@ -859,7 +859,7 @@ def properties_to_include(self) -> list[str]: def support_autocapture_elements(self) -> bool: if ( - self.query.funnelCorrelationType == FunnelCorrelationResultsType.event_with_properties + self.query.funnelCorrelationType == FunnelCorrelationResultsType.EVENT_WITH_PROPERTIES and AUTOCAPTURE_EVENT in (self.query.funnelCorrelationEventNames or []) ): return True diff --git a/posthog/hogql_queries/insights/funnels/funnel_query_context.py b/posthog/hogql_queries/insights/funnels/funnel_query_context.py index 499dc3eb9ed4c..14ec8ba4d1624 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_query_context.py +++ b/posthog/hogql_queries/insights/funnels/funnel_query_context.py @@ -57,15 +57,15 @@ def __init__( self.breakdownFilter = self.query.breakdownFilter or BreakdownFilter() # defaults - self.interval = self.query.interval or IntervalType.day + self.interval = self.query.interval or IntervalType.DAY - self.breakdownType = self.breakdownFilter.breakdown_type or BreakdownType.event + self.breakdownType = self.breakdownFilter.breakdown_type or BreakdownType.EVENT self.breakdownAttributionType = ( - self.funnelsFilter.breakdownAttributionType or BreakdownAttributionType.first_touch + self.funnelsFilter.breakdownAttributionType or BreakdownAttributionType.FIRST_TOUCH ) self.funnelWindowInterval = self.funnelsFilter.funnelWindowInterval or 14 self.funnelWindowIntervalUnit = ( - self.funnelsFilter.funnelWindowIntervalUnit or FunnelConversionWindowTimeUnit.day + self.funnelsFilter.funnelWindowIntervalUnit or FunnelConversionWindowTimeUnit.DAY ) self.includeTimestamp = include_timestamp diff --git a/posthog/hogql_queries/insights/funnels/funnel_strict.py b/posthog/hogql_queries/insights/funnels/funnel_strict.py index a9e60361514e5..4bf9b5ce19ea1 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_strict.py +++ b/posthog/hogql_queries/insights/funnels/funnel_strict.py @@ -9,9 +9,9 @@ def get_query(self): max_steps = self.context.max_steps if self.context.breakdown and self.context.breakdownType in [ - BreakdownType.person, - BreakdownType.event, - BreakdownType.group, + BreakdownType.PERSON, + BreakdownType.EVENT, + BreakdownType.GROUP, ]: return self._breakdown_other_subquery() diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered.py b/posthog/hogql_queries/insights/funnels/funnel_unordered.py index 3cc68af391427..2bc3be1f8ca81 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_unordered.py +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered.py @@ -44,9 +44,9 @@ def get_query(self): raise ValidationError("Partial Exclusions not allowed in unordered funnels") if self.context.breakdown and self.context.breakdownType in [ - BreakdownType.person, - BreakdownType.event, - BreakdownType.group, + BreakdownType.PERSON, + BreakdownType.EVENT, + BreakdownType.GROUP, ]: return self._breakdown_other_subquery() diff --git a/posthog/hogql_queries/insights/funnels/funnels_query_runner.py b/posthog/hogql_queries/insights/funnels/funnels_query_runner.py index c6b8660132dab..1b85d8858302e 100644 --- a/posthog/hogql_queries/insights/funnels/funnels_query_runner.py +++ b/posthog/hogql_queries/insights/funnels/funnels_query_runner.py @@ -112,9 +112,9 @@ def funnel_order_class(self): def funnel_class(self): funnelVizType = self.context.funnelsFilter.funnelVizType - if funnelVizType == FunnelVizType.trends: + if funnelVizType == FunnelVizType.TRENDS: return FunnelTrends(context=self.context, **self.kwargs) - elif funnelVizType == FunnelVizType.time_to_convert: + elif funnelVizType == FunnelVizType.TIME_TO_CONVERT: return FunnelTimeToConvert(context=self.context) else: return self.funnel_order_class diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_correlation.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_correlation.py index bd5db4b9235a9..75daa35aa5cc5 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_correlation.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_correlation.py @@ -55,7 +55,7 @@ class TestClickhouseFunnelCorrelation(ClickhouseTestMixin, APIBaseTest): def _get_events_for_filters( self, filters, - funnelCorrelationType=FunnelCorrelationResultsType.events, + funnelCorrelationType=FunnelCorrelationResultsType.EVENTS, funnelCorrelationNames=None, funnelCorrelationExcludeNames=None, funnelCorrelationExcludeEventNames=None, @@ -90,10 +90,10 @@ def _get_actors_for_property( ): funnelCorrelationPropertyValues = [ ( - PersonPropertyFilter(key=prop, value=value, operator=PropertyOperator.exact) + PersonPropertyFilter(key=prop, value=value, operator=PropertyOperator.EXACT) if type == "person" else GroupPropertyFilter( - key=prop, value=value, group_type_index=group_type_index, operator=PropertyOperator.exact + key=prop, value=value, group_type_index=group_type_index, operator=PropertyOperator.EXACT ) ) for prop, value, type, group_type_index in property_values @@ -102,7 +102,7 @@ def _get_actors_for_property( serialized_actors = get_actors( filters, self.team, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=funnelCorrelationNames, funnelCorrelationPersonConverted=success, funnelCorrelationPropertyValues=funnelCorrelationPropertyValues, @@ -158,7 +158,7 @@ def test_basic_funnel_correlation_with_events(self): timestamp="2020-01-03T14:00:00Z", ) - result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.events) + result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.EVENTS) odds_ratios = [item.pop("odds_ratio") for item in result] expected_odds_ratios = [11, 1 / 11] @@ -200,7 +200,7 @@ def test_basic_funnel_correlation_with_events(self): # Now exclude positively_related result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.events, + funnelCorrelationType=FunnelCorrelationResultsType.EVENTS, funnelCorrelationExcludeEventNames=["positively_related"], ) @@ -417,7 +417,7 @@ def test_funnel_correlation_with_events_and_groups(self): "aggregation_group_type_index": 0, } - result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.events) + result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.EVENTS) odds_ratios = [item.pop("odds_ratio") for item in result] expected_odds_ratios = [12 / 7, 1 / 11] @@ -585,7 +585,7 @@ def test_basic_funnel_correlation_with_properties(self): ) result, _ = self._get_events_for_filters( - filters, funnelCorrelationType=FunnelCorrelationResultsType.properties, funnelCorrelationNames=["$browser"] + filters, funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["$browser"] ) odds_ratios = [item.pop("odds_ratio") for item in result] @@ -784,7 +784,7 @@ def test_funnel_correlation_with_properties_and_groups(self): } result, _ = self._get_events_for_filters( - filters, funnelCorrelationType=FunnelCorrelationResultsType.properties, funnelCorrelationNames=["industry"] + filters, funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["industry"] ) odds_ratios = [item.pop("odds_ratio") for item in result] @@ -852,7 +852,7 @@ def test_funnel_correlation_with_properties_and_groups(self): # test with `$all` as property # _run property correlation with filter on all properties new_result, _ = self._get_events_for_filters( - filters, funnelCorrelationType=FunnelCorrelationResultsType.properties, funnelCorrelationNames=["$all"] + filters, funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["$all"] ) odds_ratios = [item.pop("odds_ratio") for item in new_result] @@ -989,7 +989,7 @@ def test_funnel_correlation_with_properties_and_groups_person_on_events(self): with override_instance_config("PERSON_ON_EVENTS_ENABLED", True): result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["industry"], ) @@ -1055,7 +1055,7 @@ def test_funnel_correlation_with_properties_and_groups_person_on_events(self): # _run property correlation with filter on all properties new_result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["$all"], ) @@ -1193,14 +1193,14 @@ def test_correlation_with_properties_raises_validation_error(self): with self.assertRaises(ValidationError): self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, # funnelCorrelationNames=["$browser"] -- missing ) with self.assertRaises(ValidationError): self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.event_with_properties, + funnelCorrelationType=FunnelCorrelationResultsType.EVENT_WITH_PROPERTIES, # "funnelCorrelationEventNames": ["rick"] -- missing ) @@ -1306,7 +1306,7 @@ def test_correlation_with_multiple_properties(self): result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["$browser", "$nice"], ) @@ -1377,7 +1377,7 @@ def test_correlation_with_multiple_properties(self): # _run property correlation with filter on all properties new_result, _ = self._get_events_for_filters( - filters, funnelCorrelationType=FunnelCorrelationResultsType.properties, funnelCorrelationNames=["$all"] + filters, funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["$all"] ) odds_ratios = [item.pop("odds_ratio") for item in new_result] @@ -1396,7 +1396,7 @@ def test_correlation_with_multiple_properties(self): # search for $all but exclude $browser new_result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationNames=["$all"], funnelCorrelationExcludeNames=["$browser"], ) @@ -1506,7 +1506,7 @@ def test_discarding_insignificant_events(self): # Discard both due to % FunnelCorrelationQueryRunner.MIN_PERSON_PERCENTAGE = 0.11 FunnelCorrelationQueryRunner.MIN_PERSON_COUNT = 25 - result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.events) + result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.EVENTS) self.assertEqual(len(result), 2) @@ -1557,7 +1557,7 @@ def test_events_within_conversion_window_for_correlation(self): timestamp="2020-01-02T14:15:00Z", # event happened outside conversion window ) - result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.events) + result, _ = self._get_events_for_filters(filters, funnelCorrelationType=FunnelCorrelationResultsType.EVENTS) odds_ratios = [item.pop("odds_ratio") for item in result] expected_odds_ratios = [4] @@ -1637,7 +1637,7 @@ def test_funnel_correlation_with_event_properties(self): result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.event_with_properties, + funnelCorrelationType=FunnelCorrelationResultsType.EVENT_WITH_PROPERTIES, funnelCorrelationEventNames=[ "positively_related", "negatively_related", @@ -1682,7 +1682,7 @@ def test_funnel_correlation_with_event_properties(self): self._get_actors_for_event( filters, "positively_related", - [EventPropertyFilter(operator=PropertyOperator.exact, key="blah", value="value_bleh")], + [EventPropertyFilter(operator=PropertyOperator.EXACT, key="blah", value="value_bleh")], ) ), 5, @@ -1692,7 +1692,7 @@ def test_funnel_correlation_with_event_properties(self): self._get_actors_for_event( filters, "positively_related", - [EventPropertyFilter(operator=PropertyOperator.exact, key="signup_source", value="facebook")], + [EventPropertyFilter(operator=PropertyOperator.EXACT, key="signup_source", value="facebook")], ) ), 3, @@ -1702,7 +1702,7 @@ def test_funnel_correlation_with_event_properties(self): self._get_actors_for_event( filters, "positively_related", - [EventPropertyFilter(operator=PropertyOperator.exact, key="signup_source", value="facebook")], + [EventPropertyFilter(operator=PropertyOperator.EXACT, key="signup_source", value="facebook")], False, ) ), @@ -1713,7 +1713,7 @@ def test_funnel_correlation_with_event_properties(self): self._get_actors_for_event( filters, "negatively_related", - [EventPropertyFilter(operator=PropertyOperator.exact, key="signup_source", value="email")], + [EventPropertyFilter(operator=PropertyOperator.EXACT, key="signup_source", value="email")], False, ) ), @@ -1803,7 +1803,7 @@ def test_funnel_correlation_with_event_properties_and_groups(self): result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.event_with_properties, + funnelCorrelationType=FunnelCorrelationResultsType.EVENT_WITH_PROPERTIES, funnelCorrelationEventNames=[ "positively_related", "negatively_related", @@ -1888,7 +1888,7 @@ def test_funnel_correlation_with_event_properties_exclusions(self): result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.event_with_properties, + funnelCorrelationType=FunnelCorrelationResultsType.EVENT_WITH_PROPERTIES, funnelCorrelationEventNames=["positively_related"], funnelCorrelationEventExcludePropertyNames=["signup_source"], ) @@ -1912,7 +1912,7 @@ def test_funnel_correlation_with_event_properties_exclusions(self): self._get_actors_for_event( filters, "positively_related", - [EventPropertyFilter(operator=PropertyOperator.exact, key="blah", value="value_bleh")], + [EventPropertyFilter(operator=PropertyOperator.EXACT, key="blah", value="value_bleh")], ) ), 3, @@ -1924,7 +1924,7 @@ def test_funnel_correlation_with_event_properties_exclusions(self): self._get_actors_for_event( filters, "positively_related", - [EventPropertyFilter(operator=PropertyOperator.exact, key="signup_source", value="facebook")], + [EventPropertyFilter(operator=PropertyOperator.EXACT, key="signup_source", value="facebook")], ) ), 3, @@ -1996,7 +1996,7 @@ def test_funnel_correlation_with_event_properties_autocapture(self): result, _ = self._get_events_for_filters( filters, - funnelCorrelationType=FunnelCorrelationResultsType.event_with_properties, + funnelCorrelationType=FunnelCorrelationResultsType.EVENT_WITH_PROPERTIES, funnelCorrelationEventNames=["$autocapture"], ) diff --git a/posthog/hogql_queries/insights/funnels/test/test_funnel_correlations_persons.py b/posthog/hogql_queries/insights/funnels/test/test_funnel_correlations_persons.py index 02f65a468e4eb..a6694deed8330 100644 --- a/posthog/hogql_queries/insights/funnels/test/test_funnel_correlations_persons.py +++ b/posthog/hogql_queries/insights/funnels/test/test_funnel_correlations_persons.py @@ -39,7 +39,7 @@ def get_actors( filters: dict[str, Any], team: Team, - funnelCorrelationType: Optional[FunnelCorrelationResultsType] = FunnelCorrelationResultsType.events, + funnelCorrelationType: Optional[FunnelCorrelationResultsType] = FunnelCorrelationResultsType.EVENTS, funnelCorrelationNames=None, funnelCorrelationPersonConverted: Optional[bool] = None, funnelCorrelationPersonEntity: Optional[EventsNode] = None, @@ -50,7 +50,7 @@ def get_actors( funnel_actors_query = FunnelsActorsQuery(source=funnels_query, includeRecordings=includeRecordings) correlation_query = FunnelCorrelationQuery( source=funnel_actors_query, - funnelCorrelationType=(funnelCorrelationType or FunnelCorrelationResultsType.events), + funnelCorrelationType=(funnelCorrelationType or FunnelCorrelationResultsType.EVENTS), funnelCorrelationNames=funnelCorrelationNames, # funnelCorrelationExcludeNames=funnelCorrelationExcludeNames, # funnelCorrelationExcludeEventNames=funnelCorrelationExcludeEventNames, @@ -466,7 +466,7 @@ def test_funnel_correlation_on_properties_with_recordings(self): results = get_actors( filters, self.team, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationPersonConverted=True, funnelCorrelationPropertyValues=[ { @@ -579,7 +579,7 @@ def test_strict_funnel_correlation_with_recordings(self): results = get_actors( filters, self.team, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationPersonConverted=True, funnelCorrelationPropertyValues=[ { @@ -613,7 +613,7 @@ def test_strict_funnel_correlation_with_recordings(self): results = get_actors( filters, self.team, - funnelCorrelationType=FunnelCorrelationResultsType.properties, + funnelCorrelationType=FunnelCorrelationResultsType.PROPERTIES, funnelCorrelationPersonConverted=False, funnelCorrelationPropertyValues=[ { diff --git a/posthog/hogql_queries/insights/funnels/utils.py b/posthog/hogql_queries/insights/funnels/utils.py index 6da16512bdceb..d5c968a913494 100644 --- a/posthog/hogql_queries/insights/funnels/utils.py +++ b/posthog/hogql_queries/insights/funnels/utils.py @@ -12,9 +12,9 @@ def get_funnel_order_class(funnelsFilter: FunnelsFilter): FunnelUnordered, ) - if funnelsFilter.funnelOrderType == StepOrderValue.unordered: + if funnelsFilter.funnelOrderType == StepOrderValue.UNORDERED: return FunnelUnordered - elif funnelsFilter.funnelOrderType == StepOrderValue.strict: + elif funnelsFilter.funnelOrderType == StepOrderValue.STRICT: return FunnelStrict return Funnel @@ -27,12 +27,12 @@ def get_funnel_actor_class(funnelsFilter: FunnelsFilter): FunnelTrendsActors, ) - if funnelsFilter.funnelVizType == FunnelVizType.trends: + if funnelsFilter.funnelVizType == FunnelVizType.TRENDS: return FunnelTrendsActors else: - if funnelsFilter.funnelOrderType == StepOrderValue.unordered: + if funnelsFilter.funnelOrderType == StepOrderValue.UNORDERED: return FunnelUnorderedActors - elif funnelsFilter.funnelOrderType == StepOrderValue.strict: + elif funnelsFilter.funnelOrderType == StepOrderValue.STRICT: return FunnelStrictActors else: return FunnelActors diff --git a/posthog/hogql_queries/insights/paths_query_runner.py b/posthog/hogql_queries/insights/paths_query_runner.py index a0c3ba92e5e95..30185e28f8923 100644 --- a/posthog/hogql_queries/insights/paths_query_runner.py +++ b/posthog/hogql_queries/insights/paths_query_runner.py @@ -82,13 +82,13 @@ def _get_event_query(self) -> list[ast.Expr]: if not self.query.pathsFilter.includeEventTypes: return [] - if PathType.field_pageview in self.query.pathsFilter.includeEventTypes: + if PathType.FIELD_PAGEVIEW in self.query.pathsFilter.includeEventTypes: or_conditions.append(parse_expr("event = {event}", {"event": ast.Constant(value=PAGEVIEW_EVENT)})) - if PathType.field_screen in self.query.pathsFilter.includeEventTypes: + if PathType.FIELD_SCREEN in self.query.pathsFilter.includeEventTypes: or_conditions.append(parse_expr("event = {event}", {"event": ast.Constant(value=SCREEN_EVENT)})) - if PathType.custom_event in self.query.pathsFilter.includeEventTypes: + if PathType.CUSTOM_EVENT in self.query.pathsFilter.includeEventTypes: or_conditions.append(parse_expr("NOT startsWith(events.event, '$')")) if or_conditions: @@ -146,8 +146,8 @@ def handle_funnel(self) -> tuple[list, Optional[ast.Expr]]: funnelSourceFilter = funnelSource.funnelsFilter or FunnelsFilter() if funnelPathType in ( - FunnelPathType.funnel_path_after_step, - FunnelPathType.funnel_path_before_step, + FunnelPathType.FUNNEL_PATH_AFTER_STEP, + FunnelPathType.FUNNEL_PATH_BEFORE_STEP, ): funnel_fields = [ ast.Alias(alias="target_timestamp", expr=ast.Field(chain=["funnel_actors", "timestamp"])), @@ -155,15 +155,15 @@ def handle_funnel(self) -> tuple[list, Optional[ast.Expr]]: interval = funnelSourceFilter.funnelWindowInterval or 14 unit = funnelSourceFilter.funnelWindowIntervalUnit interval_unit = funnel_window_interval_unit_to_sql(unit) - operator = ">=" if funnelPathType == FunnelPathType.funnel_path_after_step else "<=" + operator = ">=" if funnelPathType == FunnelPathType.FUNNEL_PATH_AFTER_STEP else "<=" default_case = f"events.timestamp {operator} toTimeZone({{target_timestamp}}, 'UTC')" - if funnelPathType == FunnelPathType.funnel_path_after_step and funnelStep and funnelStep < 0: + if funnelPathType == FunnelPathType.FUNNEL_PATH_AFTER_STEP and funnelStep and funnelStep < 0: default_case += f" + INTERVAL {interval} {interval_unit}" event_filter = parse_expr( default_case, {"target_timestamp": ast.Field(chain=["funnel_actors", "timestamp"])} ) return funnel_fields, event_filter - elif funnelPathType == FunnelPathType.funnel_path_between_steps: + elif funnelPathType == FunnelPathType.FUNNEL_PATH_BETWEEN_STEPS: funnel_fields = [ ast.Alias(alias="min_timestamp", expr=ast.Field(chain=["funnel_actors", "min_timestamp"])), ast.Alias(alias="max_timestamp", expr=ast.Field(chain=["funnel_actors", "max_timestamp"])), @@ -210,11 +210,11 @@ def funnel_join(self) -> ast.JoinExpr: assert isinstance(actors_query_runner.source_runner, FunnelsQueryRunner) assert actors_query_runner.source_runner.context is not None actors_query_runner.source_runner.context.includeTimestamp = funnelPathType in ( - FunnelPathType.funnel_path_after_step, - FunnelPathType.funnel_path_before_step, + FunnelPathType.FUNNEL_PATH_AFTER_STEP, + FunnelPathType.FUNNEL_PATH_BEFORE_STEP, ) actors_query_runner.source_runner.context.includePrecedingTimestamp = ( - funnelPathType == FunnelPathType.funnel_path_between_steps + funnelPathType == FunnelPathType.FUNNEL_PATH_BETWEEN_STEPS ) actors_query = actors_query_runner.to_query() @@ -530,7 +530,7 @@ def get_session_threshold_clause(self) -> ast.Expr: funnelSourceFilter = self.query.funnelPathsFilter.funnelSource.funnelsFilter or FunnelsFilter() interval = 14 - interval_unit = FunnelConversionWindowTimeUnit.day + interval_unit = FunnelConversionWindowTimeUnit.DAY if funnelSourceFilter.funnelWindowInterval: interval = funnelSourceFilter.funnelWindowInterval diff --git a/posthog/hogql_queries/insights/retention_query_runner.py b/posthog/hogql_queries/insights/retention_query_runner.py index c069c475262a2..7bbd39e6c14ff 100644 --- a/posthog/hogql_queries/insights/retention_query_runner.py +++ b/posthog/hogql_queries/insights/retention_query_runner.py @@ -86,7 +86,7 @@ def filter_timestamp(self) -> ast.Expr: ) def _get_events_for_entity(self, entity: RetentionEntity) -> list[str | None]: - if entity.type == EntityType.actions and entity.id: + if entity.type == EntityType.ACTIONS and entity.id: action = Action.objects.get(pk=int(entity.id)) return action.get_step_events() return [entity.id] if isinstance(entity.id, str) else [None] @@ -131,7 +131,7 @@ def actor_query(self, breakdown_values_filter: Optional[int] = None) -> ast.Sele event_query_type = ( RetentionQueryType.TARGET_FIRST_TIME - if self.query.retentionFilter.retentionType == RetentionType.retention_first_time + if self.query.retentionFilter.retentionType == RetentionType.RETENTION_FIRST_TIME else RetentionQueryType.TARGET ) @@ -290,13 +290,15 @@ def actor_query(self, breakdown_values_filter: Optional[int] = None) -> ast.Sele select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), where=ast.And(exprs=event_filters), group_by=[ast.Field(chain=["actor_id"])], - having=ast.CompareOperation( - op=ast.CompareOperationOp.Eq, - left=ast.Field(chain=["breakdown_values"]), - right=ast.Constant(value=breakdown_values_filter), - ) - if breakdown_values_filter is not None - else None, + having=( + ast.CompareOperation( + op=ast.CompareOperationOp.Eq, + left=ast.Field(chain=["breakdown_values"]), + right=ast.Constant(value=breakdown_values_filter), + ) + if breakdown_values_filter is not None + else None + ), ) if self.query.samplingFactor is not None and isinstance(self.query.samplingFactor, float): inner_query.select_from.sample = ast.SampleExpr( diff --git a/posthog/hogql_queries/insights/test/test_lifecycle_query_runner.py b/posthog/hogql_queries/insights/test/test_lifecycle_query_runner.py index 4818966fbf2ee..258f4bed96859 100644 --- a/posthog/hogql_queries/insights/test/test_lifecycle_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_lifecycle_query_runner.py @@ -106,7 +106,7 @@ def test_lifecycle_query_group_0(self): date_from = "2020-01-09" date_to = "2020-01-19" - response = self._run_lifecycle_query(date_from, date_to, IntervalType.day, 0) + response = self._run_lifecycle_query(date_from, date_to, IntervalType.DAY, 0) statuses = [res["status"] for res in response.results] self.assertEqual(["new", "returning", "resurrecting", "dormant"], statuses) @@ -357,7 +357,7 @@ def test_lifecycle_query_group_1(self): date_from = "2020-01-09" date_to = "2020-01-19" - response = self._run_lifecycle_query(date_from, date_to, IntervalType.day, 1) + response = self._run_lifecycle_query(date_from, date_to, IntervalType.DAY, 1) statuses = [res["status"] for res in response.results] self.assertEqual(["new", "returning", "resurrecting", "dormant"], statuses) @@ -670,7 +670,7 @@ def test_lifecycle_query_whole_range(self): date_from = "2020-01-09" date_to = "2020-01-19" - response = self._run_lifecycle_query(date_from, date_to, IntervalType.day) + response = self._run_lifecycle_query(date_from, date_to, IntervalType.DAY) statuses = [res["status"] for res in response.results] self.assertEqual(["new", "returning", "resurrecting", "dormant"], statuses) @@ -891,7 +891,7 @@ def test_events_query_whole_range(self): date_from = "2020-01-09" date_to = "2020-01-19" - response = self._run_events_query(date_from, date_to, IntervalType.day) + response = self._run_events_query(date_from, date_to, IntervalType.DAY) self.assertEqual( { @@ -919,7 +919,7 @@ def test_events_query_partial_range(self): self._create_test_events() date_from = "2020-01-12" date_to = "2020-01-14" - response = self._run_events_query(date_from, date_to, IntervalType.day) + response = self._run_events_query(date_from, date_to, IntervalType.DAY) self.assertEqual( { @@ -959,7 +959,7 @@ def test_lifecycle_trend(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], ), ) @@ -1009,7 +1009,7 @@ def test_lifecycle_trend_all_events(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event=None)], ), ) @@ -1073,7 +1073,7 @@ def test_lifecycle_trend_with_zero_person_ids(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], ), ) @@ -1174,9 +1174,9 @@ def test_lifecycle_trend_prop_filtering(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], - properties=[EventPropertyFilter(key="$number", value="1", operator=PropertyOperator.exact)], + properties=[EventPropertyFilter(key="$number", value="1", operator=PropertyOperator.EXACT)], ), ) .calculate() @@ -1199,11 +1199,11 @@ def test_lifecycle_trend_prop_filtering(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[ EventsNode( event="$pageview", - properties=[EventPropertyFilter(key="$number", value="1", operator=PropertyOperator.exact)], + properties=[EventPropertyFilter(key="$number", value="1", operator=PropertyOperator.EXACT)], ) ], ), @@ -1305,11 +1305,11 @@ def test_lifecycle_trend_person_prop_filtering(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[ EventsNode( event="$pageview", - properties=[PersonPropertyFilter(key="name", value="p1", operator=PropertyOperator.exact)], + properties=[PersonPropertyFilter(key="name", value="p1", operator=PropertyOperator.EXACT)], ) ], ), @@ -1374,7 +1374,7 @@ def test_lifecycle_trends_distinct_id_repeat(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], ), ) @@ -1419,7 +1419,7 @@ def test_lifecycle_trend_action(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[ActionsNode(id=pageview_action.pk)], ), ) @@ -1463,7 +1463,7 @@ def test_lifecycle_trend_all_time(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="all"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], ), ) @@ -1510,7 +1510,7 @@ def test_lifecycle_trend_weeks_sunday(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-02-05T00:00:00Z", date_to="2020-03-09T00:00:00Z"), - interval=IntervalType.week, + interval=IntervalType.WEEK, series=[EventsNode(event="$pageview")], ), ) @@ -1569,7 +1569,7 @@ def test_lifecycle_trend_weeks_monday(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-02-05T00:00:00Z", date_to="2020-03-09T00:00:00Z"), - interval=IntervalType.week, + interval=IntervalType.WEEK, series=[EventsNode(event="$pageview")], ), ) @@ -1624,7 +1624,7 @@ def test_lifecycle_trend_months(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-02-01T00:00:00Z", date_to="2020-09-01T00:00:00Z"), - interval=IntervalType.month, + interval=IntervalType.MONTH, series=[EventsNode(event="$pageview")], ), ) @@ -1667,7 +1667,7 @@ def test_filter_test_accounts(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], filterTestAccounts=True, ), @@ -1712,7 +1712,7 @@ def test_timezones(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12", date_to="2020-01-19"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], ), ) @@ -1738,7 +1738,7 @@ def test_timezones(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12", date_to="2020-01-19"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], ), ) @@ -1785,7 +1785,7 @@ def test_sampling(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], samplingFactor=0.1, ), @@ -1832,7 +1832,7 @@ def test_cohort_filter(self): team=self.team, query=LifecycleQuery( dateRange=InsightDateRange(date_from="2020-01-12T00:00:00Z", date_to="2020-01-19T00:00:00Z"), - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], properties=[CohortPropertyFilter(value=cohort.pk)], ), diff --git a/posthog/hogql_queries/insights/test/test_paginators.py b/posthog/hogql_queries/insights/test/test_paginators.py index d76d6ff2fcf01..06108117bd95a 100644 --- a/posthog/hogql_queries/insights/test/test_paginators.py +++ b/posthog/hogql_queries/insights/test/test_paginators.py @@ -112,7 +112,7 @@ def test_empty_result_set(self): select=["properties.email"], limit=10, properties=[ - PersonPropertyFilter(key="email", value="random", operator=PropertyOperator.exact), + PersonPropertyFilter(key="email", value="random", operator=PropertyOperator.EXACT), ], ) ) diff --git a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py index e9dace1280091..f9a2cbc17a038 100644 --- a/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py +++ b/posthog/hogql_queries/insights/test/test_stickiness_query_runner.py @@ -27,7 +27,7 @@ PersonPropertyFilter, PropertyGroupFilter, PropertyOperator, - RecordingDurationFilter, + RecordingPropertyFilter, SessionPropertyFilter, StickinessFilter, StickinessQuery, @@ -58,7 +58,7 @@ class SeriesTestData: ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -205,7 +205,7 @@ def _run_query( query_series: list[EventsNode | ActionsNode] = [EventsNode(event="$pageview")] if series is None else series query_date_from = date_from or self.default_date_from query_date_to = None if date_to == "now" else date_to or self.default_date_to - query_interval = interval or IntervalType.day + query_interval = interval or IntervalType.DAY query = StickinessQuery( series=query_series, @@ -276,7 +276,7 @@ def test_labels(self): def test_interval_hour(self): self._create_test_events() - response = self._run_query(interval=IntervalType.hour, date_from="2020-01-11", date_to="2020-01-12") + response = self._run_query(interval=IntervalType.HOUR, date_from="2020-01-11", date_to="2020-01-12") result = response.results[0] @@ -293,7 +293,7 @@ def test_interval_hour_last_days(self): self._create_test_events() with freeze_time("2020-01-20T12:00:00Z"): - response = self._run_query(interval=IntervalType.hour, date_from="-2d", date_to="now") + response = self._run_query(interval=IntervalType.HOUR, date_from="-2d", date_to="now") result = response.results[0] # 61 = 48 + 12 + 1 hours_labels = [f"{hour + 1} hour{'' if hour == 0 else 's'}" for hour in range(61)] @@ -309,7 +309,7 @@ def test_interval_hour_last_days(self): def test_interval_day(self): self._create_test_events() - response = self._run_query(interval=IntervalType.day) + response = self._run_query(interval=IntervalType.DAY) result = response.results[0] @@ -343,7 +343,7 @@ def test_interval_day(self): def test_interval_week(self): self._create_test_events() - response = self._run_query(interval=IntervalType.week) + response = self._run_query(interval=IntervalType.WEEK) result = response.results[0] @@ -356,7 +356,7 @@ def test_interval_full_weeks(self): self._create_test_events() with freeze_time("2020-01-23T12:00:00Z"): - response = self._run_query(interval=IntervalType.week, date_from="-30d", date_to="now") + response = self._run_query(interval=IntervalType.WEEK, date_from="-30d", date_to="now") result = response.results[0] @@ -368,7 +368,7 @@ def test_interval_full_weeks(self): def test_interval_month(self): self._create_test_events() - response = self._run_query(interval=IntervalType.month) + response = self._run_query(interval=IntervalType.MONTH) result = response.results[0] @@ -381,7 +381,7 @@ def test_property_filtering(self): self._create_test_events() response = self._run_query( - properties=[EventPropertyFilter(key="$browser", operator=PropertyOperator.exact, value="Chrome")] + properties=[EventPropertyFilter(key="$browser", operator=PropertyOperator.EXACT, value="Chrome")] ) result = response.results[0] @@ -425,7 +425,7 @@ def test_event_filtering(self): series: list[EventsNode | ActionsNode] = [ EventsNode( event="$pageview", - properties=[EventPropertyFilter(key="$browser", operator=PropertyOperator.exact, value="Chrome")], + properties=[EventPropertyFilter(key="$browser", operator=PropertyOperator.EXACT, value="Chrome")], ) ] @@ -545,7 +545,7 @@ def test_group_aggregations(self): self._create_test_events() series: list[EventsNode | ActionsNode] = [ - EventsNode(event="$pageview", math="unique_group", math_group_type_index=MathGroupTypeIndex.number_0) + EventsNode(event="$pageview", math="unique_group", math_group_type_index=MathGroupTypeIndex.NUMBER_0) ] response = self._run_query(series=series) diff --git a/posthog/hogql_queries/insights/trends/aggregation_operations.py b/posthog/hogql_queries/insights/trends/aggregation_operations.py index f339faf823865..075599cb9beec 100644 --- a/posthog/hogql_queries/insights/trends/aggregation_operations.py +++ b/posthog/hogql_queries/insights/trends/aggregation_operations.py @@ -326,8 +326,8 @@ def _events_query( ) -> ast.SelectQuery | ast.SelectUnionQuery: date_from_with_lookback = "{date_from} - {inclusive_lookback}" if self.chart_display_type in NON_TIME_SERIES_DISPLAY_TYPES and self.series.math in ( - BaseMathType.weekly_active, - BaseMathType.monthly_active, + BaseMathType.WEEKLY_ACTIVE, + BaseMathType.MONTHLY_ACTIVE, ): # TRICKY: On total value (non-time-series) insights, WAU/MAU math is simply meaningless. # There's no intuitive way to define the semantics of such a combination, so what we do is just turn it diff --git a/posthog/hogql_queries/insights/trends/breakdown.py b/posthog/hogql_queries/insights/trends/breakdown.py index b62a157bfc24d..49491429cf54f 100644 --- a/posthog/hogql_queries/insights/trends/breakdown.py +++ b/posthog/hogql_queries/insights/trends/breakdown.py @@ -81,7 +81,7 @@ def column_expr(self) -> ast.Alias: return ast.Alias(alias="breakdown_value", expr=self._get_breakdown_histogram_multi_if()) if self.query.breakdownFilter.breakdown_type == "cohort": - if self.modifiers.inCohortVia == InCohortVia.leftjoin_conjoined: + if self.modifiers.inCohortVia == InCohortVia.LEFTJOIN_CONJOINED: return ast.Alias( alias="breakdown_value", expr=hogql_to_string(ast.Field(chain=["__in_cohort", "cohort_id"])), diff --git a/posthog/hogql_queries/insights/trends/breakdown_values.py b/posthog/hogql_queries/insights/trends/breakdown_values.py index cc04637d5e6ec..aee02dd9ccefb 100644 --- a/posthog/hogql_queries/insights/trends/breakdown_values.py +++ b/posthog/hogql_queries/insights/trends/breakdown_values.py @@ -113,7 +113,7 @@ def get_breakdown_values(self) -> list[str | int]: select_field.expr = ast.Call(name="toString", args=[select_field.expr]) - if self.chart_display_type == ChartDisplayType.WorldMap: + if self.chart_display_type == ChartDisplayType.WORLD_MAP: breakdown_limit = BREAKDOWN_VALUES_LIMIT_FOR_COUNTRIES else: breakdown_limit = int(self.breakdown_limit) diff --git a/posthog/hogql_queries/insights/trends/display.py b/posthog/hogql_queries/insights/trends/display.py index de0cd18f8e79a..4a1229e7ba9c2 100644 --- a/posthog/hogql_queries/insights/trends/display.py +++ b/posthog/hogql_queries/insights/trends/display.py @@ -10,26 +10,26 @@ def __init__(self, display_type: ChartDisplayType | None) -> None: if display_type: self.display_type = display_type else: - self.display_type = ChartDisplayType.ActionsLineGraph + self.display_type = ChartDisplayType.ACTIONS_LINE_GRAPH # No time range def is_total_value(self) -> bool: return ( - self.display_type == ChartDisplayType.BoldNumber - or self.display_type == ChartDisplayType.ActionsPie - or self.display_type == ChartDisplayType.ActionsBarValue - or self.display_type == ChartDisplayType.WorldMap - or self.display_type == ChartDisplayType.ActionsTable + self.display_type == ChartDisplayType.BOLD_NUMBER + or self.display_type == ChartDisplayType.ACTIONS_PIE + or self.display_type == ChartDisplayType.ACTIONS_BAR_VALUE + or self.display_type == ChartDisplayType.WORLD_MAP + or self.display_type == ChartDisplayType.ACTIONS_TABLE ) def wrap_inner_query(self, inner_query: ast.SelectQuery, breakdown_enabled: bool) -> ast.SelectQuery: - if self.display_type == ChartDisplayType.ActionsLineGraphCumulative: + if self.display_type == ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE: return self._get_cumulative_query(inner_query, breakdown_enabled) return inner_query def should_wrap_inner_query(self) -> bool: - return self.display_type == ChartDisplayType.ActionsLineGraphCumulative + return self.display_type == ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE def _build_aggregate_dates(self, dates_queries: ast.SelectUnionQuery) -> ast.Expr: return parse_select( @@ -81,7 +81,7 @@ def _get_cumulative_query(self, inner_query: ast.SelectQuery, breakdown_enabled: alias="count", expr=ast.WindowFunction( name="sum", - args=[ast.Field(chain=["count"])], + exprs=[ast.Field(chain=["count"])], over_expr=window_expr, ), ), diff --git a/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py b/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py index 6dfb40247f364..2195ca2a344f5 100644 --- a/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py +++ b/posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py @@ -64,26 +64,26 @@ def test_replace_select_from(self): @pytest.mark.parametrize( "math,math_property", [ - [BaseMathType.total, None], - [BaseMathType.dau, None], - [BaseMathType.weekly_active, None], - [BaseMathType.monthly_active, None], - [BaseMathType.unique_session, None], - [PropertyMathType.avg, "$browser"], - [PropertyMathType.sum, "$browser"], - [PropertyMathType.min, "$browser"], - [PropertyMathType.max, "$browser"], - [PropertyMathType.median, "$browser"], - [PropertyMathType.p90, "$browser"], - [PropertyMathType.p95, "$browser"], - [PropertyMathType.p99, "$browser"], - [CountPerActorMathType.avg_count_per_actor, None], - [CountPerActorMathType.min_count_per_actor, None], - [CountPerActorMathType.max_count_per_actor, None], - [CountPerActorMathType.median_count_per_actor, None], - [CountPerActorMathType.p90_count_per_actor, None], - [CountPerActorMathType.p95_count_per_actor, None], - [CountPerActorMathType.p99_count_per_actor, None], + [BaseMathType.TOTAL, None], + [BaseMathType.DAU, None], + [BaseMathType.WEEKLY_ACTIVE, None], + [BaseMathType.MONTHLY_ACTIVE, None], + [BaseMathType.UNIQUE_SESSION, None], + [PropertyMathType.AVG, "$browser"], + [PropertyMathType.SUM, "$browser"], + [PropertyMathType.MIN, "$browser"], + [PropertyMathType.MAX, "$browser"], + [PropertyMathType.MEDIAN, "$browser"], + [PropertyMathType.P90, "$browser"], + [PropertyMathType.P95, "$browser"], + [PropertyMathType.P99, "$browser"], + [CountPerActorMathType.AVG_COUNT_PER_ACTOR, None], + [CountPerActorMathType.MIN_COUNT_PER_ACTOR, None], + [CountPerActorMathType.MAX_COUNT_PER_ACTOR, None], + [CountPerActorMathType.MEDIAN_COUNT_PER_ACTOR, None], + [CountPerActorMathType.P90_COUNT_PER_ACTOR, None], + [CountPerActorMathType.P95_COUNT_PER_ACTOR, None], + [CountPerActorMathType.P99_COUNT_PER_ACTOR, None], ["hogql", None], ], ) @@ -102,7 +102,7 @@ def test_all_cases_return( series = EventsNode(event="$pageview", math=math, math_property=math_property) query_date_range = QueryDateRange(date_range=None, interval=None, now=datetime.now(), team=team) - agg_ops = AggregationOperations(team, series, ChartDisplayType.ActionsLineGraph, query_date_range, False) + agg_ops = AggregationOperations(team, series, ChartDisplayType.ACTIONS_LINE_GRAPH, query_date_range, False) res = agg_ops.select_aggregation() assert isinstance(res, ast.Expr) @@ -110,26 +110,26 @@ def test_all_cases_return( @pytest.mark.parametrize( "math,result", [ - [BaseMathType.total, False], - [BaseMathType.dau, False], - [BaseMathType.weekly_active, True], - [BaseMathType.monthly_active, True], - [BaseMathType.unique_session, False], - [PropertyMathType.avg, False], - [PropertyMathType.sum, False], - [PropertyMathType.min, False], - [PropertyMathType.max, False], - [PropertyMathType.median, False], - [PropertyMathType.p90, False], - [PropertyMathType.p95, False], - [PropertyMathType.p99, False], - [CountPerActorMathType.avg_count_per_actor, True], - [CountPerActorMathType.min_count_per_actor, True], - [CountPerActorMathType.max_count_per_actor, True], - [CountPerActorMathType.median_count_per_actor, True], - [CountPerActorMathType.p90_count_per_actor, True], - [CountPerActorMathType.p95_count_per_actor, True], - [CountPerActorMathType.p99_count_per_actor, True], + [BaseMathType.TOTAL, False], + [BaseMathType.DAU, False], + [BaseMathType.WEEKLY_ACTIVE, True], + [BaseMathType.MONTHLY_ACTIVE, True], + [BaseMathType.UNIQUE_SESSION, False], + [PropertyMathType.AVG, False], + [PropertyMathType.SUM, False], + [PropertyMathType.MIN, False], + [PropertyMathType.MAX, False], + [PropertyMathType.MEDIAN, False], + [PropertyMathType.P90, False], + [PropertyMathType.P95, False], + [PropertyMathType.P99, False], + [CountPerActorMathType.AVG_COUNT_PER_ACTOR, True], + [CountPerActorMathType.MIN_COUNT_PER_ACTOR, True], + [CountPerActorMathType.MAX_COUNT_PER_ACTOR, True], + [CountPerActorMathType.MEDIAN_COUNT_PER_ACTOR, True], + [CountPerActorMathType.P90_COUNT_PER_ACTOR, True], + [CountPerActorMathType.P95_COUNT_PER_ACTOR, True], + [CountPerActorMathType.P99_COUNT_PER_ACTOR, True], ["hogql", False], ], ) @@ -147,6 +147,6 @@ def test_requiring_query_orchestration( series = EventsNode(event="$pageview", math=math) query_date_range = QueryDateRange(date_range=None, interval=None, now=datetime.now(), team=team) - agg_ops = AggregationOperations(team, series, ChartDisplayType.ActionsLineGraph, query_date_range, False) + agg_ops = AggregationOperations(team, series, ChartDisplayType.ACTIONS_LINE_GRAPH, query_date_range, False) res = agg_ops.requires_query_orchestration() assert res == result diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_actors_query_builder.py b/posthog/hogql_queries/insights/trends/test/test_trends_actors_query_builder.py index 91d5c95a77f3c..ab9761f445441 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_actors_query_builder.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_actors_query_builder.py @@ -102,7 +102,7 @@ def test_date_range_with_timezone(self): def test_date_range_hourly(self): self.team.timezone = "Europe/Berlin" - trends_query = default_query.model_copy(update={"interval": IntervalType.hour}, deep=True) + trends_query = default_query.model_copy(update={"interval": IntervalType.HOUR}, deep=True) self.assertEqual( self._get_date_where_sql(trends_query=trends_query, time_frame="2023-05-08T15:00:00"), @@ -114,12 +114,12 @@ def test_date_range_compare_previous(self): trends_query = default_query.model_copy(update={"trendsFilter": TrendsFilter(compare=True)}, deep=True) self.assertEqual( - self._get_date_where_sql(trends_query=trends_query, time_frame="2023-05-10", compare_value=Compare.current), + self._get_date_where_sql(trends_query=trends_query, time_frame="2023-05-10", compare_value=Compare.CURRENT), "greaterOrEquals(timestamp, toDateTime('2023-05-09 22:00:00.000000')), less(timestamp, toDateTime('2023-05-10 22:00:00.000000'))", ) self.assertEqual( self._get_date_where_sql( - trends_query=trends_query, time_frame="2023-05-10", compare_value=Compare.previous + trends_query=trends_query, time_frame="2023-05-10", compare_value=Compare.PREVIOUS ), "greaterOrEquals(timestamp, toDateTime('2023-05-02 22:00:00.000000')), less(timestamp, toDateTime('2023-05-03 22:00:00.000000'))", ) @@ -127,17 +127,17 @@ def test_date_range_compare_previous(self): def test_date_range_compare_previous_hourly(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( - update={"trendsFilter": TrendsFilter(compare=True), "interval": IntervalType.hour}, deep=True + update={"trendsFilter": TrendsFilter(compare=True), "interval": IntervalType.HOUR}, deep=True ) self.assertEqual( self._get_date_where_sql( - trends_query=trends_query, time_frame="2023-05-10T15:00:00", compare_value=Compare.current + trends_query=trends_query, time_frame="2023-05-10T15:00:00", compare_value=Compare.CURRENT ), "greaterOrEquals(timestamp, toDateTime('2023-05-10 13:00:00.000000')), less(timestamp, toDateTime('2023-05-10 14:00:00.000000'))", ) self.assertEqual( self._get_date_where_sql( - trends_query=trends_query, time_frame="2023-05-10T15:00:00", compare_value=Compare.previous + trends_query=trends_query, time_frame="2023-05-10T15:00:00", compare_value=Compare.PREVIOUS ), "greaterOrEquals(timestamp, toDateTime('2023-05-03 13:00:00.000000')), less(timestamp, toDateTime('2023-05-03 14:00:00.000000'))", ) @@ -145,7 +145,7 @@ def test_date_range_compare_previous_hourly(self): def test_date_range_total_value(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( - update={"trendsFilter": TrendsFilter(display=ChartDisplayType.BoldNumber)}, deep=True + update={"trendsFilter": TrendsFilter(display=ChartDisplayType.BOLD_NUMBER)}, deep=True ) with freeze_time("2022-06-15T12:00:00.000Z"): @@ -157,23 +157,23 @@ def test_date_range_total_value(self): def test_date_range_total_value_compare_previous(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( - update={"trendsFilter": TrendsFilter(display=ChartDisplayType.BoldNumber, compare=True)}, deep=True + update={"trendsFilter": TrendsFilter(display=ChartDisplayType.BOLD_NUMBER, compare=True)}, deep=True ) with freeze_time("2022-06-15T12:00:00.000Z"): self.assertEqual( - self._get_date_where_sql(trends_query=trends_query, compare_value=Compare.current), + self._get_date_where_sql(trends_query=trends_query, compare_value=Compare.CURRENT), "greaterOrEquals(timestamp, toDateTime('2022-06-07 22:00:00.000000')), lessOrEquals(timestamp, toDateTime('2022-06-15 21:59:59.999999'))", ) self.assertEqual( - self._get_date_where_sql(trends_query=trends_query, compare_value=Compare.previous), + self._get_date_where_sql(trends_query=trends_query, compare_value=Compare.PREVIOUS), "greaterOrEquals(timestamp, toDateTime('2022-05-31 22:00:00.000000')), lessOrEquals(timestamp, toDateTime('2022-06-08 21:59:59.999999'))", ) def test_date_range_weekly_active_users_math(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( - update={"series": [EventsNode(event="$pageview", math=BaseMathType.weekly_active)]}, deep=True + update={"series": [EventsNode(event="$pageview", math=BaseMathType.WEEKLY_ACTIVE)]}, deep=True ) with freeze_time("2024-05-30T12:00:00.000Z"): @@ -186,7 +186,7 @@ def test_date_range_weekly_active_users_math_compare_previous(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( update={ - "series": [EventsNode(event="$pageview", math=BaseMathType.weekly_active)], + "series": [EventsNode(event="$pageview", math=BaseMathType.WEEKLY_ACTIVE)], "trendsFilter": TrendsFilter(compare=True), }, deep=True, @@ -195,13 +195,13 @@ def test_date_range_weekly_active_users_math_compare_previous(self): with freeze_time("2024-05-30T12:00:00.000Z"): self.assertEqual( self._get_date_where_sql( - trends_query=trends_query, time_frame="2024-05-27", compare_value=Compare.current + trends_query=trends_query, time_frame="2024-05-27", compare_value=Compare.CURRENT ), "greaterOrEquals(timestamp, minus(toDateTime('2024-05-26 22:00:00.000000'), toIntervalDay(6))), less(timestamp, toDateTime('2024-05-27 22:00:00.000000'))", ) self.assertEqual( self._get_date_where_sql( - trends_query=trends_query, time_frame="2024-05-27", compare_value=Compare.previous + trends_query=trends_query, time_frame="2024-05-27", compare_value=Compare.PREVIOUS ), "greaterOrEquals(timestamp, minus(toDateTime('2024-05-19 22:00:00.000000'), toIntervalDay(6))), less(timestamp, toDateTime('2024-05-20 22:00:00.000000'))", ) @@ -210,8 +210,8 @@ def test_date_range_weekly_active_users_math_total_value(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( update={ - "series": [EventsNode(event="$pageview", math=BaseMathType.weekly_active)], - "trendsFilter": TrendsFilter(display=ChartDisplayType.BoldNumber), + "series": [EventsNode(event="$pageview", math=BaseMathType.WEEKLY_ACTIVE)], + "trendsFilter": TrendsFilter(display=ChartDisplayType.BOLD_NUMBER), }, deep=True, ) @@ -226,22 +226,22 @@ def test_date_range_weekly_active_users_math_total_value_compare_previous(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( update={ - "series": [EventsNode(event="$pageview", math=BaseMathType.weekly_active)], - "trendsFilter": TrendsFilter(compare=True, display=ChartDisplayType.BoldNumber), + "series": [EventsNode(event="$pageview", math=BaseMathType.WEEKLY_ACTIVE)], + "trendsFilter": TrendsFilter(compare=True, display=ChartDisplayType.BOLD_NUMBER), }, deep=True, ) with freeze_time("2024-05-30T12:00:00.000Z"): self.assertEqual( - self._get_date_where_sql(trends_query=trends_query, compare_value=Compare.previous), + self._get_date_where_sql(trends_query=trends_query, compare_value=Compare.PREVIOUS), "greaterOrEquals(timestamp, minus(toDateTime('2024-05-23 21:59:59.999999'), toIntervalDay(6))), lessOrEquals(timestamp, toDateTime('2024-05-23 21:59:59.999999'))", ) def test_date_range_monthly_active_users_math(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( - update={"series": [EventsNode(event="$pageview", math=BaseMathType.monthly_active)]}, deep=True + update={"series": [EventsNode(event="$pageview", math=BaseMathType.MONTHLY_ACTIVE)]}, deep=True ) with freeze_time("2024-05-30T12:00:00.000Z"): @@ -284,7 +284,7 @@ def test_date_range_explicit_monthly_active_users_math(self): self.team.timezone = "Europe/Berlin" trends_query = default_query.model_copy( update={ - "series": [EventsNode(event="$pageview", math=BaseMathType.monthly_active)], + "series": [EventsNode(event="$pageview", math=BaseMathType.MONTHLY_ACTIVE)], "dateRange": InsightDateRange( date_from="2024-05-08T14:29:13.634000Z", date_to="2024-05-08T14:32:57.692000Z", explicitDate=True ), diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_dashboard_filters.py b/posthog/hogql_queries/insights/trends/test/test_trends_dashboard_filters.py index b14e5fad7bd4c..db0879c188fac 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_dashboard_filters.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_dashboard_filters.py @@ -52,7 +52,7 @@ def test_empty_dashboard_filters_change_nothing(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, None, ) @@ -75,7 +75,7 @@ def test_date_from_override_updates_whole_date_range(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, None, ) @@ -98,7 +98,7 @@ def test_date_from_and_date_to_override_updates_whole_date_range(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, None, ) @@ -121,7 +121,7 @@ def test_properties_set_when_no_filters_present(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, None, ) @@ -146,7 +146,7 @@ def test_properties_list_extends_filters_list(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, None, properties=[EventPropertyFilter(key="abc", value="foo", operator="exact")], ) @@ -165,16 +165,16 @@ def test_properties_list_extends_filters_list(self): assert query_runner.query.dateRange.date_from == "2020-01-09" assert query_runner.query.dateRange.date_to == "2020-01-20" assert query_runner.query.properties == PropertyGroupFilter( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ EventPropertyFilter(key="abc", value="foo", operator="exact"), ], ), PropertyGroupFilterValue( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ EventPropertyFilter(key="xyz", value="bar", operator="regex"), ], @@ -188,17 +188,17 @@ def test_properties_list_extends_filters_group(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, None, properties=PropertyGroupFilter( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[EventPropertyFilter(key="abc", value="foo", operator="exact")], ), PropertyGroupFilterValue( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[EventPropertyFilter(key="klm", value="foo", operator="exact")], ), ], @@ -209,14 +209,14 @@ def test_properties_list_extends_filters_group(self): assert query_runner.query.dateRange.date_from == "2020-01-09" assert query_runner.query.dateRange.date_to == "2020-01-20" assert query_runner.query.properties == PropertyGroupFilter( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[EventPropertyFilter(key="abc", value="foo", operator="exact")], ), PropertyGroupFilterValue( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[EventPropertyFilter(key="klm", value="foo", operator="exact")], ), ], @@ -235,23 +235,23 @@ def test_properties_list_extends_filters_group(self): assert query_runner.query.dateRange.date_from == "2020-01-09" assert query_runner.query.dateRange.date_to == "2020-01-20" assert query_runner.query.properties == PropertyGroupFilter( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[ PropertyGroupFilterValue( - type=FilterLogicalOperator.OR, + type=FilterLogicalOperator.OR_, values=[EventPropertyFilter(key="abc", value="foo", operator="exact")], ), PropertyGroupFilterValue( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[EventPropertyFilter(key="klm", value="foo", operator="exact")], ), ], ), PropertyGroupFilterValue( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[EventPropertyFilter(key="xyz", value="bar", operator="regex")], ), ], @@ -263,7 +263,7 @@ def test_breakdown_limit_is_removed_when_too_large_for_dashboard(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, None, breakdown=BreakdownFilter(breakdown="abc", breakdown_limit=5), ) @@ -296,7 +296,7 @@ def test_compare_is_removed_for_all_time_range(self): query_runner = self._create_query_runner( "2024-07-07", "2024-07-14", - IntervalType.day, + IntervalType.DAY, None, trends_filters=TrendsFilter(compare=True), ) diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_data_warehouse_query.py b/posthog/hogql_queries/insights/trends/test/test_trends_data_warehouse_query.py index a606d560cf130..e05045c28ff51 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_data_warehouse_query.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_data_warehouse_query.py @@ -241,7 +241,7 @@ def test_trends_breakdown(self): timestamp_field="created", ) ], - breakdownFilter=BreakdownFilter(breakdown_type=BreakdownType.data_warehouse, breakdown="prop_1"), + breakdownFilter=BreakdownFilter(breakdown_type=BreakdownType.DATA_WAREHOUSE, breakdown="prop_1"), ) with freeze_time("2023-01-07"): @@ -279,7 +279,7 @@ def test_trends_breakdown_with_property(self): ) ], properties=clean_entity_properties([{"key": "prop_1", "value": "a", "type": "data_warehouse"}]), - breakdownFilter=BreakdownFilter(breakdown_type=BreakdownType.data_warehouse, breakdown="prop_1"), + breakdownFilter=BreakdownFilter(breakdown_type=BreakdownType.DATA_WAREHOUSE, breakdown="prop_1"), ) with freeze_time("2023-01-07"): @@ -317,11 +317,11 @@ def assert_column_names_with_display_type(self, display_type: ChartDisplayType): assert set(response.columns).issubset({"date", "total"}) def test_column_names_with_display_type(self): - self.assert_column_names_with_display_type(ChartDisplayType.ActionsAreaGraph) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsBar) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsBarValue) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsLineGraph) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsPie) - self.assert_column_names_with_display_type(ChartDisplayType.BoldNumber) - self.assert_column_names_with_display_type(ChartDisplayType.WorldMap) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsLineGraphCumulative) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_AREA_GRAPH) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_BAR) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_BAR_VALUE) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_LINE_GRAPH) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_PIE) + self.assert_column_names_with_display_type(ChartDisplayType.BOLD_NUMBER) + self.assert_column_names_with_display_type(ChartDisplayType.WORLD_MAP) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE) diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_persons.py b/posthog/hogql_queries/insights/trends/test/test_trends_persons.py index 27ac8f3b2da6b..34cde171dfa4f 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_persons.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_persons.py @@ -277,7 +277,7 @@ def test_trends_person_breakdown_persons(self): source_query = TrendsQuery( series=[EventsNode(event="$pageview")], dateRange=InsightDateRange(date_from="-7d"), - breakdownFilter=BreakdownFilter(breakdown="$geoip_country_code", breakdown_type=BreakdownType.person), + breakdownFilter=BreakdownFilter(breakdown="$geoip_country_code", breakdown_type=BreakdownType.PERSON), ) result = self._get_actors(trends_query=source_query, day="2023-05-01", breakdown="DE") @@ -338,7 +338,7 @@ def test_trends_breakdown_hogql_persons(self): source_query = TrendsQuery( series=[EventsNode(event="$pageview")], dateRange=InsightDateRange(date_from="-7d"), - breakdownFilter=BreakdownFilter(breakdown="properties.some_property", breakdown_type=BreakdownType.hogql), + breakdownFilter=BreakdownFilter(breakdown="properties.some_property", breakdown_type=BreakdownType.HOGQL), ) result = self._get_actors(trends_query=source_query, day="2023-05-01", breakdown=20) @@ -361,7 +361,7 @@ def test_trends_cohort_breakdown_persons(self): source_query = TrendsQuery( series=[EventsNode(event="$pageview")], dateRange=InsightDateRange(date_from="-7d"), - breakdownFilter=BreakdownFilter(breakdown=[cohort.pk], breakdown_type=BreakdownType.cohort), + breakdownFilter=BreakdownFilter(breakdown=[cohort.pk], breakdown_type=BreakdownType.COHORT), ) result = self._get_actors(trends_query=source_query, day="2023-05-01", breakdown=cohort.pk) @@ -387,7 +387,7 @@ def test_trends_multi_cohort_breakdown_persons(self): source_query = TrendsQuery( series=[EventsNode(event="$pageview")], dateRange=InsightDateRange(date_from="-7d"), - breakdownFilter=BreakdownFilter(breakdown=[cohort1.pk, cohort2.pk], breakdown_type=BreakdownType.cohort), + breakdownFilter=BreakdownFilter(breakdown=[cohort1.pk, cohort2.pk], breakdown_type=BreakdownType.COHORT), ) result = self._get_actors(trends_query=source_query, day="2023-05-01", breakdown=cohort1.pk) @@ -414,7 +414,7 @@ def trends_all_cohort_breakdown_persons(self, inCohortVia: str): source_query = TrendsQuery( series=[EventsNode(event="$pageview")], dateRange=InsightDateRange(date_from="-7d"), - breakdownFilter=BreakdownFilter(breakdown=[cohort1.pk, "all"], breakdown_type=BreakdownType.cohort), + breakdownFilter=BreakdownFilter(breakdown=[cohort1.pk, "all"], breakdown_type=BreakdownType.COHORT), ) source_query.modifiers = HogQLQueryModifiers(inCohortVia=inCohortVia) @@ -458,7 +458,7 @@ def test_trends_math_weekly_active_persons(self): team=self.team, ) source_query = TrendsQuery( - series=[EventsNode(event="$pageview", math=BaseMathType.weekly_active)], + series=[EventsNode(event="$pageview", math=BaseMathType.WEEKLY_ACTIVE)], dateRange=InsightDateRange(date_from="-7d"), ) @@ -473,7 +473,7 @@ def test_trends_math_weekly_active_persons(self): def test_trends_math_property_sum_persons(self): self._create_events() source_query = TrendsQuery( - series=[EventsNode(event="$pageview", math=PropertyMathType.sum, math_property="some_property")], + series=[EventsNode(event="$pageview", math=PropertyMathType.SUM, math_property="some_property")], dateRange=InsightDateRange(date_from="-7d"), ) @@ -493,7 +493,7 @@ def test_trends_math_count_per_actor_persons(self): source_query = TrendsQuery( series=[ EventsNode( - event="$pageview", math=CountPerActorMathType.max_count_per_actor, math_property="some_property" + event="$pageview", math=CountPerActorMathType.MAX_COUNT_PER_ACTOR, math_property="some_property" ) ], dateRange=InsightDateRange(date_from="-7d"), @@ -537,7 +537,7 @@ def test_trends_math_group_persons(self): ) source_query = TrendsQuery( series=[ - EventsNode(event="$pageview", math="unique_group", math_group_type_index=MathGroupTypeIndex.number_0) + EventsNode(event="$pageview", math="unique_group", math_group_type_index=MathGroupTypeIndex.NUMBER_0) ], dateRange=InsightDateRange(date_from="-7d"), ) @@ -570,7 +570,7 @@ def test_trends_math_group_persons_filters_empty(self): ) source_query = TrendsQuery( series=[ - EventsNode(event="$pageview", math="unique_group", math_group_type_index=MathGroupTypeIndex.number_0) + EventsNode(event="$pageview", math="unique_group", math_group_type_index=MathGroupTypeIndex.NUMBER_0) ], dateRange=InsightDateRange(date_from="-7d"), ) @@ -586,7 +586,7 @@ def test_trends_total_value_persons(self): source_query = TrendsQuery( series=[EventsNode(event="$pageview")], dateRange=InsightDateRange(date_from="-7d"), - trendsFilter=TrendsFilter(display=ChartDisplayType.BoldNumber), + trendsFilter=TrendsFilter(display=ChartDisplayType.BOLD_NUMBER), ) with freeze_time("2023-05-01T20:00:00.000Z"): @@ -607,13 +607,13 @@ def test_trends_compare_persons(self): trendsFilter=TrendsFilter(compare=True), ) - result = self._get_actors(trends_query=source_query, day="2023-05-06", compare=Compare.current) + result = self._get_actors(trends_query=source_query, day="2023-05-06", compare=Compare.CURRENT) self.assertEqual(len(result), 1) self.assertEqual(get_distinct_id(result[0]), "person1") self.assertEqual(get_event_count(result[0]), 1) - result = self._get_actors(trends_query=source_query, day="2023-05-06", compare=Compare.previous) + result = self._get_actors(trends_query=source_query, day="2023-05-06", compare=Compare.PREVIOUS) self.assertEqual(len(result), 2) self.assertEqual(get_distinct_id(result[0]), "person2") diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_builder.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_builder.py index 3b9b69fa289c2..4826813cb54de 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_query_builder.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_builder.py @@ -74,7 +74,7 @@ def test_column_names(self): trends_query = TrendsQuery( kind="TrendsQuery", dateRange=InsightDateRange(date_from="2023-01-01"), - series=[EventsNode(event="$pageview", math=BaseMathType.total)], + series=[EventsNode(event="$pageview", math=BaseMathType.TOTAL)], ) response = self.get_response(trends_query) @@ -101,7 +101,7 @@ def assert_column_names_with_display_type_and_breakdowns(self, display_type: Cha dateRange=InsightDateRange(date_from="2023-01-01"), series=[EventsNode(event="$pageview")], trendsFilter=TrendsFilter(display=display_type), - breakdownFilter=BreakdownFilter(breakdown="$geoip_country_code", breakdown_type=BreakdownType.event), + breakdownFilter=BreakdownFilter(breakdown="$geoip_country_code", breakdown_type=BreakdownType.EVENT), ) response = self.get_response(trends_query) @@ -110,20 +110,20 @@ def assert_column_names_with_display_type_and_breakdowns(self, display_type: Cha assert set(response.columns).issubset({"date", "total", "breakdown_value"}) def test_column_names_with_display_type(self): - self.assert_column_names_with_display_type(ChartDisplayType.ActionsAreaGraph) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsBar) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsBarValue) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsLineGraph) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsPie) - self.assert_column_names_with_display_type(ChartDisplayType.BoldNumber) - self.assert_column_names_with_display_type(ChartDisplayType.WorldMap) - self.assert_column_names_with_display_type(ChartDisplayType.ActionsLineGraphCumulative) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_AREA_GRAPH) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_BAR) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_BAR_VALUE) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_LINE_GRAPH) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_PIE) + self.assert_column_names_with_display_type(ChartDisplayType.BOLD_NUMBER) + self.assert_column_names_with_display_type(ChartDisplayType.WORLD_MAP) + self.assert_column_names_with_display_type(ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE) def test_column_names_with_display_type_and_breakdowns(self): - self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ActionsAreaGraph) - self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ActionsBar) - self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ActionsBarValue) - self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ActionsLineGraph) - self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ActionsPie) - self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.WorldMap) - self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ActionsLineGraphCumulative) + self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ACTIONS_AREA_GRAPH) + self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ACTIONS_BAR) + self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ACTIONS_BAR_VALUE) + self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ACTIONS_LINE_GRAPH) + self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ACTIONS_PIE) + self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.WORLD_MAP) + self.assert_column_names_with_display_type_and_breakdowns(ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE) diff --git a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py index d3f92ba85c253..98a75f49d4a6a 100644 --- a/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/test/test_trends_query_runner.py @@ -224,7 +224,7 @@ def test_trends_label(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, None, None, None, @@ -238,7 +238,7 @@ def test_trends_count(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, None, None, None, @@ -252,7 +252,7 @@ def test_trends_data(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, None, None, None, @@ -266,7 +266,7 @@ def test_trends_days(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, None, None, None, @@ -295,7 +295,7 @@ def test_trends_labels(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, None, None, None, @@ -324,7 +324,7 @@ def test_trends_labels_hour(self): response = self._run_trends_query( self.default_date_from, self.default_date_from, - IntervalType.hour, + IntervalType.HOUR, [EventsNode(event="$pageview")], ) @@ -342,7 +342,7 @@ def test_trends_multiple_series(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], ) @@ -363,7 +363,7 @@ def test_formula(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter(formula="A+2*B"), ) @@ -379,11 +379,11 @@ def test_formula_total_value(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter( formula="A+2*B", - display=ChartDisplayType.BoldNumber, # total value + display=ChartDisplayType.BOLD_NUMBER, # total value ), ) self.assertEqual(1, len(response.results)) @@ -398,7 +398,7 @@ def test_formula_with_compare(self): response = self._run_trends_query( "2020-01-15", "2020-01-19", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter(formula="A+2*B", compare=True), ) @@ -426,11 +426,11 @@ def test_formula_with_compare_total_value(self): response = self._run_trends_query( "2020-01-15", "2020-01-19", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter( formula="A+2*B", - display=ChartDisplayType.BoldNumber, # total value + display=ChartDisplayType.BOLD_NUMBER, # total value compare=True, ), ) @@ -457,10 +457,10 @@ def test_formula_with_breakdown(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter(formula="A+2*B"), - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) # one for each breakdown value @@ -485,10 +485,10 @@ def test_formula_with_breakdown_and_compare(self): response = self._run_trends_query( "2020-01-15", "2020-01-19", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter(formula="A+2*B", compare=True), - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) # chrome, ff and edge for previous, and chrome and safari for current @@ -515,14 +515,14 @@ def test_formula_with_breakdown_and_compare_total_value(self): response = self._run_trends_query( "2020-01-15", "2020-01-19", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter( formula="A+2*B", - display=ChartDisplayType.BoldNumber, # total value + display=ChartDisplayType.BOLD_NUMBER, # total value compare=True, ), - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) # chrome, ff and edge for previous, and chrome and safari for current @@ -582,10 +582,10 @@ def test_formula_with_multi_cohort_breakdown(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter(formula="A+B"), - BreakdownFilter(breakdown_type=BreakdownType.cohort, breakdown=[cohort1.pk, cohort2.pk]), + BreakdownFilter(breakdown_type=BreakdownType.COHORT, breakdown=[cohort1.pk, cohort2.pk]), ) assert len(response.results) == 2 @@ -624,10 +624,10 @@ def test_formula_with_multi_cohort_all_breakdown(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter(formula="A+B"), - BreakdownFilter(breakdown_type=BreakdownType.cohort, breakdown=[cohort1.pk, "all"]), + BreakdownFilter(breakdown_type=BreakdownType.COHORT, breakdown=[cohort1.pk, "all"]), ) assert len(response.results) == 2 @@ -651,10 +651,10 @@ def test_formula_with_breakdown_and_no_data(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageviewxxx"), EventsNode(event="$pageleavexxx")], TrendsFilter(formula="A+2*B"), - BreakdownFilter(breakdown_type=BreakdownType.person, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.PERSON, breakdown="$browser"), ) self.assertEqual(0, len(response.results)) @@ -662,10 +662,10 @@ def test_formula_with_breakdown_and_no_data(self): response = self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleavexxx")], TrendsFilter(formula="A+2*B"), - BreakdownFilter(breakdown_type=BreakdownType.person, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.PERSON, breakdown="$browser"), ) self.assertEqual([1, 0, 1, 3, 1, 0, 2, 0, 1, 0, 1], response.results[0]["data"]) @@ -676,10 +676,10 @@ def test_breakdown_is_context_aware(self, mock_sync_execute: MagicMock): self._run_trends_query( self.default_date_from, self.default_date_to, - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageviewxxx"), EventsNode(event="$pageleavexxx")], TrendsFilter(formula="A+2*B"), - BreakdownFilter(breakdown_type=BreakdownType.person, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.PERSON, breakdown="$browser"), limit_context=LimitContext.QUERY_ASYNC, ) @@ -693,7 +693,7 @@ def test_trends_compare(self): response = self._run_trends_query( "2020-01-15", "2020-01-19", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], TrendsFilter(compare=True), ) @@ -737,7 +737,7 @@ def test_trends_compare_weeks(self): response = self._run_trends_query( "-7d", None, - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], TrendsFilter(compare=True), ) @@ -790,10 +790,10 @@ def test_trends_breakdowns(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) breakdown_labels = [result["breakdown_value"] for result in response.results] @@ -815,10 +815,10 @@ def test_trends_breakdowns_boolean(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="bool_field"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="bool_field"), ) breakdown_labels = [result["breakdown_value"] for result in response.results] @@ -838,11 +838,11 @@ def test_trends_breakdowns_histogram(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, BreakdownFilter( - breakdown_type=BreakdownType.event, + breakdown_type=BreakdownType.EVENT, breakdown="prop", breakdown_histogram_bin_count=4, ), @@ -885,10 +885,10 @@ def test_trends_breakdowns_cohort(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.cohort, breakdown=[cohort.pk]), + BreakdownFilter(breakdown_type=BreakdownType.COHORT, breakdown=[cohort.pk]), ) assert len(response.results) == 1 @@ -916,10 +916,10 @@ def test_trends_breakdowns_hogql(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.hogql, breakdown="properties.$browser"), + BreakdownFilter(breakdown_type=BreakdownType.HOGQL, breakdown="properties.$browser"), ) breakdown_labels = [result["breakdown_value"] for result in response.results] @@ -941,10 +941,10 @@ def test_trends_breakdowns_multiple_hogql(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], None, - BreakdownFilter(breakdown_type=BreakdownType.hogql, breakdown="properties.$browser"), + BreakdownFilter(breakdown_type=BreakdownType.HOGQL, breakdown="properties.$browser"), ) breakdown_labels = [result["breakdown_value"] for result in response.results] @@ -974,10 +974,10 @@ def test_trends_breakdowns_and_compare(self): response = self._run_trends_query( "2020-01-15", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], TrendsFilter(compare=True), - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) breakdown_labels = [result["breakdown_value"] for result in response.results] @@ -1021,10 +1021,10 @@ def test_trends_breakdown_and_aggregation_query_orchestration(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=PropertyMathType.sum, math_property="prop")], + IntervalType.DAY, + [EventsNode(event="$pageview", math=PropertyMathType.SUM, math_property="prop")], None, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) breakdown_labels = [result["breakdown_value"] for result in response.results] @@ -1100,7 +1100,7 @@ def test_trends_aggregation_hogql(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview", math="hogql", math_hogql="sum(properties.prop)")], None, None, @@ -1128,8 +1128,8 @@ def test_trends_aggregation_total(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=BaseMathType.total)], + IntervalType.DAY, + [EventsNode(event="$pageview", math=BaseMathType.TOTAL)], None, None, ) @@ -1143,8 +1143,8 @@ def test_trends_aggregation_dau(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=BaseMathType.dau)], + IntervalType.DAY, + [EventsNode(event="$pageview", math=BaseMathType.DAU)], None, None, ) @@ -1158,8 +1158,8 @@ def test_trends_aggregation_wau(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=BaseMathType.weekly_active)], + IntervalType.DAY, + [EventsNode(event="$pageview", math=BaseMathType.WEEKLY_ACTIVE)], None, None, ) @@ -1173,8 +1173,8 @@ def test_trends_aggregation_mau(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=BaseMathType.monthly_active)], + IntervalType.DAY, + [EventsNode(event="$pageview", math=BaseMathType.MONTHLY_ACTIVE)], None, None, ) @@ -1188,8 +1188,8 @@ def test_trends_aggregation_unique(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=BaseMathType.unique_session)], + IntervalType.DAY, + [EventsNode(event="$pageview", math=BaseMathType.UNIQUE_SESSION)], None, None, ) @@ -1203,8 +1203,8 @@ def test_trends_aggregation_property_sum(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=PropertyMathType.sum, math_property="prop")], + IntervalType.DAY, + [EventsNode(event="$pageview", math=PropertyMathType.SUM, math_property="prop")], None, None, ) @@ -1231,8 +1231,8 @@ def test_trends_aggregation_property_avg(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=PropertyMathType.avg, math_property="prop")], + IntervalType.DAY, + [EventsNode(event="$pageview", math=PropertyMathType.AVG, math_property="prop")], None, None, ) @@ -1259,8 +1259,8 @@ def test_trends_aggregation_per_actor_max(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, - [EventsNode(event="$pageview", math=CountPerActorMathType.max_count_per_actor)], + IntervalType.DAY, + [EventsNode(event="$pageview", math=CountPerActorMathType.MAX_COUNT_PER_ACTOR)], None, None, ) @@ -1287,9 +1287,9 @@ def test_trends_display_aggregate(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.BoldNumber), + TrendsFilter(display=ChartDisplayType.BOLD_NUMBER), None, ) @@ -1305,9 +1305,9 @@ def test_trends_display_cumulative(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsLineGraphCumulative), + TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE), None, ) @@ -1343,10 +1343,10 @@ def test_breakdown_values_limit(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsLineGraph), - BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event), + TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.EVENT), ) self.assertEqual(len(response.results), 26) @@ -1354,20 +1354,20 @@ def test_breakdown_values_limit(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsLineGraph), - BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event, breakdown_limit=10), + TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.EVENT, breakdown_limit=10), ) self.assertEqual(len(response.results), 11) response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsLineGraph), - BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event), + TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.EVENT), limit_context=LimitContext.EXPORT, ) self.assertEqual(len(response.results), 30) @@ -1386,10 +1386,10 @@ def test_breakdown_values_unknown_property(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsLineGraph), - BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event), + TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.EVENT), ) self.assertEqual(len(response.results), 26) @@ -1397,10 +1397,10 @@ def test_breakdown_values_unknown_property(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsLineGraph), - BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event, breakdown_limit=10), + TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.EVENT, breakdown_limit=10), ) self.assertEqual(len(response.results), 11) @@ -1419,10 +1419,10 @@ def test_breakdown_values_world_map_limit(self): query_runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.WorldMap), - BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.event), + TrendsFilter(display=ChartDisplayType.WORLD_MAP), + BreakdownFilter(breakdown="breakdown_value", breakdown_type=BreakdownType.EVENT), ) query = query_runner.to_queries()[0] assert isinstance(query, ast.SelectQuery) and query.limit == ast.Constant(value=MAX_SELECT_RETURNED_ROWS) @@ -1436,9 +1436,9 @@ def test_previous_period_with_number_display(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.BoldNumber, compare=True), + TrendsFilter(display=ChartDisplayType.BOLD_NUMBER, compare=True), None, ) @@ -1477,7 +1477,7 @@ def test_formula_rounding(self): response = self._run_trends_query( "2020-01-11T00:00:00Z", "2020-01-11T23:59:59Z", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], TrendsFilter(formula="B/A"), ) @@ -1508,7 +1508,7 @@ def test_properties_filtering_with_materialized_columns_and_empty_string_as_prop response = self._run_trends_query( date_from="2020-01-11T00:00:00Z", date_to="2020-01-11T23:59:59Z", - interval=IntervalType.day, + interval=IntervalType.DAY, series=[EventsNode(event="$pageview")], filter_test_accounts=True, ) @@ -1521,7 +1521,7 @@ def test_smoothing(self): response = self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], TrendsFilter(smoothingIntervals=7), None, @@ -1574,14 +1574,14 @@ def test_cohort_modifier(self, patch_create_default_modifiers_for_team): self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.cohort, breakdown=[cohort1.pk, cohort2.pk]), + BreakdownFilter(breakdown_type=BreakdownType.COHORT, breakdown=[cohort1.pk, cohort2.pk]), hogql_modifiers=modifiers, ) - assert modifiers.inCohortVia == InCohortVia.leftjoin_conjoined + assert modifiers.inCohortVia == InCohortVia.LEFTJOIN_CONJOINED @patch("posthog.hogql_queries.query_runner.create_default_modifiers_for_team") def test_cohort_modifier_with_all_cohort(self, patch_create_default_modifiers_for_team): @@ -1628,14 +1628,14 @@ def test_cohort_modifier_with_all_cohort(self, patch_create_default_modifiers_fo self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.cohort, breakdown=[cohort1.pk, cohort2.pk, "all"]), + BreakdownFilter(breakdown_type=BreakdownType.COHORT, breakdown=[cohort1.pk, cohort2.pk, "all"]), hogql_modifiers=modifiers, ) - assert modifiers.inCohortVia == InCohortVia.auto + assert modifiers.inCohortVia == InCohortVia.AUTO @patch("posthog.hogql_queries.query_runner.create_default_modifiers_for_team") def test_cohort_modifier_with_too_few_cohorts(self, patch_create_default_modifiers_for_team): @@ -1682,14 +1682,14 @@ def test_cohort_modifier_with_too_few_cohorts(self, patch_create_default_modifie self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.cohort, breakdown=[cohort1.pk, cohort2.pk, "all"]), + BreakdownFilter(breakdown_type=BreakdownType.COHORT, breakdown=[cohort1.pk, cohort2.pk, "all"]), hogql_modifiers=modifiers, ) - assert modifiers.inCohortVia == InCohortVia.auto + assert modifiers.inCohortVia == InCohortVia.AUTO @patch("posthog.hogql_queries.insights.trends.trends_query_runner.execute_hogql_query") def test_should_throw_exception(self, patch_sync_execute): @@ -1699,7 +1699,7 @@ def test_should_throw_exception(self, patch_sync_execute): self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, None, @@ -1717,7 +1717,7 @@ def test_to_actors_query_options(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, None, @@ -1752,7 +1752,7 @@ def test_to_actors_query_options_compare(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], TrendsFilter(compare=True), None, @@ -1790,7 +1790,7 @@ def test_to_actors_query_options_multiple_series(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview"), EventsNode(event="$pageleave")], None, None, @@ -1809,10 +1809,10 @@ def test_to_actors_query_options_breakdowns(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser", breakdown_limit=3), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser", breakdown_limit=3), ) response = runner.to_actors_query_options() @@ -1833,10 +1833,10 @@ def test_to_actors_query_options_breakdowns_boolean(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="bool_field"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="bool_field"), ) response = runner.to_actors_query_options() @@ -1855,11 +1855,11 @@ def test_to_actors_query_options_breakdowns_histogram(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, BreakdownFilter( - breakdown_type=BreakdownType.event, + breakdown_type=BreakdownType.EVENT, breakdown="prop", breakdown_histogram_bin_count=4, ), @@ -1901,10 +1901,10 @@ def test_to_actors_query_options_breakdowns_cohort(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.cohort, breakdown=[cohort.pk]), + BreakdownFilter(breakdown_type=BreakdownType.COHORT, breakdown=[cohort.pk]), ) response = runner.to_actors_query_options() @@ -1920,10 +1920,10 @@ def test_to_actors_query_options_breakdowns_hogql(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, - BreakdownFilter(breakdown_type=BreakdownType.hogql, breakdown="properties.$browser"), + BreakdownFilter(breakdown_type=BreakdownType.HOGQL, breakdown="properties.$browser"), ) response = runner.to_actors_query_options() @@ -1944,10 +1944,10 @@ def test_to_actors_query_options_bar_value(self): runner = self._create_query_runner( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsBarValue), - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + TrendsFilter(display=ChartDisplayType.ACTIONS_BAR_VALUE), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) response = runner.to_actors_query_options() @@ -1966,7 +1966,7 @@ def test_limit_is_context_aware(self, mock_sync_execute: MagicMock): self._run_trends_query( "2020-01-09", "2020-01-20", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], limit_context=LimitContext.QUERY_ASYNC, ) @@ -1981,7 +1981,7 @@ def test_actors_query_explicit_dates(self): runner = self._create_query_runner( "2020-01-09 12:37:42", "2020-01-20 12:37:42", - IntervalType.day, + IntervalType.DAY, [EventsNode(event="$pageview")], None, None, @@ -2024,9 +2024,9 @@ def test_sampling_adjustment(self): runner = self._create_query_runner( "2020-01-01", "2020-01-31", - IntervalType.month, + IntervalType.MONTH, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.ActionsLineGraph), + TrendsFilter(display=ChartDisplayType.ACTIONS_LINE_GRAPH), ) runner.query.samplingFactor = 0.1 response = runner.calculate() @@ -2038,9 +2038,9 @@ def test_sampling_adjustment(self): runner = self._create_query_runner( "2020-01-01", "2020-01-31", - IntervalType.month, + IntervalType.MONTH, [EventsNode(event="$pageview")], - TrendsFilter(display=ChartDisplayType.BoldNumber), + TrendsFilter(display=ChartDisplayType.BOLD_NUMBER), ) runner.query.samplingFactor = 0.1 response = runner.calculate() diff --git a/posthog/hogql_queries/insights/trends/test/test_utils.py b/posthog/hogql_queries/insights/trends/test/test_utils.py index 5fecab14914b7..e4527fae76e31 100644 --- a/posthog/hogql_queries/insights/trends/test/test_utils.py +++ b/posthog/hogql_queries/insights/trends/test/test_utils.py @@ -4,58 +4,60 @@ def test_properties_chain_person(): - p1 = get_properties_chain(breakdown_type=BreakdownType.person, breakdown_field="field", group_type_index=None) + p1 = get_properties_chain(breakdown_type=BreakdownType.PERSON, breakdown_field="field", group_type_index=None) assert p1 == ["person", "properties", "field"] - p2 = get_properties_chain(breakdown_type=BreakdownType.person, breakdown_field="field", group_type_index=1) + p2 = get_properties_chain(breakdown_type=BreakdownType.PERSON, breakdown_field="field", group_type_index=1) assert p2 == ["person", "properties", "field"] def test_properties_chain_session(): - p1 = get_properties_chain(breakdown_type=BreakdownType.session, breakdown_field="anything", group_type_index=None) + p1 = get_properties_chain(breakdown_type=BreakdownType.SESSION, breakdown_field="anything", group_type_index=None) assert p1 == ["session", "anything"] - p2 = get_properties_chain(breakdown_type=BreakdownType.session, breakdown_field="anything", group_type_index=1) + p2 = get_properties_chain(breakdown_type=BreakdownType.SESSION, breakdown_field="anything", group_type_index=1) assert p2 == ["session", "anything"] p3 = get_properties_chain( - breakdown_type=BreakdownType.session, breakdown_field="$session_duration", group_type_index=None + breakdown_type=BreakdownType.SESSION, breakdown_field="$session_duration", group_type_index=None ) assert p3 == ["session", "$session_duration"] def test_properties_chain_groups(): - p1 = get_properties_chain(breakdown_type=BreakdownType.group, breakdown_field="anything", group_type_index=1) + p1 = get_properties_chain(breakdown_type=BreakdownType.GROUP, breakdown_field="anything", group_type_index=1) assert p1 == ["group_1", "properties", "anything"] with pytest.raises(Exception) as e: - get_properties_chain(breakdown_type=BreakdownType.group, breakdown_field="anything", group_type_index=None) + get_properties_chain(breakdown_type=BreakdownType.GROUP, breakdown_field="anything", group_type_index=None) assert "group_type_index missing from params" in str(e.value) def test_properties_chain_events(): - p1 = get_properties_chain(breakdown_type=BreakdownType.event, breakdown_field="anything", group_type_index=None) + p1 = get_properties_chain(breakdown_type=BreakdownType.EVENT, breakdown_field="anything", group_type_index=None) assert p1 == ["properties", "anything"] - p2 = get_properties_chain(breakdown_type=BreakdownType.event, breakdown_field="anything_else", group_type_index=1) + p2 = get_properties_chain(breakdown_type=BreakdownType.EVENT, breakdown_field="anything_else", group_type_index=1) assert p2 == ["properties", "anything_else"] def test_properties_chain_warehouse_props(): p1 = get_properties_chain( - breakdown_type=BreakdownType.data_warehouse_person_property, + breakdown_type=BreakdownType.DATA_WAREHOUSE_PERSON_PROPERTY, breakdown_field="some_table.field", group_type_index=None, ) assert p1 == ["person", "some_table", "field"] p2 = get_properties_chain( - breakdown_type=BreakdownType.data_warehouse_person_property, breakdown_field="some_table", group_type_index=None + breakdown_type=BreakdownType.DATA_WAREHOUSE_PERSON_PROPERTY, + breakdown_field="some_table", + group_type_index=None, ) assert p2 == ["person", "some_table"] p3 = get_properties_chain( - breakdown_type=BreakdownType.data_warehouse_person_property, + breakdown_type=BreakdownType.DATA_WAREHOUSE_PERSON_PROPERTY, breakdown_field="some_table.props.obj.blah", group_type_index=None, ) diff --git a/posthog/hogql_queries/insights/trends/trends_actors_query_builder.py b/posthog/hogql_queries/insights/trends/trends_actors_query_builder.py index 1eff2f0aae9cc..c29be46cc791c 100644 --- a/posthog/hogql_queries/insights/trends/trends_actors_query_builder.py +++ b/posthog/hogql_queries/insights/trends/trends_actors_query_builder.py @@ -119,7 +119,7 @@ def trends_aggregation_operations(self) -> AggregationOperations: def is_compare_previous(self) -> bool: return ( bool(self.trends_query.trendsFilter and self.trends_query.trendsFilter.compare) - and self.compare_value == Compare.previous + and self.compare_value == Compare.PREVIOUS ) @cached_property @@ -128,11 +128,11 @@ def is_active_users_math(self) -> bool: @cached_property def is_weekly_active_math(self) -> bool: - return self.entity.math == BaseMathType.weekly_active + return self.entity.math == BaseMathType.WEEKLY_ACTIVE @cached_property def is_monthly_active_math(self) -> bool: - return self.entity.math == BaseMathType.monthly_active + return self.entity.math == BaseMathType.MONTHLY_ACTIVE @cached_property def is_hourly(self) -> bool: diff --git a/posthog/hogql_queries/insights/trends/trends_query_builder.py b/posthog/hogql_queries/insights/trends/trends_query_builder.py index 00b7fad057384..015e269e5628e 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_builder.py +++ b/posthog/hogql_queries/insights/trends/trends_query_builder.py @@ -136,7 +136,7 @@ def _get_events_subquery( # For cumulative unique users or groups, we want to count each user or group once per query, not per day if ( self.query.trendsFilter - and self.query.trendsFilter.display == ChartDisplayType.ActionsLineGraphCumulative + and self.query.trendsFilter.display == ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE and (self.series.math == "unique_group" or self.series.math == "dau") ): day_start.expr = ast.Call(name="min", args=[day_start.expr]) @@ -237,7 +237,8 @@ def _get_events_subquery( return default_query def _outer_select_query(self, breakdown: Breakdown, inner_query: ast.SelectQuery) -> ast.SelectQuery: - total_array = parse_expr(""" + total_array = parse_expr( + """ arrayMap( _match_date -> arraySum( @@ -249,9 +250,10 @@ def _outer_select_query(self, breakdown: Breakdown, inner_query: ast.SelectQuery ), date ) - """) + """ + ) - if self._trends_display.display_type == ChartDisplayType.ActionsLineGraphCumulative: + if self._trends_display.display_type == ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE: # fill zeros in with the previous value total_array = parse_expr( """ diff --git a/posthog/hogql_queries/insights/trends/trends_query_runner.py b/posthog/hogql_queries/insights/trends/trends_query_runner.py index cbd5f87e1c4c5..d64e80a38330a 100644 --- a/posthog/hogql_queries/insights/trends/trends_query_runner.py +++ b/posthog/hogql_queries/insights/trends/trends_query_runner.py @@ -157,7 +157,7 @@ def to_actors_query( include_recordings: Optional[bool] = None, ) -> ast.SelectQuery | ast.SelectUnionQuery: with self.timings.measure("trends_to_actors_query"): - if self.query.breakdownFilter and self.query.breakdownFilter.breakdown_type == BreakdownType.cohort: + if self.query.breakdownFilter and self.query.breakdownFilter.breakdown_type == BreakdownType.COHORT: if self.query.breakdownFilter.breakdown in ("all", ["all"]) or breakdown_value == "all": self.query.breakdownFilter = None elif isinstance(self.query.breakdownFilter.breakdown, list): @@ -443,7 +443,7 @@ def get_value(name: str, val: Any): }, } else: - if self._trends_display.display_type == ChartDisplayType.ActionsLineGraphCumulative: + if self._trends_display.display_type == ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE: count = get_value("total", val)[-1] else: count = float(sum(get_value("total", val))) @@ -587,14 +587,14 @@ def series_event(self, series: Union[EventsNode, ActionsNode, DataWarehouseNode] def update_hogql_modifiers(self) -> None: if ( - self.modifiers.inCohortVia == InCohortVia.auto + self.modifiers.inCohortVia == InCohortVia.AUTO and self.query.breakdownFilter is not None and self.query.breakdownFilter.breakdown_type == "cohort" and isinstance(self.query.breakdownFilter.breakdown, list) and len(self.query.breakdownFilter.breakdown) > 1 and not any(value == "all" for value in self.query.breakdownFilter.breakdown) ): - self.modifiers.inCohortVia = InCohortVia.leftjoin_conjoined + self.modifiers.inCohortVia = InCohortVia.LEFTJOIN_CONJOINED datawarehouse_modifiers = [] for series in self.query.series: @@ -623,7 +623,7 @@ def setup_series(self) -> list[SeriesWithExtras]: ] if ( - self.modifiers.inCohortVia != InCohortVia.leftjoin_conjoined + self.modifiers.inCohortVia != InCohortVia.LEFTJOIN_CONJOINED and self.query.breakdownFilter is not None and self.query.breakdownFilter.breakdown_type == "cohort" ): @@ -696,7 +696,7 @@ def apply_formula( and self.query.breakdownFilter.breakdown_type == "cohort" and isinstance(self.query.breakdownFilter.breakdown, list) and "all" in self.query.breakdownFilter.breakdown - and self.modifiers.inCohortVia != InCohortVia.leftjoin_conjoined + and self.modifiers.inCohortVia != InCohortVia.LEFTJOIN_CONJOINED and not in_breakdown_clause and self.query.trendsFilter and self.query.trendsFilter.formula @@ -888,7 +888,7 @@ def _query_to_filter(self) -> dict[str, Any]: @cached_property def _trends_display(self) -> TrendsDisplay: if self.query.trendsFilter is None or self.query.trendsFilter.display is None: - display = ChartDisplayType.ActionsLineGraph + display = ChartDisplayType.ACTIONS_LINE_GRAPH else: display = self.query.trendsFilter.display diff --git a/posthog/hogql_queries/insights/utils/test/test_entities.py b/posthog/hogql_queries/insights/utils/test/test_entities.py index df89b129031b4..587f9e8c9cc3f 100644 --- a/posthog/hogql_queries/insights/utils/test/test_entities.py +++ b/posthog/hogql_queries/insights/utils/test/test_entities.py @@ -22,10 +22,10 @@ (ActionsNode(id=1), ActionsNode(id=2), False), ( EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), True, ), @@ -44,50 +44,50 @@ # different type ( EventsNode( - properties=[PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), False, ), # different key ( EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - properties=[EventPropertyFilter(key="other_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="other_key", value="some_value", operator=PropertyOperator.EXACT)] ), False, ), # different value ( EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - properties=[EventPropertyFilter(key="some_key", value="other_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="other_value", operator=PropertyOperator.EXACT)] ), False, ), # different operator ( EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.is_not)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.IS_NOT)] ), False, ), # different fixed properties ( EventsNode( - fixedProperties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + fixedProperties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - fixedProperties=[EventPropertyFilter(key="other_key", value="some_value", operator=PropertyOperator.exact)] + fixedProperties=[EventPropertyFilter(key="other_key", value="some_value", operator=PropertyOperator.EXACT)] ), False, ), @@ -103,10 +103,10 @@ def test_is_equal(a, b, expected): # everything equal ( EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), True, ), @@ -114,24 +114,24 @@ def test_is_equal(a, b, expected): ( EventsNode( properties=[ - EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact), - PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact), + EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT), + PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT), ] ), EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), True, ), # subset ( EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( properties=[ - EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact), - PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact), + EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT), + PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT), ] ), False, @@ -154,10 +154,10 @@ def test_is_equal(a, b, expected): # different type ( EventsNode( - properties=[PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[PersonPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), EventsNode( - properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.exact)] + properties=[EventPropertyFilter(key="some_key", value="some_value", operator=PropertyOperator.EXACT)] ), False, ), diff --git a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py index 5510b198257df..cad685ec9675e 100644 --- a/posthog/hogql_queries/legacy_compatibility/filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/filter_to_query.py @@ -40,16 +40,16 @@ class MathAvailability(str, Enum): actors_only_math_types = [ - BaseMathType.dau, - BaseMathType.weekly_active, - BaseMathType.monthly_active, + BaseMathType.DAU, + BaseMathType.WEEKLY_ACTIVE, + BaseMathType.MONTHLY_ACTIVE, "unique_group", "hogql", ] def clean_display(display: str): - if display not in ChartDisplayType.__members__: + if display not in [c.value for c in ChartDisplayType]: return None else: return display @@ -81,7 +81,7 @@ def legacy_entity_to_node( and math_availability == MathAvailability.ActorsOnly and entity.math not in actors_only_math_types ): - shared = {**shared, "math": BaseMathType.dau} + shared = {**shared, "math": BaseMathType.DAU} else: shared = { **shared, @@ -321,7 +321,7 @@ def _insight_filter(filter: dict): # Backwards compatibility # Before Filter.funnel_viz_type funnel trends were indicated by Filter.display being TRENDS_LINEAR if funnel_viz_type is None and filter.get("display") == "ActionsLineGraph": - funnel_viz_type = FunnelVizType.trends + funnel_viz_type = FunnelVizType.TRENDS insight_filter = { "funnelsFilter": FunnelsFilter( diff --git a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py index 13f5ea9f4d7f6..87c007c110833 100644 --- a/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py +++ b/posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py @@ -1007,9 +1007,9 @@ def test_series_custom(self): query.series, [ ActionsNode(id=1), - ActionsNode(id=1, math=BaseMathType.dau), + ActionsNode(id=1, math=BaseMathType.DAU), EventsNode(event="$pageview", name="$pageview"), - EventsNode(event="$pageview", name="$pageview", math=BaseMathType.dau), + EventsNode(event="$pageview", name="$pageview", math=BaseMathType.DAU), ], ) @@ -1037,7 +1037,7 @@ def test_series_data_warehouse(self): DataWarehouseNode( id="some_table", name="some_table", - math=BaseMathType.total, + math=BaseMathType.TOTAL, table_name="some_table", id_field="id", timestamp_field="created_at", @@ -1061,9 +1061,9 @@ def test_series_order(self): self.assertEqual( query.series, [ - ActionsNode(id=1, math=BaseMathType.dau), + ActionsNode(id=1, math=BaseMathType.DAU), EventsNode(event="$pageview", name="$pageview"), - EventsNode(event="$pageview", name="$pageview", math=BaseMathType.dau), + EventsNode(event="$pageview", name="$pageview", math=BaseMathType.DAU), ActionsNode(id=1), ], ) @@ -1100,23 +1100,23 @@ def test_series_math(self): self.assertEqual( query.series, [ - EventsNode(event="$pageview", name="$pageview", math=BaseMathType.dau), + EventsNode(event="$pageview", name="$pageview", math=BaseMathType.DAU), EventsNode( event="$pageview", name="$pageview", - math=PropertyMathType.median, + math=PropertyMathType.MEDIAN, math_property="$math_prop", ), EventsNode( event="$pageview", name="$pageview", - math=CountPerActorMathType.avg_count_per_actor, + math=CountPerActorMathType.AVG_COUNT_PER_ACTOR, ), EventsNode( event="$pageview", name="$pageview", math="unique_group", - math_group_type_index=MathGroupTypeIndex.number_0, + math_group_type_index=MathGroupTypeIndex.NUMBER_0, ), EventsNode( event="$pageview", @@ -1235,7 +1235,7 @@ def test_series_properties(self): EventPropertyFilter( key="success", value=["true"], - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, ) ], ), @@ -1246,7 +1246,7 @@ def test_series_properties(self): PersonPropertyFilter( key="email", value="is_set", - operator=PropertyOperator.is_set, + operator=PropertyOperator.IS_SET, ) ], ), @@ -1255,16 +1255,16 @@ def test_series_properties(self): name="$pageview", properties=[ ElementPropertyFilter( - key=Key.text, + key=Key.TEXT, value=["some text"], - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, ) ], ), EventsNode( event="$pageview", name="$pageview", - properties=[SessionPropertyFilter(key="$session_duration", value=1, operator=PropertyOperator.gt)], + properties=[SessionPropertyFilter(key="$session_duration", value=1, operator=PropertyOperator.GT)], ), EventsNode( event="$pageview", @@ -1278,7 +1278,7 @@ def test_series_properties(self): GroupPropertyFilter( key="name", value=["Hedgebox Inc."], - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, group_type_index=2, ) ], @@ -1295,12 +1295,12 @@ def test_series_properties(self): EventPropertyFilter( key="$referring_domain", value="google", - operator=PropertyOperator.icontains, + operator=PropertyOperator.ICONTAINS, ), EventPropertyFilter( key="utm_source", value="is_not_set", - operator=PropertyOperator.is_not_set, + operator=PropertyOperator.IS_NOT_SET, ), ], ), @@ -1315,7 +1315,7 @@ def test_breakdown(self): assert isinstance(query, TrendsQuery) self.assertEqual( query.breakdownFilter, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) def test_breakdown_converts_multi(self): @@ -1326,7 +1326,7 @@ def test_breakdown_converts_multi(self): assert isinstance(query, TrendsQuery) self.assertEqual( query.breakdownFilter, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="$browser"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="$browser"), ) def test_breakdown_type_default(self): @@ -1337,7 +1337,7 @@ def test_breakdown_type_default(self): assert isinstance(query, TrendsQuery) self.assertEqual( query.breakdownFilter, - BreakdownFilter(breakdown_type=BreakdownType.event, breakdown="some_prop"), + BreakdownFilter(breakdown_type=BreakdownType.EVENT, breakdown="some_prop"), ) def test_trends_filter(self): @@ -1363,11 +1363,11 @@ def test_trends_filter(self): TrendsFilter( smoothingIntervals=2, compare=True, - aggregationAxisFormat=AggregationAxisFormat.duration_ms, + aggregationAxisFormat=AggregationAxisFormat.DURATION_MS, aggregationAxisPrefix="pre", aggregationAxisPostfix="post", formula="A + B", - display=ChartDisplayType.ActionsAreaGraph, + display=ChartDisplayType.ACTIONS_AREA_GRAPH, decimalPlaces=5, showLegend=True, showPercentStackView=True, @@ -1427,14 +1427,14 @@ def test_funnels_filter(self): self.assertEqual( query.funnelsFilter, FunnelsFilter( - funnelVizType=FunnelVizType.steps, + funnelVizType=FunnelVizType.STEPS, funnelFromStep=1, funnelToStep=2, - funnelWindowIntervalUnit=FunnelConversionWindowTimeUnit.hour, + funnelWindowIntervalUnit=FunnelConversionWindowTimeUnit.HOUR, funnelWindowInterval=13, - breakdownAttributionType=BreakdownAttributionType.step, + breakdownAttributionType=BreakdownAttributionType.STEP, breakdownAttributionValue=2, - funnelOrderType=StepOrderValue.strict, + funnelOrderType=StepOrderValue.STRICT, exclusions=[ FunnelExclusionEventsNode( event="$pageview", @@ -1477,9 +1477,9 @@ def test_retention_filter(self): self.assertEqual( query.retentionFilter, RetentionFilter( - retentionType=RetentionType.retention_first_time, + retentionType=RetentionType.RETENTION_FIRST_TIME, totalIntervals=12, - period=RetentionPeriod.Week, + period=RetentionPeriod.WEEK, returningEntity={ "id": "$pageview", "name": "$pageview", @@ -1539,7 +1539,7 @@ def test_paths_filter(self): self.assertEqual( query.pathsFilter, PathsFilter( - includeEventTypes=[PathType.field_pageview, PathType.hogql], + includeEventTypes=[PathType.FIELD_PAGEVIEW, PathType.HOGQL], pathsHogQLExpression="event", startPoint="http://localhost:8000/events", endPoint="http://localhost:8000/home", @@ -1558,14 +1558,14 @@ def test_paths_filter(self): self.assertEqual( query.funnelPathsFilter, FunnelPathsFilter( - funnelPathType=FunnelPathType.funnel_path_between_steps, + funnelPathType=FunnelPathType.FUNNEL_PATH_BETWEEN_STEPS, funnelSource=FunnelsQuery( series=[ EventsNode(event="$pageview", name="$pageview"), EventsNode(event=None, name="All events"), ], filterTestAccounts=True, - funnelsFilter=FunnelsFilter(funnelVizType=FunnelVizType.steps, exclusions=[]), + funnelsFilter=FunnelsFilter(funnelVizType=FunnelVizType.STEPS, exclusions=[]), breakdownFilter=BreakdownFilter(), dateRange=InsightDateRange(), ), @@ -1605,6 +1605,6 @@ def test_lifecycle_filter(self): query.lifecycleFilter, LifecycleFilter( showValuesOnSeries=True, - toggledLifecycles=[LifecycleToggle.new, LifecycleToggle.dormant], + toggledLifecycles=[LifecycleToggle.NEW, LifecycleToggle.DORMANT], ), ) diff --git a/posthog/hogql_queries/query_runner.py b/posthog/hogql_queries/query_runner.py index 850ca5b663e3d..8ea3e2e806e79 100644 --- a/posthog/hogql_queries/query_runner.py +++ b/posthog/hogql_queries/query_runner.py @@ -264,31 +264,62 @@ def get_query_runner( team=team, timings=timings, modifiers=modifiers, + limit_context=limit_context, ) if kind == "WebOverviewQuery": use_session_table = get_from_dict_or_attr(query, "useSessionsTable") if use_session_table: from .web_analytics.web_overview import WebOverviewQueryRunner - return WebOverviewQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) + return WebOverviewQueryRunner( + query=query, + team=team, + timings=timings, + modifiers=modifiers, + limit_context=limit_context, + ) else: from .web_analytics.web_overview_legacy import LegacyWebOverviewQueryRunner - return LegacyWebOverviewQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) + return LegacyWebOverviewQueryRunner( + query=query, + team=team, + timings=timings, + modifiers=modifiers, + limit_context=limit_context, + ) if kind == "WebTopClicksQuery": from .web_analytics.top_clicks import WebTopClicksQueryRunner - return WebTopClicksQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) + return WebTopClicksQueryRunner( + query=query, + team=team, + timings=timings, + modifiers=modifiers, + limit_context=limit_context, + ) if kind == "WebStatsTableQuery": use_session_table = get_from_dict_or_attr(query, "useSessionsTable") if use_session_table: from .web_analytics.stats_table import WebStatsTableQueryRunner - return WebStatsTableQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) + return WebStatsTableQueryRunner( + query=query, + team=team, + timings=timings, + modifiers=modifiers, + limit_context=limit_context, + ) else: from .web_analytics.stats_table_legacy import LegacyWebStatsTableQueryRunner - return LegacyWebStatsTableQueryRunner(query=query, team=team, timings=timings, modifiers=modifiers) + return LegacyWebStatsTableQueryRunner( + query=query, + team=team, + timings=timings, + modifiers=modifiers, + limit_context=limit_context, + ) raise ValueError(f"Can't get a runner for an unknown query kind: {kind}") @@ -543,13 +574,15 @@ def apply_dashboard_filters(self, dashboard_filter: DashboardFilter): if self.query.properties: try: self.query.properties = PropertyGroupFilter( - type=FilterLogicalOperator.AND, + type=FilterLogicalOperator.AND_, values=[ - PropertyGroupFilterValue(type=FilterLogicalOperator.AND, values=self.query.properties) - if isinstance(self.query.properties, list) - else PropertyGroupFilterValue(**self.query.properties.model_dump()), + ( + PropertyGroupFilterValue(type=FilterLogicalOperator.AND_, values=self.query.properties) + if isinstance(self.query.properties, list) + else PropertyGroupFilterValue(**self.query.properties.model_dump()) + ), PropertyGroupFilterValue( - type=FilterLogicalOperator.AND, values=dashboard_filter.properties + type=FilterLogicalOperator.AND_, values=dashboard_filter.properties ), ], ) diff --git a/posthog/hogql_queries/test/test_actors_query_runner.py b/posthog/hogql_queries/test/test_actors_query_runner.py index d6bed9fec969b..904c1adad8d9f 100644 --- a/posthog/hogql_queries/test/test_actors_query_runner.py +++ b/posthog/hogql_queries/test/test_actors_query_runner.py @@ -94,7 +94,7 @@ def test_persons_query_properties(self): PersonPropertyFilter( key="random_uuid", value=self.random_uuid, - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, ), HogQLPropertyFilter(key="toInt(properties.index) > 5"), ] @@ -110,7 +110,7 @@ def test_persons_query_fixed_properties(self): PersonPropertyFilter( key="random_uuid", value=self.random_uuid, - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, ), HogQLPropertyFilter(key="toInt(properties.index) < 2"), ] @@ -221,10 +221,10 @@ def test_source_lifecycle_query(self): PersonPropertyFilter( key="random_uuid", value=self.random_uuid, - operator=PropertyOperator.exact, + operator=PropertyOperator.EXACT, ) ], - interval=IntervalType.day, + interval=IntervalType.DAY, dateRange=InsightDateRange(date_from="-7d"), ) query = ActorsQuery( diff --git a/posthog/hogql_queries/test/test_events_query_runner.py b/posthog/hogql_queries/test/test_events_query_runner.py index 345df985bf6fa..f42fe3dc65755 100644 --- a/posthog/hogql_queries/test/test_events_query_runner.py +++ b/posthog/hogql_queries/test/test_events_query_runner.py @@ -97,8 +97,8 @@ def test_is_not_set_boolean(self): EventPropertyFilter( type="event", key="boolean_field", - operator=PropertyOperator.is_not_set, - value=PropertyOperator.is_not_set, + operator=PropertyOperator.IS_NOT_SET, + value=PropertyOperator.IS_NOT_SET, ) ) @@ -111,8 +111,8 @@ def test_is_set_boolean(self): EventPropertyFilter( type="event", key="boolean_field", - operator=PropertyOperator.is_set, - value=PropertyOperator.is_set, + operator=PropertyOperator.IS_SET, + value=PropertyOperator.IS_SET, ) ) diff --git a/posthog/hogql_queries/test/test_query_runner.py b/posthog/hogql_queries/test/test_query_runner.py index 6b43b0c76e2da..96003badffd03 100644 --- a/posthog/hogql_queries/test/test_query_runner.py +++ b/posthog/hogql_queries/test/test_query_runner.py @@ -200,7 +200,7 @@ def test_modifier_passthrough(self): runner = HogQLQueryRunner( query=HogQLQuery(query="select properties.$browser from events"), team=self.team, - modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.legacy_null_as_string), + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.LEGACY_NULL_AS_STRING), ) response = runner.calculate() assert response.clickhouse is not None @@ -209,7 +209,7 @@ def test_modifier_passthrough(self): runner = HogQLQueryRunner( query=HogQLQuery(query="select properties.$browser from events"), team=self.team, - modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.disabled), + modifiers=HogQLQueryModifiers(materializationMode=MaterializationMode.DISABLED), ) response = runner.calculate() assert response.clickhouse is not None diff --git a/posthog/hogql_queries/utils/query_date_range.py b/posthog/hogql_queries/utils/query_date_range.py index ecdf9411b0054..1f5d5bf7996a1 100644 --- a/posthog/hogql_queries/utils/query_date_range.py +++ b/posthog/hogql_queries/utils/query_date_range.py @@ -38,10 +38,10 @@ def __init__( ) -> None: self._team = team self._date_range = date_range - self._interval = interval or IntervalType.day + self._interval = interval or IntervalType.DAY self._now_without_timezone = now - if not isinstance(self._interval, IntervalType) or re.match(r"[^a-z]", self._interval.name): + if not isinstance(self._interval, IntervalType) or re.match(r"[^a-z]", "DAY", re.IGNORECASE): raise ValueError(f"Invalid interval: {interval}") def date_to(self) -> datetime: @@ -114,18 +114,18 @@ def previous_period_date_from_str(self) -> str: @cached_property def interval_type(self) -> IntervalType: - return self._interval or IntervalType.day + return self._interval or IntervalType.DAY @cached_property def interval_name(self) -> IntervalLiteral: - return cast(IntervalLiteral, self.interval_type.name) + return cast(IntervalLiteral, self.interval_type.name.lower()) @cached_property def is_hourly(self) -> bool: if self._interval is None: return False - return self._interval == IntervalType.hour + return self._interval == IntervalType.HOUR @cached_property def explicit(self) -> bool: @@ -229,9 +229,9 @@ def use_start_of_interval(self): is_delta_hours = delta_mapping.get("hours", None) is not None - if interval in (IntervalType.hour, IntervalType.minute): + if interval in (IntervalType.HOUR, IntervalType.MINUTE): return False - elif interval == IntervalType.day: + elif interval == IntervalType.DAY: if is_delta_hours: return False return True @@ -310,15 +310,15 @@ def determine_time_delta(total_intervals: int, period: str) -> timedelta: def date_from(self) -> datetime: delta = self.determine_time_delta(self.total_intervals, self._interval.name) - if self._interval in (IntervalType.hour, IntervalType.minute): + if self._interval in (IntervalType.HOUR, IntervalType.MINUTE): return self.date_to() - delta - elif self._interval == IntervalType.week: + elif self._interval == IntervalType.WEEK: date_from = self.date_to() - delta week_start_alignment_days = date_from.isoweekday() % 7 if self._team.week_start_day == WeekStartDay.MONDAY: week_start_alignment_days = date_from.weekday() return date_from - timedelta(days=week_start_alignment_days) - elif self._interval == IntervalType.month: + elif self._interval == IntervalType.MONTH: return self.date_to().replace(day=1, hour=0, minute=0, second=0, microsecond=0) - delta else: date_to = self.date_to().replace(hour=0, minute=0, second=0, microsecond=0) diff --git a/posthog/hogql_queries/utils/test/test_query_date_range.py b/posthog/hogql_queries/utils/test/test_query_date_range.py index c645df7744802..c87c29ebcbd72 100644 --- a/posthog/hogql_queries/utils/test/test_query_date_range.py +++ b/posthog/hogql_queries/utils/test/test_query_date_range.py @@ -13,14 +13,14 @@ class TestQueryDateRange(APIBaseTest): def test_parsed_date(self): now = parser.isoparse("2021-08-25T00:00:00.000Z") date_range = InsightDateRange(date_from="-48h") - query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.day, now=now) + query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.DAY, now=now) self.assertEqual(query_date_range.date_from(), parser.isoparse("2021-08-23T00:00:00Z")) self.assertEqual(query_date_range.date_to(), parser.isoparse("2021-08-25T23:59:59.999999Z")) def test_parsed_date_hour(self): now = parser.isoparse("2021-08-25T00:00:00.000Z") date_range = InsightDateRange(date_from="-48h") - query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.hour, now=now) + query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.HOUR, now=now) self.assertEqual(query_date_range.date_from(), parser.isoparse("2021-08-23T00:00:00Z")) self.assertEqual( @@ -30,7 +30,7 @@ def test_parsed_date_hour(self): def test_parsed_date_middle_of_hour(self): now = parser.isoparse("2021-08-25T00:00:00.000Z") date_range = InsightDateRange(date_from="2021-08-23 05:00:00", date_to="2021-08-26 07:00:00") - query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.hour, now=now) + query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.HOUR, now=now) self.assertEqual(query_date_range.date_from(), parser.isoparse("2021-08-23 05:00:00Z")) self.assertEqual( @@ -40,7 +40,7 @@ def test_parsed_date_middle_of_hour(self): def test_parsed_date_week(self): now = parser.isoparse("2021-08-25T00:00:00.000Z") date_range = InsightDateRange(date_from="-7d") - query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.week, now=now) + query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.WEEK, now=now) self.assertEqual(query_date_range.date_from(), parser.isoparse("2021-08-18 00:00:00Z")) self.assertEqual(query_date_range.date_to(), parser.isoparse("2021-08-25 23:59:59.999999Z")) @@ -49,13 +49,13 @@ def test_all_values(self): now = parser.isoparse("2021-08-25T00:00:00.000Z") self.assertEqual( QueryDateRange( - team=self.team, date_range=InsightDateRange(date_from="-20h"), interval=IntervalType.day, now=now + team=self.team, date_range=InsightDateRange(date_from="-20h"), interval=IntervalType.DAY, now=now ).all_values(), [parser.isoparse("2021-08-24T00:00:00Z"), parser.isoparse("2021-08-25T00:00:00Z")], ) self.assertEqual( QueryDateRange( - team=self.team, date_range=InsightDateRange(date_from="-20d"), interval=IntervalType.week, now=now + team=self.team, date_range=InsightDateRange(date_from="-20d"), interval=IntervalType.WEEK, now=now ).all_values(), [ parser.isoparse("2021-08-01T00:00:00Z"), @@ -67,7 +67,7 @@ def test_all_values(self): self.team.week_start_day = WeekStartDay.MONDAY self.assertEqual( QueryDateRange( - team=self.team, date_range=InsightDateRange(date_from="-20d"), interval=IntervalType.week, now=now + team=self.team, date_range=InsightDateRange(date_from="-20d"), interval=IntervalType.WEEK, now=now ).all_values(), [ parser.isoparse("2021-08-02T00:00:00Z"), @@ -78,13 +78,13 @@ def test_all_values(self): ) self.assertEqual( QueryDateRange( - team=self.team, date_range=InsightDateRange(date_from="-50d"), interval=IntervalType.month, now=now + team=self.team, date_range=InsightDateRange(date_from="-50d"), interval=IntervalType.MONTH, now=now ).all_values(), [parser.isoparse("2021-07-01T00:00:00Z"), parser.isoparse("2021-08-01T00:00:00Z")], ) self.assertEqual( QueryDateRange( - team=self.team, date_range=InsightDateRange(date_from="-3h"), interval=IntervalType.hour, now=now + team=self.team, date_range=InsightDateRange(date_from="-3h"), interval=IntervalType.HOUR, now=now ).all_values(), [ parser.isoparse("2021-08-24T21:00:00Z"), @@ -99,7 +99,7 @@ def test_date_to_explicit(self): date_range = InsightDateRange( date_from="2021-02-25T12:25:23.000Z", date_to="2021-04-25T10:59:23.000Z", explicitDate=True ) - query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.day, now=now) + query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.DAY, now=now) self.assertEqual(query_date_range.date_from(), parser.isoparse("2021-02-25T12:25:23.000Z")) self.assertEqual(query_date_range.date_to(), parser.isoparse("2021-04-25T10:59:23.000Z")) @@ -108,12 +108,12 @@ def test_yesterday(self): now = parser.isoparse("2021-08-25T00:00:00.000Z") date_range = InsightDateRange(date_from="-1dStart", date_to="-1dEnd", explicitDate=False) - query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.hour, now=now) + query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.HOUR, now=now) self.assertEqual(query_date_range.date_from(), parser.isoparse("2021-08-24T00:00:00.000000Z")) self.assertEqual(query_date_range.date_to(), parser.isoparse("2021-08-24T23:59:59.999999Z")) - query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.day, now=now) + query_date_range = QueryDateRange(team=self.team, date_range=date_range, interval=IntervalType.DAY, now=now) self.assertEqual(query_date_range.date_from(), parser.isoparse("2021-08-24T00:00:00.000000Z")) self.assertEqual(query_date_range.date_to(), parser.isoparse("2021-08-24T23:59:59.999999Z")) @@ -125,7 +125,7 @@ def setUp(self): self.total_intervals = 5 def test_constructor_initialization(self): - query = QueryDateRangeWithIntervals(None, self.total_intervals, self.team, IntervalType.day, self.now) + query = QueryDateRangeWithIntervals(None, self.total_intervals, self.team, IntervalType.DAY, self.now) self.assertEqual(query.total_intervals, self.total_intervals) def test_determine_time_delta_valid(self): @@ -137,44 +137,44 @@ def test_determine_time_delta_invalid_period(self): QueryDateRangeWithIntervals.determine_time_delta(5, "decade") def test_date_from_day_interval(self): - query = QueryDateRangeWithIntervals(None, 2, self.team, IntervalType.day, self.now) + query = QueryDateRangeWithIntervals(None, 2, self.team, IntervalType.DAY, self.now) self.assertEqual(query.date_from(), parser.isoparse("2021-08-24T00:00:00Z")) def test_date_from_hour_interval(self): - query = QueryDateRangeWithIntervals(None, 48, self.team, IntervalType.hour, self.now) + query = QueryDateRangeWithIntervals(None, 48, self.team, IntervalType.HOUR, self.now) self.assertEqual(query.date_from(), parser.isoparse("2021-08-23T01:00:00Z")) def test_date_from_week_interval_starting_monday(self): self.team.week_start_day = WeekStartDay.MONDAY - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.week, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.WEEK, self.now) self.assertEqual(query.date_from(), parser.isoparse("2021-08-23T00:00:00Z")) def test_date_from_week_interval_starting_sunday(self): self.team.week_start_day = WeekStartDay.SUNDAY - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.week, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.WEEK, self.now) self.assertEqual(query.date_from(), parser.isoparse("2021-08-22T00:00:00Z")) def test_date_to_day_interval(self): - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.day, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.DAY, self.now) self.assertEqual(query.date_to(), parser.isoparse("2021-08-26T00:00:00Z")) def test_date_to_hour_interval(self): - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.hour, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.HOUR, self.now) self.assertEqual(query.date_to(), parser.isoparse("2021-08-25T01:00:00Z")) def test_get_start_of_interval_hogql_day_interval(self): - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.day, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.DAY, self.now) expected_expr = ast.Call(name="toStartOfDay", args=[ast.Constant(value=query.date_from())]) self.assertEqual(query.get_start_of_interval_hogql(), expected_expr) def test_get_start_of_interval_hogql_hour_interval(self): - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.hour, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.HOUR, self.now) expected_expr = ast.Call(name="toStartOfHour", args=[ast.Constant(value=query.date_from())]) self.assertEqual(query.get_start_of_interval_hogql(), expected_expr) def test_get_start_of_interval_hogql_week_interval(self): self.team.week_start_day = WeekStartDay.MONDAY - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.week, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.WEEK, self.now) week_mode = WeekStartDay(self.team.week_start_day or 0).clickhouse_mode expected_expr = ast.Call( name="toStartOfWeek", args=[ast.Constant(value=query.date_from()), ast.Constant(value=int(week_mode))] @@ -183,6 +183,6 @@ def test_get_start_of_interval_hogql_week_interval(self): def test_get_start_of_interval_hogql_with_source(self): source_expr = ast.Constant(value="2021-08-25T00:00:00.000Z") - query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.day, self.now) + query = QueryDateRangeWithIntervals(None, 1, self.team, IntervalType.DAY, self.now) expected_expr = ast.Call(name="toStartOfDay", args=[source_expr]) self.assertEqual(query.get_start_of_interval_hogql(source=source_expr), expected_expr) diff --git a/posthog/hogql_queries/web_analytics/stats_table.py b/posthog/hogql_queries/web_analytics/stats_table.py index 0b224932b7d76..92521454bdb7f 100644 --- a/posthog/hogql_queries/web_analytics/stats_table.py +++ b/posthog/hogql_queries/web_analytics/stats_table.py @@ -38,12 +38,12 @@ def __init__(self, *args, **kwargs): ) def to_query(self) -> ast.SelectQuery: - if self.query.breakdownBy == WebStatsBreakdown.Page: + if self.query.breakdownBy == WebStatsBreakdown.PAGE: if self.query.includeScrollDepth and self.query.includeBounceRate: return self.to_path_scroll_bounce_query() elif self.query.includeBounceRate: return self.to_path_bounce_query() - if self.query.breakdownBy == WebStatsBreakdown.InitialPage: + if self.query.breakdownBy == WebStatsBreakdown.INITIAL_PAGE: if self.query.includeBounceRate: return self.to_entry_bounce_query() if self._has_session_properties(): @@ -178,7 +178,7 @@ def to_entry_bounce_query(self) -> ast.SelectQuery: return query def to_path_scroll_bounce_query(self) -> ast.SelectQuery: - if self.query.breakdownBy != WebStatsBreakdown.Page: + if self.query.breakdownBy != WebStatsBreakdown.PAGE: raise NotImplementedError("Scroll depth is only supported for page breakdowns") with self.timings.measure("stats_table_bounce_query"): @@ -290,7 +290,7 @@ def to_path_scroll_bounce_query(self) -> ast.SelectQuery: return query def to_path_bounce_query(self) -> ast.SelectQuery: - if self.query.breakdownBy not in [WebStatsBreakdown.InitialPage, WebStatsBreakdown.Page]: + if self.query.breakdownBy not in [WebStatsBreakdown.INITIAL_PAGE, WebStatsBreakdown.PAGE]: raise NotImplementedError("Bounce rate is only supported for page breakdowns") with self.timings.measure("stats_table_scroll_query"): @@ -394,15 +394,15 @@ def _has_session_properties(self) -> bool: return any( get_property_type(p) == "session" for p in self.query.properties + self._test_account_filters ) or self.query.breakdownBy in { - WebStatsBreakdown.InitialChannelType, - WebStatsBreakdown.InitialReferringDomain, - WebStatsBreakdown.InitialUTMSource, - WebStatsBreakdown.InitialUTMCampaign, - WebStatsBreakdown.InitialUTMMedium, - WebStatsBreakdown.InitialUTMTerm, - WebStatsBreakdown.InitialUTMContent, - WebStatsBreakdown.InitialPage, - WebStatsBreakdown.ExitPage, + WebStatsBreakdown.INITIAL_CHANNEL_TYPE, + WebStatsBreakdown.INITIAL_REFERRING_DOMAIN, + WebStatsBreakdown.INITIAL_UTM_SOURCE, + WebStatsBreakdown.INITIAL_UTM_CAMPAIGN, + WebStatsBreakdown.INITIAL_UTM_MEDIUM, + WebStatsBreakdown.INITIAL_UTM_TERM, + WebStatsBreakdown.INITIAL_UTM_CONTENT, + WebStatsBreakdown.INITIAL_PAGE, + WebStatsBreakdown.EXIT_PAGE, } def _session_properties(self) -> ast.Expr: @@ -453,60 +453,60 @@ def calculate(self): def _counts_breakdown_value(self): match self.query.breakdownBy: - case WebStatsBreakdown.Page: + case WebStatsBreakdown.PAGE: return self._apply_path_cleaning(ast.Field(chain=["events", "properties", "$pathname"])) - case WebStatsBreakdown.InitialPage: + case WebStatsBreakdown.INITIAL_PAGE: return self._apply_path_cleaning(ast.Field(chain=["sessions", "$entry_pathname"])) - case WebStatsBreakdown.ExitPage: + case WebStatsBreakdown.EXIT_PAGE: return self._apply_path_cleaning(ast.Field(chain=["sessions", "$exit_pathname"])) - case WebStatsBreakdown.InitialReferringDomain: + case WebStatsBreakdown.INITIAL_REFERRING_DOMAIN: return ast.Field(chain=["sessions", "$entry_referring_domain"]) - case WebStatsBreakdown.InitialUTMSource: + case WebStatsBreakdown.INITIAL_UTM_SOURCE: return ast.Field(chain=["sessions", "$entry_utm_source"]) - case WebStatsBreakdown.InitialUTMCampaign: + case WebStatsBreakdown.INITIAL_UTM_CAMPAIGN: return ast.Field(chain=["sessions", "$entry_utm_campaign"]) - case WebStatsBreakdown.InitialUTMMedium: + case WebStatsBreakdown.INITIAL_UTM_MEDIUM: return ast.Field(chain=["sessions", "$entry_utm_medium"]) - case WebStatsBreakdown.InitialUTMTerm: + case WebStatsBreakdown.INITIAL_UTM_TERM: return ast.Field(chain=["sessions", "$entry_utm_term"]) - case WebStatsBreakdown.InitialUTMContent: + case WebStatsBreakdown.INITIAL_UTM_CONTENT: return ast.Field(chain=["sessions", "$entry_utm_content"]) - case WebStatsBreakdown.InitialChannelType: + case WebStatsBreakdown.INITIAL_CHANNEL_TYPE: return ast.Field(chain=["sessions", "$channel_type"]) - case WebStatsBreakdown.Browser: + case WebStatsBreakdown.BROWSER: return ast.Field(chain=["properties", "$browser"]) case WebStatsBreakdown.OS: return ast.Field(chain=["properties", "$os"]) - case WebStatsBreakdown.DeviceType: + case WebStatsBreakdown.DEVICE_TYPE: return ast.Field(chain=["properties", "$device_type"]) - case WebStatsBreakdown.Country: + case WebStatsBreakdown.COUNTRY: return ast.Field(chain=["properties", "$geoip_country_code"]) - case WebStatsBreakdown.Region: + case WebStatsBreakdown.REGION: return parse_expr( "tuple(properties.$geoip_country_code, properties.$geoip_subdivision_1_code, properties.$geoip_subdivision_1_name)" ) - case WebStatsBreakdown.City: + case WebStatsBreakdown.CITY: return parse_expr("tuple(properties.$geoip_country_code, properties.$geoip_city_name)") case _: raise NotImplementedError("Breakdown not implemented") def where_breakdown(self): match self.query.breakdownBy: - case WebStatsBreakdown.Region: + case WebStatsBreakdown.REGION: return parse_expr("tupleElement(breakdown_value, 2) IS NOT NULL") - case WebStatsBreakdown.City: + case WebStatsBreakdown.CITY: return parse_expr("tupleElement(breakdown_value, 2) IS NOT NULL") - case WebStatsBreakdown.InitialChannelType: + case WebStatsBreakdown.INITIAL_CHANNEL_TYPE: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMSource: + case WebStatsBreakdown.INITIAL_UTM_SOURCE: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMCampaign: + case WebStatsBreakdown.INITIAL_UTM_CAMPAIGN: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMMedium: + case WebStatsBreakdown.INITIAL_UTM_MEDIUM: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMTerm: + case WebStatsBreakdown.INITIAL_UTM_TERM: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMContent: + case WebStatsBreakdown.INITIAL_UTM_CONTENT: return parse_expr("TRUE") # actually show null values case _: return parse_expr("breakdown_value IS NOT NULL") diff --git a/posthog/hogql_queries/web_analytics/stats_table_legacy.py b/posthog/hogql_queries/web_analytics/stats_table_legacy.py index edb72c39ae92e..5cb6a2a3c0889 100644 --- a/posthog/hogql_queries/web_analytics/stats_table_legacy.py +++ b/posthog/hogql_queries/web_analytics/stats_table_legacy.py @@ -71,7 +71,7 @@ def _scroll_depth_subquery(self): def to_query(self) -> ast.SelectQuery: # special case for channel, as some hogql features to use the general code are still being worked on - if self.query.breakdownBy == WebStatsBreakdown.InitialChannelType: + if self.query.breakdownBy == WebStatsBreakdown.INITIAL_CHANNEL_TYPE: query = self.to_channel_query() elif self.query.includeScrollDepth: query = parse_select( @@ -193,51 +193,51 @@ def calculate(self): def counts_breakdown(self): match self.query.breakdownBy: - case WebStatsBreakdown.Page: + case WebStatsBreakdown.PAGE: return self._apply_path_cleaning(ast.Field(chain=["properties", "$pathname"])) - case WebStatsBreakdown.InitialChannelType: + case WebStatsBreakdown.INITIAL_CHANNEL_TYPE: raise NotImplementedError("Breakdown InitialChannelType not implemented") - case WebStatsBreakdown.InitialPage: + case WebStatsBreakdown.INITIAL_PAGE: return self._apply_path_cleaning(ast.Field(chain=["person", "properties", "$initial_pathname"])) - case WebStatsBreakdown.InitialReferringDomain: + case WebStatsBreakdown.INITIAL_REFERRING_DOMAIN: return ast.Field(chain=["person", "properties", "$initial_referring_domain"]) - case WebStatsBreakdown.InitialUTMSource: + case WebStatsBreakdown.INITIAL_UTM_SOURCE: return ast.Field(chain=["person", "properties", "$initial_utm_source"]) - case WebStatsBreakdown.InitialUTMCampaign: + case WebStatsBreakdown.INITIAL_UTM_CAMPAIGN: return ast.Field(chain=["person", "properties", "$initial_utm_campaign"]) - case WebStatsBreakdown.InitialUTMMedium: + case WebStatsBreakdown.INITIAL_UTM_MEDIUM: return ast.Field(chain=["person", "properties", "$initial_utm_medium"]) - case WebStatsBreakdown.InitialUTMTerm: + case WebStatsBreakdown.INITIAL_UTM_TERM: return ast.Field(chain=["person", "properties", "$initial_utm_term"]) - case WebStatsBreakdown.InitialUTMContent: + case WebStatsBreakdown.INITIAL_UTM_CONTENT: return ast.Field(chain=["person", "properties", "$initial_utm_content"]) - case WebStatsBreakdown.Browser: + case WebStatsBreakdown.BROWSER: return ast.Field(chain=["properties", "$browser"]) case WebStatsBreakdown.OS: return ast.Field(chain=["properties", "$os"]) - case WebStatsBreakdown.DeviceType: + case WebStatsBreakdown.DEVICE_TYPE: return ast.Field(chain=["properties", "$device_type"]) - case WebStatsBreakdown.Country: + case WebStatsBreakdown.COUNTRY: return ast.Field(chain=["properties", "$geoip_country_code"]) - case WebStatsBreakdown.Region: + case WebStatsBreakdown.REGION: return parse_expr( "tuple(properties.$geoip_country_code, properties.$geoip_subdivision_1_code, properties.$geoip_subdivision_1_name)" ) - case WebStatsBreakdown.City: + case WebStatsBreakdown.CITY: return parse_expr("tuple(properties.$geoip_country_code, properties.$geoip_city_name)") case _: raise NotImplementedError("Breakdown not implemented") def bounce_breakdown(self): match self.query.breakdownBy: - case WebStatsBreakdown.Page: + case WebStatsBreakdown.PAGE: # use initial pathname for bounce rate return self._apply_path_cleaning( ast.Call(name="any", args=[ast.Field(chain=["person", "properties", "$initial_pathname"])]) ) - case WebStatsBreakdown.InitialChannelType: + case WebStatsBreakdown.INITIAL_CHANNEL_TYPE: raise NotImplementedError("Breakdown InitialChannelType not implemented") - case WebStatsBreakdown.InitialPage: + case WebStatsBreakdown.INITIAL_PAGE: return self._apply_path_cleaning( ast.Call(name="any", args=[ast.Field(chain=["person", "properties", "$initial_pathname"])]) ) @@ -246,21 +246,21 @@ def bounce_breakdown(self): def where_breakdown(self): match self.query.breakdownBy: - case WebStatsBreakdown.Region: + case WebStatsBreakdown.REGION: return parse_expr('tupleElement("context.columns.breakdown_value", 2) IS NOT NULL') - case WebStatsBreakdown.City: + case WebStatsBreakdown.CITY: return parse_expr('tupleElement("context.columns.breakdown_value", 2) IS NOT NULL') - case WebStatsBreakdown.InitialChannelType: + case WebStatsBreakdown.INITIAL_CHANNEL_TYPE: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMSource: + case WebStatsBreakdown.INITIAL_UTM_SOURCE: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMCampaign: + case WebStatsBreakdown.INITIAL_UTM_CAMPAIGN: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMMedium: + case WebStatsBreakdown.INITIAL_UTM_MEDIUM: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMTerm: + case WebStatsBreakdown.INITIAL_UTM_TERM: return parse_expr("TRUE") # actually show null values - case WebStatsBreakdown.InitialUTMContent: + case WebStatsBreakdown.INITIAL_UTM_CONTENT: return parse_expr("TRUE") # actually show null values case _: return parse_expr('"context.columns.breakdown_value" IS NOT NULL') diff --git a/posthog/hogql_queries/web_analytics/test/test_web_analytics_query_runner.py b/posthog/hogql_queries/web_analytics/test/test_web_analytics_query_runner.py index 3ea217606522c..87763dc288b61 100644 --- a/posthog/hogql_queries/web_analytics/test/test_web_analytics_query_runner.py +++ b/posthog/hogql_queries/web_analytics/test/test_web_analytics_query_runner.py @@ -48,7 +48,7 @@ def _create_events(self, data, event="$pageview"): ) return person_result - def _create_web_stats_table_query(self, date_from, date_to, properties, breakdown_by=WebStatsBreakdown.Page): + def _create_web_stats_table_query(self, date_from, date_to, properties, breakdown_by=WebStatsBreakdown.PAGE): query = WebStatsTableQuery( dateRange=DateRange(date_from=date_from, date_to=date_to), properties=properties, breakdownBy=breakdown_by ) @@ -63,8 +63,8 @@ def _create__web_overview_query(self, date_from, date_to, properties): def test_sample_rate_cache_key_is_same_across_subclasses(self): properties: list[Union[EventPropertyFilter, PersonPropertyFilter]] = [ - EventPropertyFilter(key="$current_url", value="/a", operator=PropertyOperator.is_not), - PersonPropertyFilter(key="$initial_utm_source", value="google", operator=PropertyOperator.is_not), + EventPropertyFilter(key="$current_url", value="/a", operator=PropertyOperator.IS_NOT), + PersonPropertyFilter(key="$initial_utm_source", value="google", operator=PropertyOperator.IS_NOT), ] date_from = "2023-12-08" date_to = "2023-12-15" @@ -76,10 +76,10 @@ def test_sample_rate_cache_key_is_same_across_subclasses(self): def test_sample_rate_cache_key_is_same_with_different_properties(self): properties_a: list[Union[EventPropertyFilter, PersonPropertyFilter]] = [ - EventPropertyFilter(key="$current_url", value="/a", operator=PropertyOperator.is_not), + EventPropertyFilter(key="$current_url", value="/a", operator=PropertyOperator.IS_NOT), ] properties_b: list[Union[EventPropertyFilter, PersonPropertyFilter]] = [ - EventPropertyFilter(key="$current_url", value="/b", operator=PropertyOperator.is_not), + EventPropertyFilter(key="$current_url", value="/b", operator=PropertyOperator.IS_NOT), ] date_from = "2023-12-08" date_to = "2023-12-15" @@ -91,7 +91,7 @@ def test_sample_rate_cache_key_is_same_with_different_properties(self): def test_sample_rate_cache_key_changes_with_date_range(self): properties: list[Union[EventPropertyFilter, PersonPropertyFilter]] = [ - EventPropertyFilter(key="$current_url", value="/a", operator=PropertyOperator.is_not), + EventPropertyFilter(key="$current_url", value="/a", operator=PropertyOperator.IS_NOT), ] date_from_a = "2023-12-08" date_from_b = "2023-12-09" diff --git a/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py b/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py index b753abdad0f64..c78010569a60b 100644 --- a/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py +++ b/posthog/hogql_queries/web_analytics/test/test_web_stats_table.py @@ -93,7 +93,7 @@ def _run_web_stats_table_query( self, date_from, date_to, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, limit=None, path_cleaning_filters=None, use_sessions_table=True, @@ -192,7 +192,7 @@ def test_breakdown_channel_type_doesnt_throw(self, use_sessions_table): results = self._run_web_stats_table_query( "2023-12-01", "2023-12-03", - breakdown_by=WebStatsBreakdown.InitialChannelType, + breakdown_by=WebStatsBreakdown.INITIAL_CHANNEL_TYPE, use_sessions_table=use_sessions_table, ).results @@ -279,7 +279,7 @@ def test_scroll_depth_bounce_rate_one_user(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_scroll_depth=True, include_bounce_rate=True, ).results @@ -322,7 +322,7 @@ def test_scroll_depth_bounce_rate(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_scroll_depth=True, include_bounce_rate=True, ).results @@ -365,10 +365,10 @@ def test_scroll_depth_bounce_rate_with_filter(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_scroll_depth=True, include_bounce_rate=True, - properties=[EventPropertyFilter(key="$pathname", operator=PropertyOperator.exact, value="/a")], + properties=[EventPropertyFilter(key="$pathname", operator=PropertyOperator.EXACT, value="/a")], ).results self.assertEqual( @@ -392,7 +392,7 @@ def test_scroll_depth_bounce_rate_path_cleaning(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_scroll_depth=True, include_bounce_rate=True, path_cleaning_filters=[ @@ -425,7 +425,7 @@ def test_bounce_rate_one_user(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_bounce_rate=True, ).results @@ -467,7 +467,7 @@ def test_bounce_rate(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_bounce_rate=True, ).results @@ -509,9 +509,9 @@ def test_bounce_rate_with_property(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_bounce_rate=True, - properties=[EventPropertyFilter(key="$pathname", operator=PropertyOperator.exact, value="/a")], + properties=[EventPropertyFilter(key="$pathname", operator=PropertyOperator.EXACT, value="/a")], ).results self.assertEqual( @@ -535,7 +535,7 @@ def test_bounce_rate_path_cleaning(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.Page, + breakdown_by=WebStatsBreakdown.PAGE, include_bounce_rate=True, path_cleaning_filters=[ {"regex": "\\/a\\/\\d+", "alias": "/a/:id"}, @@ -567,7 +567,7 @@ def test_entry_bounce_rate_one_user(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.InitialPage, + breakdown_by=WebStatsBreakdown.INITIAL_PAGE, include_bounce_rate=True, ).results @@ -607,7 +607,7 @@ def test_entry_bounce_rate(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.InitialPage, + breakdown_by=WebStatsBreakdown.INITIAL_PAGE, include_bounce_rate=True, ).results @@ -647,9 +647,9 @@ def test_entry_bounce_rate_with_property(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.InitialPage, + breakdown_by=WebStatsBreakdown.INITIAL_PAGE, include_bounce_rate=True, - properties=[EventPropertyFilter(key="$pathname", operator=PropertyOperator.exact, value="/a")], + properties=[EventPropertyFilter(key="$pathname", operator=PropertyOperator.EXACT, value="/a")], ).results self.assertEqual( @@ -673,7 +673,7 @@ def test_entry_bounce_rate_path_cleaning(self): "all", "2023-12-15", use_sessions_table=True, - breakdown_by=WebStatsBreakdown.InitialPage, + breakdown_by=WebStatsBreakdown.INITIAL_PAGE, include_bounce_rate=True, path_cleaning_filters=[ {"regex": "\\/a\\/\\d+", "alias": "/a/:id"}, diff --git a/posthog/jwt.py b/posthog/jwt.py index 62710d9159b86..ead4196aa4730 100644 --- a/posthog/jwt.py +++ b/posthog/jwt.py @@ -10,7 +10,7 @@ class PosthogJwtAudience(Enum): UNSUBSCRIBE = "posthog:unsubscribe" EXPORTED_ASSET = "posthog:exported_asset" IMPERSONATED_USER = "posthog:impersonted_user" # This is used by background jobs on behalf of the user e.g. exports - LIVE_EVENTS = "posthog:live_events" + LIVESTREAM = "posthog:livestream" def encode_jwt(payload: dict, expiry_delta: timedelta, audience: PosthogJwtAudience) -> str: diff --git a/posthog/management/commands/compare_hogql_insights.py b/posthog/management/commands/compare_hogql_insights.py index 44bab4f6e7127..9a49af107e063 100644 --- a/posthog/management/commands/compare_hogql_insights.py +++ b/posthog/management/commands/compare_hogql_insights.py @@ -38,7 +38,9 @@ def handle(self, *args, **options): if event.get("math") in ("median", "p90", "p95", "p99"): event["math"] = "sum" try: - print("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++") # noqa: T201 + print( # noqa: T201 + "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + ) insight_type = insight.filters.get("insight") print( # noqa: T201 f"Checking {insight_type} Insight {insight.id} {insight.short_id} - {insight.name} " @@ -58,7 +60,7 @@ def handle(self, *args, **options): continue try: query = filter_to_query(insight.filters) - modifiers = HogQLQueryModifiers(materializationMode=MaterializationMode.legacy_null_as_string) + modifiers = HogQLQueryModifiers(materializationMode=MaterializationMode.LEGACY_NULL_AS_STRING) query_runner = get_query_runner(query, insight.team, modifiers=modifiers) hogql_results = cast(HogQLQueryResponse, query_runner.calculate()).results or [] except Exception as e: diff --git a/posthog/management/commands/start_temporal_worker.py b/posthog/management/commands/start_temporal_worker.py index 3fb2e0444e87f..1b79e594323e7 100644 --- a/posthog/management/commands/start_temporal_worker.py +++ b/posthog/management/commands/start_temporal_worker.py @@ -92,7 +92,6 @@ def handle(self, *args, **options): logging.info(f"Starting Temporal Worker with options: {options}") structlog.reset_defaults() - metrics_port = int(options["metrics_port"]) asyncio.run( diff --git a/posthog/models/__init__.py b/posthog/models/__init__.py index d0f9dcf893c80..389d573cb7e7b 100644 --- a/posthog/models/__init__.py +++ b/posthog/models/__init__.py @@ -40,6 +40,7 @@ from .filters import Filter, RetentionFilter from .group import Group from .group_type_mapping import GroupTypeMapping +from .hog_functions import HogFunction from .insight import Insight, InsightViewed from .insight_caching_state import InsightCachingState from .instance_setting import InstanceSetting @@ -103,6 +104,7 @@ "Filter", "Group", "GroupTypeMapping", + "HogFunction", "Insight", "InsightCachingState", "InsightViewed", diff --git a/posthog/models/team/team.py b/posthog/models/team/team.py index 382431171680e..e06104ce1c2a6 100644 --- a/posthog/models/team/team.py +++ b/posthog/models/team/team.py @@ -302,31 +302,31 @@ def default_modifiers(self) -> dict: @property def person_on_events_mode(self) -> PersonsOnEventsMode: if self._person_on_events_person_id_override_properties_on_events: - tag_queries(person_on_events_mode=PersonsOnEventsMode.person_id_override_properties_on_events) - return PersonsOnEventsMode.person_id_override_properties_on_events + tag_queries(person_on_events_mode=PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS) + return PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS if self._person_on_events_person_id_no_override_properties_on_events: # also tag person_on_events_enabled for legacy compatibility tag_queries( person_on_events_enabled=True, - person_on_events_mode=PersonsOnEventsMode.person_id_no_override_properties_on_events, + person_on_events_mode=PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, ) - return PersonsOnEventsMode.person_id_no_override_properties_on_events + return PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS if self._person_on_events_person_id_override_properties_joined: tag_queries( person_on_events_enabled=True, - person_on_events_mode=PersonsOnEventsMode.person_id_override_properties_joined, + person_on_events_mode=PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED, ) - return PersonsOnEventsMode.person_id_override_properties_joined + return PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED - return PersonsOnEventsMode.disabled + return PersonsOnEventsMode.DISABLED # KLUDGE: DO NOT REFERENCE IN THE BACKEND! # Keeping this property for now only to be used by the frontend in certain cases @property def person_on_events_querying_enabled(self) -> bool: - return self.person_on_events_mode != PersonsOnEventsMode.disabled + return self.person_on_events_mode != PersonsOnEventsMode.DISABLED @property def _person_on_events_person_id_no_override_properties_on_events(self) -> bool: diff --git a/posthog/permissions.py b/posthog/permissions.py index db5c48d92f25b..72fd657a9ad4f 100644 --- a/posthog/permissions.py +++ b/posthog/permissions.py @@ -5,6 +5,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db.models import Model from django.views import View +import posthoganalytics from rest_framework.exceptions import NotFound, PermissionDenied from rest_framework.permissions import SAFE_METHODS, BasePermission, IsAdminUser from rest_framework.request import Request @@ -404,3 +405,39 @@ def get_scope_object(self, request, view) -> APIScopeObjectOrNotSupported: raise ImproperlyConfigured("APIScopePermission requires the view to define the scope_object attribute.") return view.scope_object + + +class PostHogFeatureFlagPermission(BasePermission): + def has_permission(self, request, view) -> bool: + user = cast(User, request.user) + organization = get_organization_from_view(view) + flag = getattr(view, "posthog_feature_flag", None) + + config = {} + + if not flag: + raise ImproperlyConfigured( + "PostHogFeatureFlagPermission requires the view to define the posthog_feature_flag attribute." + ) + + if isinstance(flag, str): + config[flag] = ["*"] + else: + config = flag + + for required_flag, actions in config.items(): + if "*" in actions or view.action in actions: + org_id = str(organization.id) + + enabled = posthoganalytics.feature_enabled( + required_flag, + user.distinct_id, + groups={"organization": org_id}, + group_properties={"organization": {"id": org_id}}, + only_evaluate_locally=False, + send_feature_flag_events=False, + ) + + return enabled or False + + return True diff --git a/posthog/queries/breakdown_props.py b/posthog/queries/breakdown_props.py index 767985e261109..23f4b0d51ddc4 100644 --- a/posthog/queries/breakdown_props.py +++ b/posthog/queries/breakdown_props.py @@ -86,7 +86,7 @@ def get_breakdown_prop_values( sessions_join_params: dict = {} null_person_filter = ( - f"AND notEmpty(e.person_id)" if team.person_on_events_mode != PersonsOnEventsMode.disabled else "" + f"AND notEmpty(e.person_id)" if team.person_on_events_mode != PersonsOnEventsMode.DISABLED else "" ) if person_properties_mode == PersonPropertiesMode.DIRECT_ON_EVENTS: @@ -277,12 +277,14 @@ def _to_value_expression( table="events" if direct_on_events else "groups", property_name=cast(str, breakdown), var="%(key)s", - column=f"group{breakdown_group_type_index}_properties" - if direct_on_events - else f"group_properties_{breakdown_group_type_index}", - materialised_table_column=f"group{breakdown_group_type_index}_properties" - if direct_on_events - else "group_properties", + column=( + f"group{breakdown_group_type_index}_properties" + if direct_on_events + else f"group_properties_{breakdown_group_type_index}" + ), + materialised_table_column=( + f"group{breakdown_group_type_index}_properties" if direct_on_events else "group_properties" + ), ) elif breakdown_type == "hogql": from posthog.hogql.hogql import translate_hogql diff --git a/posthog/queries/event_query/event_query.py b/posthog/queries/event_query/event_query.py index cc514c676f817..d8816634d6ac1 100644 --- a/posthog/queries/event_query/event_query.py +++ b/posthog/queries/event_query/event_query.py @@ -64,7 +64,7 @@ def __init__( extra_event_properties: Optional[list[PropertyName]] = None, extra_person_fields: Optional[list[ColumnName]] = None, override_aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, **kwargs, ) -> None: if extra_person_fields is None: @@ -126,9 +126,9 @@ def _determine_should_join_distinct_ids(self) -> None: pass def _get_person_id_alias(self, person_on_events_mode) -> str: - if person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: return f"if(notEmpty({self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.distinct_id), {self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id, {self.EVENT_TABLE_ALIAS}.person_id)" - elif person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + elif person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: return f"{self.EVENT_TABLE_ALIAS}.person_id" return f"{self.DISTINCT_ID_TABLE_ALIAS}.person_id" @@ -137,7 +137,7 @@ def _get_person_ids_query(self, *, relevant_events_conditions: str = "") -> str: if not self._should_join_distinct_ids: return "" - if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: return PERSON_DISTINCT_ID_OVERRIDES_JOIN_SQL.format( person_overrides_table_alias=self.PERSON_ID_OVERRIDES_TABLE_ALIAS, event_table_alias=self.EVENT_TABLE_ALIAS, diff --git a/posthog/queries/foss_cohort_query.py b/posthog/queries/foss_cohort_query.py index e801008c44726..d4925856afd94 100644 --- a/posthog/queries/foss_cohort_query.py +++ b/posthog/queries/foss_cohort_query.py @@ -191,9 +191,11 @@ def _unwrap(property_group: PropertyGroup, negate_group: bool = False) -> Proper ) else: return PropertyGroup( - type=PropertyOperatorType.AND - if property_group.type == PropertyOperatorType.OR - else PropertyOperatorType.OR, + type=( + PropertyOperatorType.AND + if property_group.type == PropertyOperatorType.OR + else PropertyOperatorType.OR + ), values=[_unwrap(v, True) for v in cast(list[PropertyGroup], property_group.values)], ) @@ -246,9 +248,11 @@ def _unwrap(property_group: PropertyGroup, negate_group: bool = False) -> Proper return PropertyGroup(type=property_group.type, values=new_property_group_list) else: return PropertyGroup( - type=PropertyOperatorType.AND - if property_group.type == PropertyOperatorType.OR - else PropertyOperatorType.OR, + type=( + PropertyOperatorType.AND + if property_group.type == PropertyOperatorType.OR + else PropertyOperatorType.OR + ), values=new_property_group_list, ) @@ -306,7 +310,7 @@ def _build_sources(self, subq: list[tuple[str, str]]) -> tuple[str, str]: fields = f"{subq_alias}.person_id" elif prev_alias: # can't join without a previous alias if subq_alias == self.PERSON_TABLE_ALIAS and self.should_pushdown_persons: - if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: # when using person-on-events, instead of inner join, we filter inside # the event query itself continue @@ -337,11 +341,11 @@ def _get_behavior_subquery(self) -> tuple[str, dict[str, Any], str]: query, params = "", {} if self._should_join_behavioral_query: _fields = [ - f"{self.DISTINCT_ID_TABLE_ALIAS if self._person_on_events_mode == PersonsOnEventsMode.disabled else self.EVENT_TABLE_ALIAS}.person_id AS person_id" + f"{self.DISTINCT_ID_TABLE_ALIAS if self._person_on_events_mode == PersonsOnEventsMode.DISABLED else self.EVENT_TABLE_ALIAS}.person_id AS person_id" ] _fields.extend(self._fields) - if self.should_pushdown_persons and self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self.should_pushdown_persons and self._person_on_events_mode != PersonsOnEventsMode.DISABLED: person_prop_query, person_prop_params = self._get_prop_groups( self._inner_property_groups, person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS, @@ -557,7 +561,7 @@ def get_performed_event_multiple(self, prop: Property, prepend: str, idx: int) - def _determine_should_join_distinct_ids(self) -> None: self._should_join_distinct_ids = ( - self._person_on_events_mode != PersonsOnEventsMode.person_id_no_override_properties_on_events + self._person_on_events_mode != PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS ) def _determine_should_join_persons(self) -> None: diff --git a/posthog/queries/funnels/base.py b/posthog/queries/funnels/base.py index a6de14b050cfa..30265cace41e3 100644 --- a/posthog/queries/funnels/base.py +++ b/posthog/queries/funnels/base.py @@ -228,9 +228,11 @@ def _format_single_funnel(self, results, with_breakdown=False): # breakdown_value will return the underlying id if different from display ready value (ex: cohort id) serialized_result.update( { - "breakdown": get_breakdown_cohort_name(breakdown_value) - if self._filter.breakdown_type == "cohort" - else breakdown_value, + "breakdown": ( + get_breakdown_cohort_name(breakdown_value) + if self._filter.breakdown_type == "cohort" + else breakdown_value + ), "breakdown_value": breakdown_value, } ) @@ -728,7 +730,7 @@ def _get_breakdown_select_prop(self) -> tuple[str, dict[str, Any]]: self.params.update({"breakdown": self._filter.breakdown}) if self._filter.breakdown_type == "person": - if self._team.person_on_events_mode != PersonsOnEventsMode.disabled: + if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED: basic_prop_selector, basic_prop_params = get_single_or_multi_property_string_expr( self._filter.breakdown, table="events", @@ -758,7 +760,7 @@ def _get_breakdown_select_prop(self) -> tuple[str, dict[str, Any]]: # :TRICKY: We only support string breakdown for group properties assert isinstance(self._filter.breakdown, str) - if self._team.person_on_events_mode != PersonsOnEventsMode.disabled and groups_on_events_querying_enabled(): + if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED and groups_on_events_querying_enabled(): properties_field = f"group{self._filter.breakdown_group_type_index}_properties" expression, _ = get_property_string_expr( table="events", diff --git a/posthog/queries/funnels/funnel_event_query.py b/posthog/queries/funnels/funnel_event_query.py index a814f9cb54801..a99f456a33eea 100644 --- a/posthog/queries/funnels/funnel_event_query.py +++ b/posthog/queries/funnels/funnel_event_query.py @@ -49,7 +49,7 @@ def get_query( _fields += [f"{self.EVENT_TABLE_ALIAS}.{field} AS {field}" for field in self._extra_fields] - if self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED: _fields += [f"{self._person_id_alias} as person_id"] _fields.extend( @@ -95,7 +95,7 @@ def get_query( null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonsOnEventsMode.disabled + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED else "" ) @@ -131,9 +131,9 @@ def _determine_should_join_distinct_ids(self) -> None: ) is_using_cohort_propertes = self._column_optimizer.is_using_cohort_propertes - if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: self._should_join_distinct_ids = True - elif self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events or ( + elif self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS or ( non_person_id_aggregation and not is_using_cohort_propertes ): self._should_join_distinct_ids = False @@ -142,7 +142,7 @@ def _determine_should_join_distinct_ids(self) -> None: def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED: self._should_join_persons = False def _get_entity_query(self, entities=None, entity_name="events") -> tuple[str, dict[str, Any]]: diff --git a/posthog/queries/groups_join_query/groups_join_query.py b/posthog/queries/groups_join_query/groups_join_query.py index 6499d39ce1e94..128398584a352 100644 --- a/posthog/queries/groups_join_query/groups_join_query.py +++ b/posthog/queries/groups_join_query/groups_join_query.py @@ -23,7 +23,7 @@ def __init__( team_id: int, column_optimizer: Optional[ColumnOptimizer] = None, join_key: Optional[str] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, ) -> None: self._filter = filter self._team_id = team_id diff --git a/posthog/queries/paths/paths_event_query.py b/posthog/queries/paths/paths_event_query.py index 31241cea64919..6380c43aae72c 100644 --- a/posthog/queries/paths/paths_event_query.py +++ b/posthog/queries/paths/paths_event_query.py @@ -116,7 +116,7 @@ def get_query(self) -> tuple[str, dict[str, Any]]: null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonsOnEventsMode.disabled + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED else "" ) @@ -141,14 +141,14 @@ def get_query(self) -> tuple[str, dict[str, Any]]: return query, self.params def _determine_should_join_distinct_ids(self) -> None: - if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: self._should_join_distinct_ids = False else: self._should_join_distinct_ids = True def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED: self._should_join_persons = False def _get_grouping_fields(self) -> tuple[list[str], dict[str, Any]]: diff --git a/posthog/queries/retention/retention.py b/posthog/queries/retention/retention.py index d3b9f43ca5c60..5d0779071ffe4 100644 --- a/posthog/queries/retention/retention.py +++ b/posthog/queries/retention/retention.py @@ -166,7 +166,7 @@ def build_returning_event_query( filter: RetentionFilter, team: Team, aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, retention_events_query=RetentionEventsQuery, ) -> tuple[str, dict[str, Any]]: returning_event_query_templated, returning_event_params = retention_events_query( @@ -184,7 +184,7 @@ def build_target_event_query( filter: RetentionFilter, team: Team, aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, retention_events_query=RetentionEventsQuery, ) -> tuple[str, dict[str, Any]]: target_event_query_templated, target_event_params = retention_events_query( diff --git a/posthog/queries/retention/retention_events_query.py b/posthog/queries/retention/retention_events_query.py index 9e64b758be6e8..ed9b45d47b584 100644 --- a/posthog/queries/retention/retention_events_query.py +++ b/posthog/queries/retention/retention_events_query.py @@ -27,7 +27,7 @@ def __init__( event_query_type: RetentionQueryType, team: Team, aggregate_users_by_distinct_id: Optional[bool] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, ): self._event_query_type = event_query_type super().__init__( @@ -56,14 +56,14 @@ def get_query(self) -> tuple[str, dict[str, Any]]: materalised_table_column = "properties" if breakdown_type == "person": - table = "person" if self._person_on_events_mode == PersonsOnEventsMode.disabled else "events" + table = "person" if self._person_on_events_mode == PersonsOnEventsMode.DISABLED else "events" column = ( "person_props" - if self._person_on_events_mode == PersonsOnEventsMode.disabled + if self._person_on_events_mode == PersonsOnEventsMode.DISABLED else "person_properties" ) materalised_table_column = ( - "properties" if self._person_on_events_mode == PersonsOnEventsMode.disabled else "person_properties" + "properties" if self._person_on_events_mode == PersonsOnEventsMode.DISABLED else "person_properties" ) breakdown_values_expression, breakdown_values_params = get_single_or_multi_property_string_expr( @@ -134,10 +134,12 @@ def get_query(self) -> tuple[str, dict[str, Any]]: self.params.update(prop_params) entity_query, entity_params = self._get_entity_query( - entity=self._filter.target_entity - if self._event_query_type == RetentionQueryType.TARGET - or self._event_query_type == RetentionQueryType.TARGET_FIRST_TIME - else self._filter.returning_entity + entity=( + self._filter.target_entity + if self._event_query_type == RetentionQueryType.TARGET + or self._event_query_type == RetentionQueryType.TARGET_FIRST_TIME + else self._filter.returning_entity + ) ) self.params.update(entity_params) @@ -149,7 +151,7 @@ def get_query(self) -> tuple[str, dict[str, Any]]: null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonsOnEventsMode.disabled + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED else "" ) @@ -197,7 +199,7 @@ def _determine_should_join_distinct_ids(self) -> None: self._filter.aggregation_group_type_index is not None or self._aggregate_users_by_distinct_id ) is_using_cohort_propertes = self._column_optimizer.is_using_cohort_propertes - if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events or ( + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS or ( non_person_id_aggregation and not is_using_cohort_propertes ): self._should_join_distinct_ids = False @@ -206,7 +208,7 @@ def _determine_should_join_distinct_ids(self) -> None: def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED: self._should_join_persons = False def _get_entity_query(self, entity: Entity): diff --git a/posthog/queries/stickiness/stickiness_event_query.py b/posthog/queries/stickiness/stickiness_event_query.py index 7c8c92222ef95..8810bed83c6ad 100644 --- a/posthog/queries/stickiness/stickiness_event_query.py +++ b/posthog/queries/stickiness/stickiness_event_query.py @@ -43,7 +43,7 @@ def get_query(self) -> tuple[str, dict[str, Any]]: null_person_filter = ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonsOnEventsMode.disabled + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED else "" ) @@ -82,14 +82,14 @@ def _person_query(self): ) def _determine_should_join_distinct_ids(self) -> None: - if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: self._should_join_distinct_ids = False else: self._should_join_distinct_ids = True def _determine_should_join_persons(self) -> None: EventQuery._determine_should_join_persons(self) - if self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED: self._should_join_persons = False def aggregation_target(self): diff --git a/posthog/queries/trends/breakdown.py b/posthog/queries/trends/breakdown.py index fb4c7f2cca2b3..db6fd0860c38f 100644 --- a/posthog/queries/trends/breakdown.py +++ b/posthog/queries/trends/breakdown.py @@ -98,7 +98,7 @@ def __init__( filter: Filter, team: Team, column_optimizer: Optional[ColumnOptimizer] = None, - person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.disabled, + person_on_events_mode: PersonsOnEventsMode = PersonsOnEventsMode.DISABLED, add_person_urls: bool = False, ): self.entity = entity @@ -109,9 +109,9 @@ def __init__( self.column_optimizer = column_optimizer or ColumnOptimizer(self.filter, self.team_id) self.add_person_urls = add_person_urls self.person_on_events_mode = person_on_events_mode - if person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: self._person_id_alias = f"if(notEmpty({self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.distinct_id), {self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id, {self.EVENT_TABLE_ALIAS}.person_id)" - elif person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + elif person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: self._person_id_alias = f"{self.EVENT_TABLE_ALIAS}.person_id" else: self._person_id_alias = f"{self.DISTINCT_ID_TABLE_ALIAS}.person_id" @@ -129,7 +129,7 @@ def _props_to_filter(self) -> tuple[str, dict]: ) target_properties: Optional[PropertyGroup] = props_to_filter - if self.person_on_events_mode == PersonsOnEventsMode.disabled: + if self.person_on_events_mode == PersonsOnEventsMode.DISABLED: target_properties = self.column_optimizer.property_optimizer.parse_property_groups(props_to_filter).outer return parse_prop_grouped_clauses( @@ -160,9 +160,11 @@ def get_query(self) -> tuple[str, dict, Callable]: self.team, filter=self.filter, event_table_alias=self.EVENT_TABLE_ALIAS, - person_id_alias=f"person_id" - if self.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events - else self._person_id_alias, + person_id_alias=( + f"person_id" + if self.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS + else self._person_id_alias + ), ) action_query = "" @@ -193,13 +195,15 @@ def get_query(self) -> tuple[str, dict, Callable]: "parsed_date_from": parsed_date_from, "parsed_date_to": parsed_date_to, "actions_query": "AND {}".format(action_query) if action_query else "", - "event_filter": "AND event = %(event)s" - if self.entity.type == TREND_FILTER_TYPE_EVENTS and self.entity.id is not None - else "", + "event_filter": ( + "AND event = %(event)s" + if self.entity.type == TREND_FILTER_TYPE_EVENTS and self.entity.id is not None + else "" + ), "filters": prop_filters, - "null_person_filter": f"AND notEmpty(e.person_id)" - if self.person_on_events_mode != PersonsOnEventsMode.disabled - else "", + "null_person_filter": ( + f"AND notEmpty(e.person_id)" if self.person_on_events_mode != PersonsOnEventsMode.DISABLED else "" + ), } _params, _breakdown_filter_params = {}, {} @@ -491,9 +495,11 @@ def _breakdown_prop_params(self, aggregate_operation: str, math_params: dict): return ( { - "values": [*values_arr, breakdown_other_value] - if has_more_values and not self.filter.breakdown_hide_other_aggregation - else values_arr, + "values": ( + [*values_arr, breakdown_other_value] + if has_more_values and not self.filter.breakdown_hide_other_aggregation + else values_arr + ), "breakdown_other_value": breakdown_other_value, "breakdown_null_value": breakdown_null_value, }, @@ -520,7 +526,7 @@ def _get_breakdown_value(self, breakdown: str) -> str: raise ValidationError(f'Invalid breakdown "{breakdown}" for breakdown type "session"') elif ( - self.person_on_events_mode != PersonsOnEventsMode.disabled + self.person_on_events_mode != PersonsOnEventsMode.DISABLED and self.filter.breakdown_type == "group" and groups_on_events_querying_enabled() ): @@ -532,7 +538,7 @@ def _get_breakdown_value(self, breakdown: str) -> str: properties_field, materialised_table_column=properties_field, ) - elif self.person_on_events_mode != PersonsOnEventsMode.disabled and self.filter.breakdown_type != "group": + elif self.person_on_events_mode != PersonsOnEventsMode.DISABLED and self.filter.breakdown_type != "group": if self.filter.breakdown_type == "person": breakdown_value, _ = get_property_string_expr( "events", @@ -626,11 +632,11 @@ def _parse(result: list) -> list: } parsed_params: dict[str, str] = encode_get_request_params({**filter_params, **extra_params}) parsed_result = { - "aggregated_value": float( - correct_result_for_sampling(aggregated_value, filter.sampling_factor, entity.math) - ) - if aggregated_value is not None - else None, + "aggregated_value": ( + float(correct_result_for_sampling(aggregated_value, filter.sampling_factor, entity.math)) + if aggregated_value is not None + else None + ), "filter": filter_params, "persons": { "filter": extra_params, @@ -746,10 +752,10 @@ def _determine_breakdown_label( return str(value) or BREAKDOWN_NULL_DISPLAY def _person_join_condition(self) -> tuple[str, dict]: - if self.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + if self.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: return "", {} - if self.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if self.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: return ( PERSON_DISTINCT_ID_OVERRIDES_JOIN_SQL.format( person_overrides_table_alias=self.PERSON_ID_OVERRIDES_TABLE_ALIAS, diff --git a/posthog/queries/trends/lifecycle.py b/posthog/queries/trends/lifecycle.py index 199e3c57973b6..141df25134d1d 100644 --- a/posthog/queries/trends/lifecycle.py +++ b/posthog/queries/trends/lifecycle.py @@ -126,12 +126,12 @@ def get_query(self): self.params.update(entity_prop_params) created_at_clause = ( - "person.created_at" if self._person_on_events_mode == PersonsOnEventsMode.disabled else "person_created_at" + "person.created_at" if self._person_on_events_mode == PersonsOnEventsMode.DISABLED else "person_created_at" ) null_person_filter = ( "" - if self._person_on_events_mode == PersonsOnEventsMode.disabled + if self._person_on_events_mode == PersonsOnEventsMode.DISABLED else f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" ) @@ -187,8 +187,8 @@ def _get_date_filter(self): def _determine_should_join_distinct_ids(self) -> None: self._should_join_distinct_ids = ( - self._person_on_events_mode != PersonsOnEventsMode.person_id_no_override_properties_on_events + self._person_on_events_mode != PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS ) def _determine_should_join_persons(self) -> None: - self._should_join_persons = self._person_on_events_mode == PersonsOnEventsMode.disabled + self._should_join_persons = self._person_on_events_mode == PersonsOnEventsMode.DISABLED diff --git a/posthog/queries/trends/total_volume.py b/posthog/queries/trends/total_volume.py index 5e91d9272cf18..355b3c1d4e721 100644 --- a/posthog/queries/trends/total_volume.py +++ b/posthog/queries/trends/total_volume.py @@ -53,9 +53,9 @@ def _total_volume_query(self, entity: Entity, filter: Filter, team: Team) -> tup interval_func = get_interval_func_ch(filter.interval) person_id_alias = f"{self.DISTINCT_ID_TABLE_ALIAS}.person_id" - if team.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if team.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: person_id_alias = f"if(notEmpty({self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id), {self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id, {self.EVENT_TABLE_ALIAS}.person_id)" - elif team.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + elif team.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: person_id_alias = f"{self.EVENT_TABLE_ALIAS}.person_id" aggregate_operation, join_condition, math_params = process_math( @@ -70,10 +70,12 @@ def _total_volume_query(self, entity: Entity, filter: Filter, team: Team) -> tup filter=filter, entity=entity, team=team, - should_join_distinct_ids=True - if join_condition != "" - or (entity.math in [WEEKLY_ACTIVE, MONTHLY_ACTIVE] and not team.aggregate_users_by_distinct_id) - else False, + should_join_distinct_ids=( + True + if join_condition != "" + or (entity.math in [WEEKLY_ACTIVE, MONTHLY_ACTIVE] and not team.aggregate_users_by_distinct_id) + else False + ), person_on_events_mode=team.person_on_events_mode, ) event_query_base, event_query_params = trend_event_query.get_query_base() diff --git a/posthog/queries/trends/trends_actors.py b/posthog/queries/trends/trends_actors.py index f7db8b36d8ac3..f69db981d309a 100644 --- a/posthog/queries/trends/trends_actors.py +++ b/posthog/queries/trends/trends_actors.py @@ -61,18 +61,18 @@ def actor_query(self, limit_actors: Optional[bool] = True) -> tuple[str, dict]: value=lower_bound, operator="gte", type=self._filter.breakdown_type, - group_type_index=self._filter.breakdown_group_type_index - if self._filter.breakdown_type == "group" - else None, + group_type_index=( + self._filter.breakdown_group_type_index if self._filter.breakdown_type == "group" else None + ), ), Property( key=self._filter.breakdown, value=upper_bound, operator="lt", type=self._filter.breakdown_type, - group_type_index=self._filter.breakdown_group_type_index - if self._filter.breakdown_type == "group" - else None, + group_type_index=( + self._filter.breakdown_group_type_index if self._filter.breakdown_type == "group" else None + ), ), ] else: @@ -81,9 +81,9 @@ def actor_query(self, limit_actors: Optional[bool] = True) -> tuple[str, dict]: key=self._filter.breakdown, value=self._filter.breakdown_value, type=self._filter.breakdown_type, - group_type_index=self._filter.breakdown_group_type_index - if self._filter.breakdown_type == "group" - else None, + group_type_index=( + self._filter.breakdown_group_type_index if self._filter.breakdown_type == "group" else None + ), ) ] @@ -104,7 +104,7 @@ def actor_query(self, limit_actors: Optional[bool] = True) -> tuple[str, dict]: team=self._team, entity=self.entity, should_join_distinct_ids=not self.is_aggregating_by_groups - and self._team.person_on_events_mode != PersonsOnEventsMode.person_id_no_override_properties_on_events, + and self._team.person_on_events_mode != PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS, extra_event_properties=["$window_id", "$session_id"] if self._filter.include_recordings else [], extra_fields=extra_fields, person_on_events_mode=self._team.person_on_events_mode, diff --git a/posthog/queries/trends/trends_event_query.py b/posthog/queries/trends/trends_event_query.py index b856cb6a035e5..837a8a352a334 100644 --- a/posthog/queries/trends/trends_event_query.py +++ b/posthog/queries/trends/trends_event_query.py @@ -10,7 +10,7 @@ def get_query(self) -> tuple[str, dict[str, Any]]: person_id_field = "" if self._should_join_distinct_ids: person_id_field = f", {self._person_id_alias} as person_id" - elif self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + elif self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: person_id_field = f", {self.EVENT_TABLE_ALIAS}.person_id as person_id" _fields = ( @@ -62,7 +62,7 @@ def get_query(self) -> tuple[str, dict[str, Any]]: return f"SELECT {_fields} {base_query}", params def _get_extra_person_columns(self) -> str: - if self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED: return " ".join( ", {extract} as {column_name}".format( extract=get_property_string_expr( diff --git a/posthog/queries/trends/trends_event_query_base.py b/posthog/queries/trends/trends_event_query_base.py index 8fb17d3579e8f..588307e1a7723 100644 --- a/posthog/queries/trends/trends_event_query_base.py +++ b/posthog/queries/trends/trends_event_query_base.py @@ -80,7 +80,7 @@ def get_query_base(self) -> tuple[str, dict[str, Any]]: return query, self.params def _determine_should_join_distinct_ids(self) -> None: - if self._person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: self._should_join_distinct_ids = False is_entity_per_user = self._entity.math in ( @@ -97,7 +97,7 @@ def _determine_should_join_distinct_ids(self) -> None: self._should_join_distinct_ids = True def _determine_should_join_persons(self) -> None: - if self._person_on_events_mode != PersonsOnEventsMode.disabled: + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED: self._should_join_persons = False else: EventQuery._determine_should_join_persons(self) @@ -107,7 +107,7 @@ def _get_not_null_actor_condition(self) -> str: # If aggregating by person, exclude events with null/zero person IDs return ( f"AND notEmpty({self.EVENT_TABLE_ALIAS}.person_id)" - if self._person_on_events_mode != PersonsOnEventsMode.disabled + if self._person_on_events_mode != PersonsOnEventsMode.DISABLED else "" ) else: diff --git a/posthog/queries/trends/util.py b/posthog/queries/trends/util.py index 5f4cef63da072..09f34ff135633 100644 --- a/posthog/queries/trends/util.py +++ b/posthog/queries/trends/util.py @@ -176,9 +176,9 @@ def determine_aggregator(entity: Entity, team: Team) -> str: return f'"$group_{entity.math_group_type_index}"' elif team.aggregate_users_by_distinct_id: return "e.distinct_id" - elif team.person_on_events_mode == PersonsOnEventsMode.person_id_no_override_properties_on_events: + elif team.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: return "e.person_id" - elif team.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + elif team.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: return f"if(notEmpty(overrides.distinct_id), overrides.person_id, e.person_id)" else: return "pdi.person_id" diff --git a/posthog/queries/util.py b/posthog/queries/util.py index 5cbeea74716b0..44dac7dd8fdb9 100644 --- a/posthog/queries/util.py +++ b/posthog/queries/util.py @@ -178,10 +178,10 @@ def correct_result_for_sampling( def get_person_properties_mode(team: Team) -> PersonPropertiesMode: - if team.person_on_events_mode == PersonsOnEventsMode.disabled: + if team.person_on_events_mode == PersonsOnEventsMode.DISABLED: return PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN - if team.person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if team.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: return PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 return PersonPropertiesMode.DIRECT_ON_EVENTS diff --git a/posthog/schema.py b/posthog/schema.py index ed138547bf66b..91a96d1c96edd 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -13,50 +13,50 @@ class SchemaRoot(RootModel[Any]): class MathGroupTypeIndex(float, Enum): - number_0 = 0 - number_1 = 1 - number_2 = 2 - number_3 = 3 - number_4 = 4 + NUMBER_0 = 0 + NUMBER_1 = 1 + NUMBER_2 = 2 + NUMBER_3 = 3 + NUMBER_4 = 4 class AggregationAxisFormat(str, Enum): - numeric = "numeric" - duration = "duration" - duration_ms = "duration_ms" - percentage = "percentage" - percentage_scaled = "percentage_scaled" + NUMERIC = "numeric" + DURATION = "duration" + DURATION_MS = "duration_ms" + PERCENTAGE = "percentage" + PERCENTAGE_SCALED = "percentage_scaled" class Kind(str, Enum): - Method = "Method" - Function = "Function" - Constructor = "Constructor" - Field = "Field" - Variable = "Variable" - Class = "Class" - Struct = "Struct" - Interface = "Interface" - Module = "Module" - Property = "Property" - Event = "Event" - Operator = "Operator" - Unit = "Unit" - Value = "Value" - Constant = "Constant" - Enum = "Enum" - EnumMember = "EnumMember" - Keyword = "Keyword" - Text = "Text" - Color = "Color" - File = "File" - Reference = "Reference" - Customcolor = "Customcolor" - Folder = "Folder" - TypeParameter = "TypeParameter" - User = "User" - Issue = "Issue" - Snippet = "Snippet" + METHOD = "Method" + FUNCTION = "Function" + CONSTRUCTOR = "Constructor" + FIELD = "Field" + VARIABLE = "Variable" + CLASS_ = "Class" + STRUCT = "Struct" + INTERFACE = "Interface" + MODULE = "Module" + PROPERTY = "Property" + EVENT = "Event" + OPERATOR = "Operator" + UNIT = "Unit" + VALUE = "Value" + CONSTANT = "Constant" + ENUM = "Enum" + ENUM_MEMBER = "EnumMember" + KEYWORD = "Keyword" + TEXT = "Text" + COLOR = "Color" + FILE = "File" + REFERENCE = "Reference" + CUSTOMCOLOR = "Customcolor" + FOLDER = "Folder" + TYPE_PARAMETER = "TypeParameter" + USER = "User" + ISSUE = "Issue" + SNIPPET = "Snippet" class AutocompleteCompletionItem(BaseModel): @@ -65,7 +65,9 @@ class AutocompleteCompletionItem(BaseModel): ) detail: Optional[str] = Field( default=None, - description="A human-readable string with additional information about this item, like type or symbol information.", + description=( + "A human-readable string with additional information about this item, like type or symbol information." + ), ) documentation: Optional[str] = Field( default=None, description="A human-readable string that represents a doc-comment." @@ -78,34 +80,37 @@ class AutocompleteCompletionItem(BaseModel): ) label: str = Field( ..., - description="The label of this completion item. By default this is also the text that is inserted when selecting this completion.", + description=( + "The label of this completion item. By default this is also the text that is inserted when selecting this" + " completion." + ), ) class BaseMathType(str, Enum): - total = "total" - dau = "dau" - weekly_active = "weekly_active" - monthly_active = "monthly_active" - unique_session = "unique_session" + TOTAL = "total" + DAU = "dau" + WEEKLY_ACTIVE = "weekly_active" + MONTHLY_ACTIVE = "monthly_active" + UNIQUE_SESSION = "unique_session" class BreakdownAttributionType(str, Enum): - first_touch = "first_touch" - last_touch = "last_touch" - all_events = "all_events" - step = "step" + FIRST_TOUCH = "first_touch" + LAST_TOUCH = "last_touch" + ALL_EVENTS = "all_events" + STEP = "step" class BreakdownType(str, Enum): - cohort = "cohort" - person = "person" - event = "event" - group = "group" - session = "session" - hogql = "hogql" - data_warehouse = "data_warehouse" - data_warehouse_person_property = "data_warehouse_person_property" + COHORT = "cohort" + PERSON = "person" + EVENT = "event" + GROUP = "group" + SESSION = "session" + HOGQL = "hogql" + DATA_WAREHOUSE = "data_warehouse" + DATA_WAREHOUSE_PERSON_PROPERTY = "data_warehouse_person_property" class BreakdownValueInt(RootModel[int]): @@ -160,15 +165,15 @@ class ChartAxis(BaseModel): class ChartDisplayType(str, Enum): - ActionsLineGraph = "ActionsLineGraph" - ActionsBar = "ActionsBar" - ActionsAreaGraph = "ActionsAreaGraph" - ActionsLineGraphCumulative = "ActionsLineGraphCumulative" - BoldNumber = "BoldNumber" - ActionsPie = "ActionsPie" - ActionsBarValue = "ActionsBarValue" - ActionsTable = "ActionsTable" - WorldMap = "WorldMap" + ACTIONS_LINE_GRAPH = "ActionsLineGraph" + ACTIONS_BAR = "ActionsBar" + ACTIONS_AREA_GRAPH = "ActionsAreaGraph" + ACTIONS_LINE_GRAPH_CUMULATIVE = "ActionsLineGraphCumulative" + BOLD_NUMBER = "BoldNumber" + ACTIONS_PIE = "ActionsPie" + ACTIONS_BAR_VALUE = "ActionsBarValue" + ACTIONS_TABLE = "ActionsTable" + WORLD_MAP = "WorldMap" class ClickhouseQueryProgress(BaseModel): @@ -193,13 +198,13 @@ class CohortPropertyFilter(BaseModel): class CountPerActorMathType(str, Enum): - avg_count_per_actor = "avg_count_per_actor" - min_count_per_actor = "min_count_per_actor" - max_count_per_actor = "max_count_per_actor" - median_count_per_actor = "median_count_per_actor" - p90_count_per_actor = "p90_count_per_actor" - p95_count_per_actor = "p95_count_per_actor" - p99_count_per_actor = "p99_count_per_actor" + AVG_COUNT_PER_ACTOR = "avg_count_per_actor" + MIN_COUNT_PER_ACTOR = "min_count_per_actor" + MAX_COUNT_PER_ACTOR = "max_count_per_actor" + MEDIAN_COUNT_PER_ACTOR = "median_count_per_actor" + P90_COUNT_PER_ACTOR = "p90_count_per_actor" + P95_COUNT_PER_ACTOR = "p95_count_per_actor" + P99_COUNT_PER_ACTOR = "p99_count_per_actor" class Response3(BaseModel): @@ -243,25 +248,25 @@ class DatabaseSchemaSource(BaseModel): class Type(str, Enum): - posthog = "posthog" - data_warehouse = "data_warehouse" - view = "view" + POSTHOG = "posthog" + DATA_WAREHOUSE = "data_warehouse" + VIEW = "view" class DatabaseSerializedFieldType(str, Enum): - integer = "integer" - float = "float" - string = "string" - datetime = "datetime" - date = "date" - boolean = "boolean" - array = "array" - json = "json" - lazy_table = "lazy_table" - virtual_table = "virtual_table" - field_traverser = "field_traverser" - expression = "expression" - view = "view" + INTEGER = "integer" + FLOAT = "float" + STRING = "string" + DATETIME = "datetime" + DATE = "date" + BOOLEAN = "boolean" + ARRAY = "array" + JSON = "json" + LAZY_TABLE = "lazy_table" + VIRTUAL_TABLE = "virtual_table" + FIELD_TRAVERSER = "field_traverser" + EXPRESSION = "expression" + VIEW = "view" class DateRange(BaseModel): @@ -272,7 +277,10 @@ class DateRange(BaseModel): date_to: Optional[str] = None explicitDate: Optional[bool] = Field( default=False, - description="Whether the date_from and date_to should be used verbatim. Disables rounding to the start and end of period.", + description=( + "Whether the date_from and date_to should be used verbatim. Disables rounding to the start and end of" + " period." + ), ) @@ -284,11 +292,17 @@ class Day(RootModel[int]): root: int +class DurationType(str, Enum): + DURATION = "duration" + ACTIVE_SECONDS = "active_seconds" + INACTIVE_SECONDS = "inactive_seconds" + + class Key(str, Enum): - tag_name = "tag_name" - text = "text" - href = "href" - selector = "selector" + TAG_NAME = "tag_name" + TEXT = "text" + HREF = "href" + SELECTOR = "selector" class ElementType(BaseModel): @@ -314,10 +328,10 @@ class EmptyPropertyFilter(BaseModel): class EntityType(str, Enum): - actions = "actions" - events = "events" - data_warehouse = "data_warehouse" - new_entity = "new_entity" + ACTIONS = "actions" + EVENTS = "events" + DATA_WAREHOUSE = "data_warehouse" + NEW_ENTITY = "new_entity" class EventDefinition(BaseModel): @@ -330,8 +344,8 @@ class EventDefinition(BaseModel): class CorrelationType(str, Enum): - success = "success" - failure = "failure" + SUCCESS = "success" + FAILURE = "failure" class EventOddsRatioSerialized(BaseModel): @@ -388,17 +402,17 @@ class EventsQueryPersonColumn(BaseModel): class FilterLogicalOperator(str, Enum): - AND = "AND" - OR = "OR" + AND_ = "AND" + OR_ = "OR" class FunnelConversionWindowTimeUnit(str, Enum): - second = "second" - minute = "minute" - hour = "hour" - day = "day" - week = "week" - month = "month" + SECOND = "second" + MINUTE = "minute" + HOUR = "hour" + DAY = "day" + WEEK = "week" + MONTH = "month" class FunnelCorrelationResult(BaseModel): @@ -410,9 +424,9 @@ class FunnelCorrelationResult(BaseModel): class FunnelCorrelationResultsType(str, Enum): - events = "events" - properties = "properties" - event_with_properties = "event_with_properties" + EVENTS = "events" + PROPERTIES = "properties" + EVENT_WITH_PROPERTIES = "event_with_properties" class FunnelExclusionLegacy(BaseModel): @@ -438,19 +452,19 @@ class FunnelExclusionSteps(BaseModel): class FunnelLayout(str, Enum): - horizontal = "horizontal" - vertical = "vertical" + HORIZONTAL = "horizontal" + VERTICAL = "vertical" class FunnelPathType(str, Enum): - funnel_path_before_step = "funnel_path_before_step" - funnel_path_between_steps = "funnel_path_between_steps" - funnel_path_after_step = "funnel_path_after_step" + FUNNEL_PATH_BEFORE_STEP = "funnel_path_before_step" + FUNNEL_PATH_BETWEEN_STEPS = "funnel_path_between_steps" + FUNNEL_PATH_AFTER_STEP = "funnel_path_after_step" class FunnelStepReference(str, Enum): - total = "total" - previous = "previous" + TOTAL = "total" + PREVIOUS = "previous" class FunnelTimeToConvertResults(BaseModel): @@ -462,9 +476,9 @@ class FunnelTimeToConvertResults(BaseModel): class FunnelVizType(str, Enum): - steps = "steps" - time_to_convert = "time_to_convert" - trends = "trends" + STEPS = "steps" + TIME_TO_CONVERT = "time_to_convert" + TRENDS = "trends" class GoalLine(BaseModel): @@ -486,40 +500,40 @@ class HogQLNotice(BaseModel): class BounceRatePageViewMode(str, Enum): - count_pageviews = "count_pageviews" - uniq_urls = "uniq_urls" + COUNT_PAGEVIEWS = "count_pageviews" + UNIQ_URLS = "uniq_urls" class InCohortVia(str, Enum): - auto = "auto" - leftjoin = "leftjoin" - subquery = "subquery" - leftjoin_conjoined = "leftjoin_conjoined" + AUTO = "auto" + LEFTJOIN = "leftjoin" + SUBQUERY = "subquery" + LEFTJOIN_CONJOINED = "leftjoin_conjoined" class MaterializationMode(str, Enum): - auto = "auto" - legacy_null_as_string = "legacy_null_as_string" - legacy_null_as_null = "legacy_null_as_null" - disabled = "disabled" + AUTO = "auto" + LEGACY_NULL_AS_STRING = "legacy_null_as_string" + LEGACY_NULL_AS_NULL = "legacy_null_as_null" + DISABLED = "disabled" class PersonsArgMaxVersion(str, Enum): - auto = "auto" - v1 = "v1" - v2 = "v2" + AUTO = "auto" + V1 = "v1" + V2 = "v2" class PersonsJoinMode(str, Enum): - inner = "inner" - left = "left" + INNER = "inner" + LEFT = "left" class PersonsOnEventsMode(str, Enum): - disabled = "disabled" - person_id_no_override_properties_on_events = "person_id_no_override_properties_on_events" - person_id_override_properties_on_events = "person_id_override_properties_on_events" - person_id_override_properties_joined = "person_id_override_properties_joined" + DISABLED = "disabled" + PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS = "person_id_no_override_properties_on_events" + PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS = "person_id_override_properties_on_events" + PERSON_ID_OVERRIDE_PROPERTIES_JOINED = "person_id_override_properties_joined" class HogQLQueryModifiers(BaseModel): @@ -548,8 +562,8 @@ class HogQueryResponse(BaseModel): class Compare(str, Enum): - current = "current" - previous = "previous" + CURRENT = "current" + PREVIOUS = "previous" class DayItem(BaseModel): @@ -580,26 +594,29 @@ class InsightDateRange(BaseModel): date_to: Optional[str] = None explicitDate: Optional[bool] = Field( default=False, - description="Whether the date_from and date_to should be used verbatim. Disables rounding to the start and end of period.", + description=( + "Whether the date_from and date_to should be used verbatim. Disables rounding to the start and end of" + " period." + ), ) class InsightFilterProperty(str, Enum): - trendsFilter = "trendsFilter" - funnelsFilter = "funnelsFilter" - retentionFilter = "retentionFilter" - pathsFilter = "pathsFilter" - stickinessFilter = "stickinessFilter" - lifecycleFilter = "lifecycleFilter" + TRENDS_FILTER = "trendsFilter" + FUNNELS_FILTER = "funnelsFilter" + RETENTION_FILTER = "retentionFilter" + PATHS_FILTER = "pathsFilter" + STICKINESS_FILTER = "stickinessFilter" + LIFECYCLE_FILTER = "lifecycleFilter" class InsightNodeKind(str, Enum): - TrendsQuery = "TrendsQuery" - FunnelsQuery = "FunnelsQuery" - RetentionQuery = "RetentionQuery" - PathsQuery = "PathsQuery" - StickinessQuery = "StickinessQuery" - LifecycleQuery = "LifecycleQuery" + TRENDS_QUERY = "TrendsQuery" + FUNNELS_QUERY = "FunnelsQuery" + RETENTION_QUERY = "RetentionQuery" + PATHS_QUERY = "PathsQuery" + STICKINESS_QUERY = "StickinessQuery" + LIFECYCLE_QUERY = "LifecycleQuery" class InsightType(str, Enum): @@ -615,55 +632,55 @@ class InsightType(str, Enum): class IntervalType(str, Enum): - minute = "minute" - hour = "hour" - day = "day" - week = "week" - month = "month" + MINUTE = "minute" + HOUR = "hour" + DAY = "day" + WEEK = "week" + MONTH = "month" class LifecycleToggle(str, Enum): - new = "new" - resurrecting = "resurrecting" - returning = "returning" - dormant = "dormant" + NEW = "new" + RESURRECTING = "resurrecting" + RETURNING = "returning" + DORMANT = "dormant" class NodeKind(str, Enum): - EventsNode = "EventsNode" - ActionsNode = "ActionsNode" - DataWarehouseNode = "DataWarehouseNode" - EventsQuery = "EventsQuery" - PersonsNode = "PersonsNode" - HogQuery = "HogQuery" - HogQLQuery = "HogQLQuery" - HogQLMetadata = "HogQLMetadata" - HogQLAutocomplete = "HogQLAutocomplete" - ActorsQuery = "ActorsQuery" - FunnelsActorsQuery = "FunnelsActorsQuery" - FunnelCorrelationActorsQuery = "FunnelCorrelationActorsQuery" - SessionsTimelineQuery = "SessionsTimelineQuery" - DataTableNode = "DataTableNode" - DataVisualizationNode = "DataVisualizationNode" - SavedInsightNode = "SavedInsightNode" - InsightVizNode = "InsightVizNode" - TrendsQuery = "TrendsQuery" - FunnelsQuery = "FunnelsQuery" - RetentionQuery = "RetentionQuery" - PathsQuery = "PathsQuery" - StickinessQuery = "StickinessQuery" - LifecycleQuery = "LifecycleQuery" - InsightActorsQuery = "InsightActorsQuery" - InsightActorsQueryOptions = "InsightActorsQueryOptions" - FunnelCorrelationQuery = "FunnelCorrelationQuery" - WebOverviewQuery = "WebOverviewQuery" - WebTopClicksQuery = "WebTopClicksQuery" - WebStatsTableQuery = "WebStatsTableQuery" - TimeToSeeDataSessionsQuery = "TimeToSeeDataSessionsQuery" - TimeToSeeDataQuery = "TimeToSeeDataQuery" - TimeToSeeDataSessionsJSONNode = "TimeToSeeDataSessionsJSONNode" - TimeToSeeDataSessionsWaterfallNode = "TimeToSeeDataSessionsWaterfallNode" - DatabaseSchemaQuery = "DatabaseSchemaQuery" + EVENTS_NODE = "EventsNode" + ACTIONS_NODE = "ActionsNode" + DATA_WAREHOUSE_NODE = "DataWarehouseNode" + EVENTS_QUERY = "EventsQuery" + PERSONS_NODE = "PersonsNode" + HOG_QUERY = "HogQuery" + HOG_QL_QUERY = "HogQLQuery" + HOG_QL_METADATA = "HogQLMetadata" + HOG_QL_AUTOCOMPLETE = "HogQLAutocomplete" + ACTORS_QUERY = "ActorsQuery" + FUNNELS_ACTORS_QUERY = "FunnelsActorsQuery" + FUNNEL_CORRELATION_ACTORS_QUERY = "FunnelCorrelationActorsQuery" + SESSIONS_TIMELINE_QUERY = "SessionsTimelineQuery" + DATA_TABLE_NODE = "DataTableNode" + DATA_VISUALIZATION_NODE = "DataVisualizationNode" + SAVED_INSIGHT_NODE = "SavedInsightNode" + INSIGHT_VIZ_NODE = "InsightVizNode" + TRENDS_QUERY = "TrendsQuery" + FUNNELS_QUERY = "FunnelsQuery" + RETENTION_QUERY = "RetentionQuery" + PATHS_QUERY = "PathsQuery" + STICKINESS_QUERY = "StickinessQuery" + LIFECYCLE_QUERY = "LifecycleQuery" + INSIGHT_ACTORS_QUERY = "InsightActorsQuery" + INSIGHT_ACTORS_QUERY_OPTIONS = "InsightActorsQueryOptions" + FUNNEL_CORRELATION_QUERY = "FunnelCorrelationQuery" + WEB_OVERVIEW_QUERY = "WebOverviewQuery" + WEB_TOP_CLICKS_QUERY = "WebTopClicksQuery" + WEB_STATS_TABLE_QUERY = "WebStatsTableQuery" + TIME_TO_SEE_DATA_SESSIONS_QUERY = "TimeToSeeDataSessionsQuery" + TIME_TO_SEE_DATA_QUERY = "TimeToSeeDataQuery" + TIME_TO_SEE_DATA_SESSIONS_JSON_NODE = "TimeToSeeDataSessionsJSONNode" + TIME_TO_SEE_DATA_SESSIONS_WATERFALL_NODE = "TimeToSeeDataSessionsWaterfallNode" + DATABASE_SCHEMA_QUERY = "DatabaseSchemaQuery" class PathCleaningFilter(BaseModel): @@ -675,10 +692,10 @@ class PathCleaningFilter(BaseModel): class PathType(str, Enum): - field_pageview = "$pageview" - field_screen = "$screen" - custom_event = "custom_event" - hogql = "hogql" + FIELD_PAGEVIEW = "$pageview" + FIELD_SCREEN = "$screen" + CUSTOM_EVENT = "custom_event" + HOGQL = "hogql" class PathsFilter(BaseModel): @@ -724,51 +741,51 @@ class PathsFilterLegacy(BaseModel): class PropertyFilterType(str, Enum): - meta = "meta" - event = "event" - person = "person" - element = "element" - feature = "feature" - session = "session" - cohort = "cohort" - recording = "recording" - group = "group" - hogql = "hogql" - data_warehouse = "data_warehouse" - data_warehouse_person_property = "data_warehouse_person_property" + META = "meta" + EVENT = "event" + PERSON = "person" + ELEMENT = "element" + FEATURE = "feature" + SESSION = "session" + COHORT = "cohort" + RECORDING = "recording" + GROUP = "group" + HOGQL = "hogql" + DATA_WAREHOUSE = "data_warehouse" + DATA_WAREHOUSE_PERSON_PROPERTY = "data_warehouse_person_property" class PropertyMathType(str, Enum): - avg = "avg" - sum = "sum" - min = "min" - max = "max" - median = "median" - p90 = "p90" - p95 = "p95" - p99 = "p99" + AVG = "avg" + SUM = "sum" + MIN = "min" + MAX = "max" + MEDIAN = "median" + P90 = "p90" + P95 = "p95" + P99 = "p99" class PropertyOperator(str, Enum): - exact = "exact" - is_not = "is_not" - icontains = "icontains" - not_icontains = "not_icontains" - regex = "regex" - not_regex = "not_regex" - gt = "gt" - gte = "gte" - lt = "lt" - lte = "lte" - is_set = "is_set" - is_not_set = "is_not_set" - is_date_exact = "is_date_exact" - is_date_before = "is_date_before" - is_date_after = "is_date_after" - between = "between" - not_between = "not_between" - min = "min" - max = "max" + EXACT = "exact" + IS_NOT = "is_not" + ICONTAINS = "icontains" + NOT_ICONTAINS = "not_icontains" + REGEX = "regex" + NOT_REGEX = "not_regex" + GT = "gt" + GTE = "gte" + LT = "lt" + LTE = "lte" + IS_SET = "is_set" + IS_NOT_SET = "is_not_set" + IS_DATE_EXACT = "is_date_exact" + IS_DATE_BEFORE = "is_date_before" + IS_DATE_AFTER = "is_date_after" + BETWEEN = "between" + NOT_BETWEEN = "not_between" + MIN = "min" + MAX = "max" class QueryResponseAlternative1(BaseModel): @@ -852,20 +869,20 @@ class QueryTiming(BaseModel): t: float = Field(..., description="Time in seconds. Shortened to 't' to save on data.") -class RecordingDurationFilter(BaseModel): +class RecordingPropertyFilter(BaseModel): model_config = ConfigDict( extra="forbid", ) - key: Literal["duration"] = "duration" + key: Union[DurationType, str] label: Optional[str] = None operator: PropertyOperator type: Literal["recording"] = "recording" - value: float + value: Optional[Union[str, float, list[Union[str, float]]]] = None class Kind1(str, Enum): - ActionsNode = "ActionsNode" - EventsNode = "EventsNode" + ACTIONS_NODE = "ActionsNode" + EVENTS_NODE = "EventsNode" class RetentionEntity(BaseModel): @@ -882,20 +899,20 @@ class RetentionEntity(BaseModel): class RetentionReference(str, Enum): - total = "total" - previous = "previous" + TOTAL = "total" + PREVIOUS = "previous" class RetentionPeriod(str, Enum): - Hour = "Hour" - Day = "Day" - Week = "Week" - Month = "Month" + HOUR = "Hour" + DAY = "Day" + WEEK = "Week" + MONTH = "Month" class RetentionType(str, Enum): - retention_recurring = "retention_recurring" - retention_first_time = "retention_first_time" + RETENTION_RECURRING = "retention_recurring" + RETENTION_FIRST_TIME = "retention_first_time" class RetentionValue(BaseModel): @@ -925,9 +942,9 @@ class SessionPropertyFilter(BaseModel): class StepOrderValue(str, Enum): - strict = "strict" - unordered = "unordered" - ordered = "ordered" + STRICT = "strict" + UNORDERED = "unordered" + ORDERED = "ordered" class StickinessFilter(BaseModel): @@ -1059,13 +1076,13 @@ class TrendsFilter(BaseModel): model_config = ConfigDict( extra="forbid", ) - aggregationAxisFormat: Optional[AggregationAxisFormat] = AggregationAxisFormat.numeric + aggregationAxisFormat: Optional[AggregationAxisFormat] = AggregationAxisFormat.NUMERIC aggregationAxisPostfix: Optional[str] = None aggregationAxisPrefix: Optional[str] = None breakdown_histogram_bin_count: Optional[float] = None compare: Optional[bool] = False decimalPlaces: Optional[float] = None - display: Optional[ChartDisplayType] = ChartDisplayType.ActionsLineGraph + display: Optional[ChartDisplayType] = ChartDisplayType.ACTIONS_LINE_GRAPH formula: Optional[str] = None hidden_legend_indexes: Optional[list[float]] = None showLabelsOnSeries: Optional[bool] = None @@ -1139,9 +1156,9 @@ class VizSpecificOptions(BaseModel): class Kind2(str, Enum): - unit = "unit" - duration_s = "duration_s" - percentage = "percentage" + UNIT = "unit" + DURATION_S = "duration_s" + PERCENTAGE = "percentage" class WebOverviewItem(BaseModel): @@ -1186,22 +1203,22 @@ class WebOverviewQueryResponse(BaseModel): class WebStatsBreakdown(str, Enum): - Page = "Page" - InitialPage = "InitialPage" - ExitPage = "ExitPage" - InitialChannelType = "InitialChannelType" - InitialReferringDomain = "InitialReferringDomain" - InitialUTMSource = "InitialUTMSource" - InitialUTMCampaign = "InitialUTMCampaign" - InitialUTMMedium = "InitialUTMMedium" - InitialUTMTerm = "InitialUTMTerm" - InitialUTMContent = "InitialUTMContent" - Browser = "Browser" + PAGE = "Page" + INITIAL_PAGE = "InitialPage" + EXIT_PAGE = "ExitPage" + INITIAL_CHANNEL_TYPE = "InitialChannelType" + INITIAL_REFERRING_DOMAIN = "InitialReferringDomain" + INITIAL_UTM_SOURCE = "InitialUTMSource" + INITIAL_UTM_CAMPAIGN = "InitialUTMCampaign" + INITIAL_UTM_MEDIUM = "InitialUTMMedium" + INITIAL_UTM_TERM = "InitialUTMTerm" + INITIAL_UTM_CONTENT = "InitialUTMContent" + BROWSER = "Browser" OS = "OS" - DeviceType = "DeviceType" - Country = "Country" - Region = "Region" - City = "City" + DEVICE_TYPE = "DeviceType" + COUNTRY = "Country" + REGION = "Region" + CITY = "City" class WebStatsTableQueryResponse(BaseModel): @@ -1292,7 +1309,7 @@ class BreakdownFilter(BaseModel): breakdown_histogram_bin_count: Optional[int] = None breakdown_limit: Optional[int] = None breakdown_normalize_url: Optional[bool] = None - breakdown_type: Optional[BreakdownType] = BreakdownType.event + breakdown_type: Optional[BreakdownType] = BreakdownType.EVENT breakdowns: Optional[list[Breakdown]] = None @@ -1859,7 +1876,7 @@ class EventPropertyFilter(BaseModel): ) key: str label: Optional[str] = None - operator: Optional[PropertyOperator] = PropertyOperator.exact + operator: Optional[PropertyOperator] = PropertyOperator.EXACT type: Literal["event"] = Field(default="event", description="Event properties") value: Optional[Union[str, float, list[Union[str, float]]]] = None @@ -2512,7 +2529,7 @@ class RetentionFilter(BaseModel): model_config = ConfigDict( extra="forbid", ) - period: Optional[RetentionPeriod] = RetentionPeriod.Day + period: Optional[RetentionPeriod] = RetentionPeriod.DAY retentionReference: Optional[RetentionReference] = None retentionType: Optional[RetentionType] = None returningEntity: Optional[RetentionEntity] = None @@ -2790,7 +2807,7 @@ class DashboardFilter(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -2843,7 +2860,7 @@ class DataWarehouseNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -2874,7 +2891,7 @@ class DataWarehouseNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -2916,7 +2933,7 @@ class EntityNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -2945,7 +2962,7 @@ class EntityNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -2972,7 +2989,7 @@ class EventsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3003,7 +3020,7 @@ class EventsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3033,7 +3050,7 @@ class EventsQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3062,7 +3079,7 @@ class EventsQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3090,7 +3107,7 @@ class FunnelExclusionActionsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3122,7 +3139,7 @@ class FunnelExclusionActionsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3149,7 +3166,7 @@ class FunnelExclusionEventsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3182,7 +3199,7 @@ class FunnelExclusionEventsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3209,7 +3226,7 @@ class HogQLFilters(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3252,7 +3269,7 @@ class PersonsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3279,7 +3296,7 @@ class PersonsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3307,7 +3324,7 @@ class PropertyGroupFilterValue(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3386,7 +3403,7 @@ class ActionsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3416,7 +3433,7 @@ class ActionsNode(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3455,19 +3472,19 @@ class FunnelsFilter(BaseModel): extra="forbid", ) binCount: Optional[int] = None - breakdownAttributionType: Optional[BreakdownAttributionType] = BreakdownAttributionType.first_touch + breakdownAttributionType: Optional[BreakdownAttributionType] = BreakdownAttributionType.FIRST_TOUCH breakdownAttributionValue: Optional[int] = None exclusions: Optional[list[Union[FunnelExclusionEventsNode, FunnelExclusionActionsNode]]] = [] funnelAggregateByHogQL: Optional[str] = None funnelFromStep: Optional[int] = None - funnelOrderType: Optional[StepOrderValue] = StepOrderValue.ordered - funnelStepReference: Optional[FunnelStepReference] = FunnelStepReference.total + funnelOrderType: Optional[StepOrderValue] = StepOrderValue.ORDERED + funnelStepReference: Optional[FunnelStepReference] = FunnelStepReference.TOTAL funnelToStep: Optional[int] = None - funnelVizType: Optional[FunnelVizType] = FunnelVizType.steps + funnelVizType: Optional[FunnelVizType] = FunnelVizType.STEPS funnelWindowInterval: Optional[int] = 14 - funnelWindowIntervalUnit: Optional[FunnelConversionWindowTimeUnit] = FunnelConversionWindowTimeUnit.day + funnelWindowIntervalUnit: Optional[FunnelConversionWindowTimeUnit] = FunnelConversionWindowTimeUnit.DAY hidden_legend_breakdowns: Optional[list[str]] = None - layout: Optional[FunnelLayout] = FunnelLayout.vertical + layout: Optional[FunnelLayout] = FunnelLayout.VERTICAL class HasPropertiesNode(RootModel[Union[EventsNode, EventsQuery, PersonsNode]]): @@ -3525,7 +3542,7 @@ class RetentionQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3551,7 +3568,7 @@ class StickinessQuery(BaseModel): default=False, description="Exclude internal and test users by applying the respective filters" ) interval: Optional[IntervalType] = Field( - default=IntervalType.day, + default=IntervalType.DAY, description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`", ) kind: Literal["StickinessQuery"] = "StickinessQuery" @@ -3567,7 +3584,7 @@ class StickinessQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3600,7 +3617,7 @@ class TrendsQuery(BaseModel): default=False, description="Exclude internal and test users by applying the respective filters" ) interval: Optional[IntervalType] = Field( - default=IntervalType.day, + default=IntervalType.DAY, description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`", ) kind: Literal["TrendsQuery"] = "TrendsQuery" @@ -3616,7 +3633,7 @@ class TrendsQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3658,7 +3675,10 @@ class FilterType(BaseModel): events: Optional[list[dict[str, Any]]] = None explicit_date: Optional[Union[bool, str]] = Field( default=None, - description='Whether the `date_from` and `date_to` should be used verbatim. Disables rounding to the start and end of period. Strings are cast to bools, e.g. "true" -> true.', + description=( + "Whether the `date_from` and `date_to` should be used verbatim. Disables rounding to the start and end of" + ' period. Strings are cast to bools, e.g. "true" -> true.' + ), ) filter_test_accounts: Optional[bool] = None from_dashboard: Optional[Union[bool, float]] = None @@ -3674,7 +3694,7 @@ class FilterType(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3718,7 +3738,7 @@ class FunnelsQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3759,7 +3779,7 @@ class InsightsQueryBaseFunnelsQueryResponse(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3797,7 +3817,7 @@ class InsightsQueryBaseLifecycleQueryResponse(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3835,7 +3855,7 @@ class InsightsQueryBasePathsQueryResponse(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3873,7 +3893,7 @@ class InsightsQueryBaseRetentionQueryResponse(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3911,7 +3931,7 @@ class InsightsQueryBaseTrendsQueryResponse(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -3937,7 +3957,7 @@ class LifecycleQuery(BaseModel): default=False, description="Exclude internal and test users by applying the respective filters" ) interval: Optional[IntervalType] = Field( - default=IntervalType.day, + default=IntervalType.DAY, description="Granularity of the response. Can be one of `hour`, `day`, `week` or `month`", ) kind: Literal["LifecycleQuery"] = "LifecycleQuery" @@ -3956,7 +3976,7 @@ class LifecycleQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -4075,15 +4095,23 @@ class FunnelsActorsQuery(BaseModel): ) funnelCustomSteps: Optional[list[int]] = Field( default=None, - description="Custom step numbers to get persons for. This overrides `funnelStep`. Primarily for correlation use.", + description=( + "Custom step numbers to get persons for. This overrides `funnelStep`. Primarily for correlation use." + ), ) funnelStep: Optional[int] = Field( default=None, - description="Index of the step for which we want to get the timestamp for, per person. Positive for converted persons, negative for dropped of persons.", + description=( + "Index of the step for which we want to get the timestamp for, per person. Positive for converted persons," + " negative for dropped of persons." + ), ) funnelStepBreakdown: Optional[Union[str, float, list[Union[str, float]]]] = Field( default=None, - description="The breakdown value for which to get persons for. This is an array for person and event properties, a string for groups and an integer for cohorts.", + description=( + "The breakdown value for which to get persons for. This is an array for person and event properties, a" + " string for groups and an integer for cohorts." + ), ) funnelTrendsDropOff: Optional[bool] = None funnelTrendsEntrancePeriodStart: Optional[str] = Field( @@ -4125,7 +4153,7 @@ class PathsQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -4205,7 +4233,7 @@ class FunnelCorrelationActorsQuery(BaseModel): ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, @@ -4264,7 +4292,10 @@ class ActorsQuery(BaseModel): list[Union[PersonPropertyFilter, CohortPropertyFilter, HogQLPropertyFilter, EmptyPropertyFilter]] ] = Field( default=None, - description="Currently only person filters supported. No filters for querying groups. See `filter_conditions()` in actor_strategies.py.", + description=( + "Currently only person filters supported. No filters for querying groups. See `filter_conditions()` in" + " actor_strategies.py." + ), ) kind: Literal["ActorsQuery"] = "ActorsQuery" limit: Optional[int] = None @@ -4277,7 +4308,10 @@ class ActorsQuery(BaseModel): list[Union[PersonPropertyFilter, CohortPropertyFilter, HogQLPropertyFilter, EmptyPropertyFilter]] ] = Field( default=None, - description="Currently only person filters supported. No filters for querying groups. See `filter_conditions()` in actor_strategies.py.", + description=( + "Currently only person filters supported. No filters for querying groups. See `filter_conditions()` in" + " actor_strategies.py." + ), ) response: Optional[ActorsQueryResponse] = None search: Optional[str] = None @@ -4394,7 +4428,10 @@ class QueryRequest(BaseModel): async_: Optional[bool] = Field( default=None, alias="async", - description="(Experimental) Whether to run the query asynchronously. Defaults to False. If True, the `id` of the query can be used to check the status and to cancel it.", + description=( + "(Experimental) Whether to run the query asynchronously. Defaults to False. If True, the `id` of the query" + " can be used to check the status and to cancel it." + ), examples=[True], ) client_query_id: Optional[str] = Field( @@ -4432,7 +4469,12 @@ class QueryRequest(BaseModel): DatabaseSchemaQuery, ] = Field( ..., - description='Submit a JSON string representing a query for PostHog data analysis, for example a HogQL query.\n\nExample payload:\n\n```\n\n{"query": {"kind": "HogQLQuery", "query": "select * from events limit 100"}}\n\n```\n\nFor more details on HogQL queries, see the [PostHog HogQL documentation](/docs/hogql#api-access).', + description=( + "Submit a JSON string representing a query for PostHog data analysis, for example a HogQL query.\n\nExample" + ' payload:\n\n```\n\n{"query": {"kind": "HogQLQuery", "query": "select * from events limit' + ' 100"}}\n\n```\n\nFor more details on HogQL queries, see the [PostHog HogQL' + " documentation](/docs/hogql#api-access)." + ), discriminator="kind", ) refresh: Optional[Union[bool, str]] = None diff --git a/posthog/schema_helpers.py b/posthog/schema_helpers.py index c50075403ea1a..d3e6cd427b68d 100644 --- a/posthog/schema_helpers.py +++ b/posthog/schema_helpers.py @@ -76,7 +76,7 @@ def serialize_query(self, next_serializer): # use a canonical value for each display category if "display" in dumped[insightFilterKey]: canonical_display = grouped_chart_display_types(dumped[insightFilterKey]["display"]) - if canonical_display == ChartDisplayType.ActionsLineGraph: + if canonical_display == ChartDisplayType.ACTIONS_LINE_GRAPH: del dumped[insightFilterKey]["display"] # default value, remove else: dumped[insightFilterKey]["display"] = canonical_display @@ -125,15 +125,15 @@ def filter_key_for_query(node: InsightQueryNode) -> str: def grouped_chart_display_types(display: ChartDisplayType) -> ChartDisplayType | None: if display in [ - ChartDisplayType.ActionsLineGraph, - ChartDisplayType.ActionsBar, - ChartDisplayType.ActionsAreaGraph, + ChartDisplayType.ACTIONS_LINE_GRAPH, + ChartDisplayType.ACTIONS_BAR, + ChartDisplayType.ACTIONS_AREA_GRAPH, ]: # time series - return ChartDisplayType.ActionsLineGraph - elif display in [ChartDisplayType.ActionsLineGraphCumulative]: + return ChartDisplayType.ACTIONS_LINE_GRAPH + elif display in [ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE]: # cumulative time series - return ChartDisplayType.ActionsLineGraphCumulative + return ChartDisplayType.ACTIONS_LINE_GRAPH_CUMULATIVE else: # total value - return ChartDisplayType.ActionsBarValue + return ChartDisplayType.ACTIONS_BAR_VALUE diff --git a/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py b/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py index 50de99273228e..78b204d872314 100644 --- a/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py +++ b/posthog/session_recordings/queries/session_recording_list_from_replay_summary.py @@ -194,7 +194,7 @@ def _data_to_return(self, results: list[Any]) -> list[dict[str, Any]]: def get_query(self) -> tuple[str, dict[str, Any]]: # we don't support PoE V1 - hopefully that's ok - if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: return "", {} prop_query, prop_params = self._get_prop_groups( @@ -299,7 +299,7 @@ def _determine_should_join_events(self): ) has_poe_filters = ( - self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events + self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS and len( [ pg @@ -311,7 +311,7 @@ def _determine_should_join_events(self): ) has_poe_person_filter = ( - self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events + self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS and self._filter.person_uuid ) @@ -367,9 +367,11 @@ def format_event_filter(self, entity: Entity, prepend: str, team_id: int) -> tup prepend=prepend, allow_denormalized_props=True, has_person_id_joined=True, - person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 - if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events - else PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN, + person_properties_mode=( + PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + else PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN + ), hogql_context=self._filter.hogql_context, ) filter_sql += f" {filters}" @@ -416,7 +418,7 @@ def build_event_filters(self) -> SummaryEventFiltersSQL: -- select the unique events in this session to support filtering sessions by presence of an event groupUniqArray(event) as event_names,""" - if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events: + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: person_id_clause, person_id_params = self._get_person_id_clause condition_sql += person_id_clause params = {**params, **person_id_params} @@ -493,7 +495,7 @@ def get_query(self, select_event_ids: bool = False) -> tuple[str, dict[str, Any] g for g in self._filter.property_groups.flat if ( - self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events + self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS and g.type == "person" ) or ( @@ -508,9 +510,11 @@ def get_query(self, select_event_ids: bool = False) -> tuple[str, dict[str, Any] # it is likely this can be returned to the default of True in future # but would need careful monitoring allow_denormalized_props=settings.ALLOW_DENORMALIZED_PROPS_IN_LISTING, - person_properties_mode=PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 - if self._person_on_events_mode == PersonsOnEventsMode.person_id_override_properties_on_events - else PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN, + person_properties_mode=( + PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 + if self._person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + else PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN + ), ) ( diff --git a/posthog/tasks/tasks.py b/posthog/tasks/tasks.py index 3e64c3fe7fc6f..d337c5827630d 100644 --- a/posthog/tasks/tasks.py +++ b/posthog/tasks/tasks.py @@ -10,6 +10,7 @@ from redis import Redis from structlog import get_logger +from posthog.clickhouse.client.limit import limit_concurrency, CeleryConcurrencyLimitExceeded from posthog.cloud_utils import is_cloud from posthog.errors import CHQueryErrorTooManySimultaneousQueries from posthog.hogql.constants import LimitContext @@ -40,11 +41,17 @@ def redis_heartbeat() -> None: autoretry_for=( # Important: Only retry for things that might be okay on the next try CHQueryErrorTooManySimultaneousQueries, + CeleryConcurrencyLimitExceeded, ), retry_backoff=1, - retry_backoff_max=2, + retry_backoff_max=10, max_retries=3, + expires=60 * 10, # Do not run queries that got stuck for more than this ) +@limit_concurrency(90) # Do not go above what CH can handle (max_concurrent_queries) +@limit_concurrency( + 10, key=lambda *args, **kwargs: kwargs.get("team_id") or args[0] +) # Do not run too many queries at once for the same team def process_query_task( team_id: int, user_id: Optional[int], @@ -173,7 +180,6 @@ def pg_row_count() -> None: "log_entries", ] - HEARTBEAT_EVENT_TO_INGESTION_LAG_METRIC = { "heartbeat": "ingestion", "heartbeat_buffer": "ingestion_buffer", diff --git a/posthog/temporal/data_imports/pipelines/stripe/__init__.py b/posthog/temporal/data_imports/pipelines/stripe/__init__.py index e69de29bb2d1d..228e94778e689 100644 --- a/posthog/temporal/data_imports/pipelines/stripe/__init__.py +++ b/posthog/temporal/data_imports/pipelines/stripe/__init__.py @@ -0,0 +1,225 @@ +import dlt +from dlt.sources.helpers.rest_client.paginators import BasePaginator +from dlt.sources.helpers.requests import Response, Request +from posthog.temporal.data_imports.pipelines.rest_source import RESTAPIConfig, rest_api_resources +from posthog.temporal.data_imports.pipelines.rest_source.typing import EndpointResource + + +def get_resource(name: str, is_incremental: bool) -> EndpointResource: + resources: dict[str, EndpointResource] = { + "BalanceTransaction": { + "name": "BalanceTransaction", + "table_name": "balance_transaction", + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "data_selector": "data", + "path": "/v1/balance_transactions", + "params": { + # the parameters below can optionally be configured + # "created": "OPTIONAL_CONFIG", + # "currency": "OPTIONAL_CONFIG", + # "ending_before": "OPTIONAL_CONFIG", + # "expand": "OPTIONAL_CONFIG", + "limit": 100, + # "payout": "OPTIONAL_CONFIG", + # "source": "OPTIONAL_CONFIG", + # "starting_after": "OPTIONAL_CONFIG", + # "type": "OPTIONAL_CONFIG", + }, + }, + }, + "Charge": { + "name": "Charge", + "table_name": "charge", + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "data_selector": "data", + "path": "/v1/charges", + "params": { + # the parameters below can optionally be configured + # "created": "OPTIONAL_CONFIG", + # "customer": "OPTIONAL_CONFIG", + # "ending_before": "OPTIONAL_CONFIG", + # "expand": "OPTIONAL_CONFIG", + "limit": 100, + # "payment_intent": "OPTIONAL_CONFIG", + # "starting_after": "OPTIONAL_CONFIG", + # "transfer_group": "OPTIONAL_CONFIG", + }, + }, + }, + "Customer": { + "name": "Customer", + "table_name": "customer", + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "data_selector": "data", + "path": "/v1/customers", + "params": { + # the parameters below can optionally be configured + # "created": "OPTIONAL_CONFIG", + # "email": "OPTIONAL_CONFIG", + # "ending_before": "OPTIONAL_CONFIG", + # "expand": "OPTIONAL_CONFIG", + "limit": 100, + # "starting_after": "OPTIONAL_CONFIG", + # "test_clock": "OPTIONAL_CONFIG", + }, + }, + }, + "Invoice": { + "name": "Invoice", + "table_name": "invoice", + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "data_selector": "data", + "path": "/v1/invoices", + "params": { + # the parameters below can optionally be configured + # "collection_method": "OPTIONAL_CONFIG", + "created[gte]": { + "type": "incremental", + "cursor_path": "created", + "initial_value": 0, # type: ignore + } + if is_incremental + else None, + # "customer": "OPTIONAL_CONFIG", + # "due_date": "OPTIONAL_CONFIG", + # "ending_before": "OPTIONAL_CONFIG", + # "expand": "OPTIONAL_CONFIG", + "limit": 100, + # "starting_after": "OPTIONAL_CONFIG", + # "status": "OPTIONAL_CONFIG", + # "subscription": "OPTIONAL_CONFIG", + }, + }, + }, + "Price": { + "name": "Price", + "table_name": "price", + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "data_selector": "data", + "path": "/v1/prices", + "params": { + # the parameters below can optionally be configured + # "active": "OPTIONAL_CONFIG", + # "created": "OPTIONAL_CONFIG", + # "currency": "OPTIONAL_CONFIG", + # "ending_before": "OPTIONAL_CONFIG", + # "expand": "OPTIONAL_CONFIG", + "limit": 100, + # "lookup_keys": "OPTIONAL_CONFIG", + # "product": "OPTIONAL_CONFIG", + # "recurring": "OPTIONAL_CONFIG", + # "starting_after": "OPTIONAL_CONFIG", + # "type": "OPTIONAL_CONFIG", + }, + }, + }, + "Product": { + "name": "Product", + "table_name": "product", + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "data_selector": "data", + "path": "/v1/products", + "params": { + # the parameters below can optionally be configured + # "active": "OPTIONAL_CONFIG", + # "created": "OPTIONAL_CONFIG", + # "ending_before": "OPTIONAL_CONFIG", + # "expand": "OPTIONAL_CONFIG", + # "ids": "OPTIONAL_CONFIG", + "limit": 100, + # "shippable": "OPTIONAL_CONFIG", + # "starting_after": "OPTIONAL_CONFIG", + # "url": "OPTIONAL_CONFIG", + }, + }, + }, + "Subscription": { + "name": "Subscription", + "table_name": "subscription", + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "data_selector": "data", + "path": "/v1/subscriptions", + "params": { + # the parameters below can optionally be configured + # "collection_method": "OPTIONAL_CONFIG", + # "created": "OPTIONAL_CONFIG", + # "current_period_end": "OPTIONAL_CONFIG", + # "current_period_start": "OPTIONAL_CONFIG", + # "customer": "OPTIONAL_CONFIG", + # "ending_before": "OPTIONAL_CONFIG", + # "expand": "OPTIONAL_CONFIG", + "limit": 100, + # "price": "OPTIONAL_CONFIG", + # "starting_after": "OPTIONAL_CONFIG", + # "status": "OPTIONAL_CONFIG", + # "test_clock": "OPTIONAL_CONFIG", + }, + }, + }, + } + + return resources[name] + + +class StripePaginator(BasePaginator): + def update_state(self, response: Response) -> None: + res = response.json() + + self._starting_after = None + + if not res: + self._has_next_page = False + return + + if res["has_more"]: + self._has_next_page = True + + earliest_value_in_response = res["data"][-1]["id"] + self._starting_after = earliest_value_in_response + else: + self._has_next_page = False + + def update_request(self, request: Request) -> None: + if request.params is None: + request.params = {} + + request.params["starting_after"] = self._starting_after + + +@dlt.source(max_table_nesting=0) +def stripe_source(api_key: str, account_id: str, endpoint: str, is_incremental: bool = False): + config: RESTAPIConfig = { + "client": { + "base_url": "https://api.stripe.com/", + "auth": { + "type": "http_basic", + "username": api_key, + "password": "", + }, + "headers": { + "Stripe-Account": account_id, + }, + "paginator": StripePaginator(), + }, + "resource_defaults": { + "primary_key": "id", + "write_disposition": "merge", + }, + "resources": [get_resource(endpoint, is_incremental)], + } + + yield from rest_api_resources(config) diff --git a/posthog/temporal/data_imports/pipelines/stripe/helpers.py b/posthog/temporal/data_imports/pipelines/stripe/helpers.py deleted file mode 100644 index 2dfad33b4a5a0..0000000000000 --- a/posthog/temporal/data_imports/pipelines/stripe/helpers.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Stripe analytics source helpers""" - -from typing import Any, Optional, Union -from collections.abc import Iterable - -import stripe -import dlt -from dlt.common import pendulum -from dlt.sources import DltResource -from pendulum import DateTime -from asgiref.sync import sync_to_async -from posthog.temporal.common.logger import bind_temporal_worker_logger -from posthog.temporal.data_imports.pipelines.helpers import check_limit -from posthog.temporal.data_imports.pipelines.stripe.settings import INCREMENTAL_ENDPOINTS -from posthog.warehouse.models import ExternalDataJob - -from posthog.warehouse.models.external_table_definitions import get_dlt_mapping_for_external_table - -stripe.api_version = "2022-11-15" - - -def transform_date(date: Union[str, DateTime, int]) -> int: - if isinstance(date, str): - date = pendulum.from_format(date, "%Y-%m-%dT%H:%M:%SZ") - if isinstance(date, DateTime): - # convert to unix timestamp - date = int(date.timestamp()) - return date - - -async def stripe_get_data( - api_key: str, - account_id: str, - resource: str, - start_date: Optional[Any] = None, - end_date: Optional[Any] = None, - **kwargs: Any, -) -> dict[Any, Any]: - if start_date: - start_date = transform_date(start_date) - if end_date: - end_date = transform_date(end_date) - - if resource == "Subscription": - kwargs.update({"status": "all"}) - - _resource = getattr(stripe, resource) - - resource_dict = await sync_to_async(_resource.list)( - api_key=api_key, - stripe_account=account_id, - created={"gte": start_date, "lt": end_date}, - limit=100, - **kwargs, - ) - response = dict(resource_dict) - - return response - - -async def stripe_pagination( - api_key: str, - account_id: str, - endpoint: str, - team_id: int, - job_id: str, - schema_id: str, - starting_after: Optional[Any] = None, - start_date: Optional[Any] = None, - end_date: Optional[Any] = None, -): - """ - Retrieves data from an endpoint with pagination. - - Args: - endpoint (str): The endpoint to retrieve data from. - start_date (Optional[Any]): An optional start date to limit the data retrieved. Defaults to None. - end_date (Optional[Any]): An optional end date to limit the data retrieved. Defaults to None. - - Returns: - Iterable[TDataItem]: Data items retrieved from the endpoint. - """ - - logger = await bind_temporal_worker_logger(team_id) - logger.info(f"Stripe: getting {endpoint}") - - if endpoint in INCREMENTAL_ENDPOINTS: - _cursor_state = dlt.current.resource_state(f"team_{team_id}_{schema_id}_{endpoint}").setdefault( - "cursors", {"ending_before": None, "starting_after": None} - ) - _starting_after = _cursor_state.get("starting_after", None) - _ending_before = _cursor_state.get("ending_before", None) if _starting_after is None else None - else: - _starting_after = starting_after - _ending_before = None - - while True: - if _ending_before is not None: - logger.info(f"Stripe: getting {endpoint} before {_ending_before}") - elif _starting_after is not None: - logger.info(f"Stripe: getting {endpoint} after {_starting_after}") - - count = 0 - - response = await stripe_get_data( - api_key, - account_id, - endpoint, - ending_before=_ending_before, - starting_after=_starting_after, - start_date=start_date, - end_date=end_date, - ) - - if len(response["data"]) > 0: - latest_value_in_response = response["data"][0]["id"] - earliest_value_in_response = response["data"][-1]["id"] - - if endpoint in INCREMENTAL_ENDPOINTS: - # First pass, store the latest value - if _starting_after is None and _ending_before is None: - _cursor_state["ending_before"] = latest_value_in_response - - # currently scrolling from past to present - if _ending_before is not None: - _cursor_state["ending_before"] = latest_value_in_response - _ending_before = latest_value_in_response - # otherwise scrolling from present to past - else: - _starting_after = earliest_value_in_response - _cursor_state["starting_after"] = earliest_value_in_response - else: - _starting_after = earliest_value_in_response - else: - if endpoint in INCREMENTAL_ENDPOINTS: - _cursor_state["starting_after"] = None - - yield response["data"] - - count, status = await check_limit( - team_id=team_id, - job_id=job_id, - new_count=count + len(response["data"]), - ) - - if not response["has_more"] or status == ExternalDataJob.Status.CANCELLED: - break - - -@dlt.source(max_table_nesting=0) -def stripe_source( - api_key: str, - account_id: str, - endpoints: tuple[str, ...], - team_id, - job_id, - schema_id, - starting_after: Optional[str] = None, - start_date: Optional[Any] = None, - end_date: Optional[Any] = None, -) -> Iterable[DltResource]: - for endpoint in endpoints: - yield dlt.resource( - stripe_pagination, - name=endpoint, - write_disposition="append", - columns=get_dlt_mapping_for_external_table(f"stripe_{endpoint}".lower()), - )( - api_key=api_key, - account_id=account_id, - endpoint=endpoint, - team_id=team_id, - job_id=job_id, - schema_id=schema_id, - starting_after=starting_after, - start_date=start_date, - end_date=end_date, - ) diff --git a/posthog/temporal/data_imports/pipelines/test/test_pipeline.py b/posthog/temporal/data_imports/pipelines/test/test_pipeline.py index 071b818b01d1c..23fc37c6d80f8 100644 --- a/posthog/temporal/data_imports/pipelines/test/test_pipeline.py +++ b/posthog/temporal/data_imports/pipelines/test/test_pipeline.py @@ -6,7 +6,7 @@ import structlog from asgiref.sync import sync_to_async from posthog.temporal.data_imports.pipelines.pipeline import DataImportPipeline, PipelineInputs -from posthog.temporal.data_imports.pipelines.stripe.helpers import stripe_source +from posthog.temporal.data_imports.pipelines.stripe import stripe_source from posthog.test.base import APIBaseTest from posthog.warehouse.models.external_data_job import ExternalDataJob from posthog.warehouse.models.external_data_schema import ExternalDataSchema @@ -43,22 +43,13 @@ async def _create_pipeline(self, schema_name: str, incremental: bool): pipeline = DataImportPipeline( inputs=PipelineInputs( source_id=source.pk, - run_id=job.pk, + run_id=str(job.pk), schema_id=schema.pk, dataset_name=job.folder_path, job_type="Stripe", team_id=self.team.pk, ), - source=stripe_source( - api_key="", - account_id="", - endpoints=(schema_name,), - team_id=self.team.pk, - job_id=job.pk, - schema_id=schema.pk, - start_date=None, - end_date=None, - ), + source=stripe_source(api_key="", account_id="", endpoint=schema_name, is_incremental=False), logger=structlog.get_logger(), incremental=incremental, ) diff --git a/posthog/temporal/data_imports/workflow_activities/import_data.py b/posthog/temporal/data_imports/workflow_activities/import_data.py index 27b3866db49c8..9062d389415ed 100644 --- a/posthog/temporal/data_imports/workflow_activities/import_data.py +++ b/posthog/temporal/data_imports/workflow_activities/import_data.py @@ -1,5 +1,4 @@ import dataclasses -import datetime as dt from typing import Any import uuid @@ -11,7 +10,6 @@ from posthog.temporal.data_imports.pipelines.zendesk.credentials import ZendeskCredentialsToken from posthog.temporal.data_imports.pipelines.pipeline import DataImportPipeline, PipelineInputs -from posthog.utils import get_instance_region from posthog.warehouse.models import ( ExternalDataJob, ExternalDataSource, @@ -19,7 +17,6 @@ ) from posthog.temporal.common.logger import bind_temporal_worker_logger import asyncio -from django.utils import timezone from structlog.typing import FilteringBoundLogger from posthog.warehouse.models.external_data_schema import ExternalDataSchema, aget_schema_by_id from posthog.warehouse.models.ssh_tunnel import SSHTunnel @@ -56,7 +53,7 @@ async def import_data_activity(inputs: ImportDataActivityInputs) -> tuple[TSchem source = None if model.pipeline.source_type == ExternalDataSource.Type.STRIPE: - from posthog.temporal.data_imports.pipelines.stripe.helpers import stripe_source + from posthog.temporal.data_imports.pipelines.stripe import stripe_source stripe_secret_key = model.pipeline.job_inputs.get("stripe_secret_key", None) account_id = model.pipeline.job_inputs.get("stripe_account_id", None) @@ -65,24 +62,9 @@ async def import_data_activity(inputs: ImportDataActivityInputs) -> tuple[TSchem if not stripe_secret_key: raise ValueError(f"Stripe secret key not found for job {model.id}") - # Hacky just for specific user - region = get_instance_region() - if region == "EU" and inputs.team_id == 11870: - start_date = timezone.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0) - end_date = start_date + dt.timedelta(weeks=5) - else: - start_date = None - end_date = None - + # TODO: add in check_limit to rest_source source = stripe_source( - api_key=stripe_secret_key, - account_id=account_id, - endpoints=tuple(endpoints), - team_id=inputs.team_id, - job_id=inputs.run_id, - schema_id=str(inputs.schema_id), - start_date=start_date, - end_date=end_date, + api_key=stripe_secret_key, account_id=account_id, endpoint=schema.name, is_incremental=schema.is_incremental ) return await _run(job_inputs=job_inputs, source=source, logger=logger, inputs=inputs, schema=schema) diff --git a/posthog/temporal/tests/external_data/test_external_data_job.py b/posthog/temporal/tests/external_data/test_external_data_job.py index 46ab3f2d903df..33363e24d5854 100644 --- a/posthog/temporal/tests/external_data/test_external_data_job.py +++ b/posthog/temporal/tests/external_data/test_external_data_job.py @@ -1,6 +1,6 @@ import uuid from unittest import mock -from typing import Optional +from typing import Any, Optional import pytest from asgiref.sync import sync_to_async from django.test import override_settings @@ -41,12 +41,11 @@ import aioboto3 import functools from django.conf import settings +from dlt.sources.helpers.rest_client.client import RESTClient import asyncio import psycopg -from posthog.temporal.tests.utils.s3 import read_parquet_from_s3 from posthog.warehouse.models.external_data_schema import get_all_schemas_for_source_id -from posthog.warehouse.models.external_table_definitions import get_imported_fields_for_table BUCKET_NAME = "test-external-data-jobs" SESSION = aioboto3.Session() @@ -125,7 +124,7 @@ async def postgres_connection(postgres_config, setup_postgres_test_db): async def _create_schema(schema_name: str, source: ExternalDataSource, team: Team, table_id: Optional[str] = None): return await sync_to_async(ExternalDataSchema.objects.create)( name=schema_name, - team_id=team.id, + team_id=team.pk, source_id=source.pk, table_id=table_id, ) @@ -271,7 +270,7 @@ async def setup_job_1(): team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) new_job: ExternalDataJob = await sync_to_async(ExternalDataJob.objects.create)( @@ -287,7 +286,7 @@ async def setup_job_1(): inputs = ImportDataActivityInputs( team_id=team.id, - run_id=new_job.pk, + run_id=str(new_job.pk), source_id=new_source.pk, schema_id=customer_schema.id, ) @@ -302,7 +301,7 @@ async def setup_job_2(): team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) new_job: ExternalDataJob = await sync_to_async(ExternalDataJob.objects.create)( @@ -318,7 +317,7 @@ async def setup_job_2(): inputs = ImportDataActivityInputs( team_id=team.id, - run_id=new_job.pk, + run_id=str(new_job.pk), source_id=new_source.pk, schema_id=charge_schema.id, ) @@ -328,9 +327,48 @@ async def setup_job_2(): job_1, job_1_inputs = await setup_job_1() job_2, job_2_inputs = await setup_job_2() + def mock_customers_paginate( + class_self, + path: str = "", + method: Any = "GET", + params: Optional[dict[str, Any]] = None, + json: Optional[dict[str, Any]] = None, + auth: Optional[Any] = None, + paginator: Optional[Any] = None, + data_selector: Optional[Any] = None, + hooks: Optional[Any] = None, + ): + return iter( + [ + { + "id": "cus_123", + "name": "John Doe", + } + ] + ) + + def mock_charges_paginate( + class_self, + path: str = "", + method: Any = "GET", + params: Optional[dict[str, Any]] = None, + json: Optional[dict[str, Any]] = None, + auth: Optional[Any] = None, + paginator: Optional[Any] = None, + data_selector: Optional[Any] = None, + hooks: Optional[Any] = None, + ): + return iter( + [ + { + "id": "chg_123", + "customer": "cus_1", + } + ] + ) + with ( - mock.patch("stripe.Customer.list") as mock_customer_list, - mock.patch("stripe.Charge.list") as mock_charge_list, + mock.patch.object(RESTClient, "paginate", mock_customers_paginate), override_settings( BUCKET_URL=f"s3://{BUCKET_NAME}", AIRBYTE_BUCKET_KEY=settings.OBJECT_STORAGE_ACCESS_KEY_ID, @@ -341,28 +379,8 @@ async def setup_job_2(): return_value={"clickhouse": {"id": "string", "name": "string"}}, ), ): - mock_customer_list.return_value = { - "data": [ - { - "id": "cus_123", - "name": "John Doe", - } - ], - "has_more": False, - } - - mock_charge_list.return_value = { - "data": [ - { - "id": "chg_123", - "customer": "cus_1", - } - ], - "has_more": False, - } await asyncio.gather( activity_environment.run(import_data_activity, job_1_inputs), - activity_environment.run(import_data_activity, job_2_inputs), ) job_1_customer_objects = await minio_client.list_objects_v2( @@ -370,37 +388,28 @@ async def setup_job_2(): ) assert len(job_1_customer_objects["Contents"]) == 1 - s3_data = await read_parquet_from_s3( - BUCKET_NAME, - job_1_customer_objects["Contents"][0]["Key"], - {}, - settings.OBJECT_STORAGE_ACCESS_KEY_ID, - settings.OBJECT_STORAGE_SECRET_ACCESS_KEY, - ) - customer_fields = get_imported_fields_for_table("stripe_customer") - all_keys = list(s3_data[0].keys()) - assert len(s3_data) == 1 - assert all(field in all_keys for field in customer_fields) + with ( + mock.patch.object(RESTClient, "paginate", mock_charges_paginate), + override_settings( + BUCKET_URL=f"s3://{BUCKET_NAME}", + AIRBYTE_BUCKET_KEY=settings.OBJECT_STORAGE_ACCESS_KEY_ID, + AIRBYTE_BUCKET_SECRET=settings.OBJECT_STORAGE_SECRET_ACCESS_KEY, + ), + mock.patch( + "posthog.warehouse.models.table.DataWarehouseTable.get_columns", + return_value={"clickhouse": {"id": "string", "name": "string"}}, + ), + ): + await asyncio.gather( + activity_environment.run(import_data_activity, job_2_inputs), + ) job_2_charge_objects = await minio_client.list_objects_v2( Bucket=BUCKET_NAME, Prefix=f"{job_2.folder_path}/charge/" ) assert len(job_2_charge_objects["Contents"]) == 1 - s3_data = await read_parquet_from_s3( - BUCKET_NAME, - job_2_charge_objects["Contents"][0]["Key"], - {}, - settings.OBJECT_STORAGE_ACCESS_KEY_ID, - settings.OBJECT_STORAGE_SECRET_ACCESS_KEY, - ) - customer_fields = get_imported_fields_for_table("stripe_charge") - all_keys = list(s3_data[0].keys()) - - assert len(s3_data) == 1 - assert all(field in all_keys for field in customer_fields) - @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio @@ -413,7 +422,7 @@ async def setup_job_1(): team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) # Already canceled so it should only run once @@ -431,7 +440,7 @@ async def setup_job_1(): inputs = ImportDataActivityInputs( team_id=team.id, - run_id=new_job.pk, + run_id=str(new_job.pk), source_id=new_source.pk, schema_id=customer_schema.id, ) @@ -440,8 +449,28 @@ async def setup_job_1(): job_1, job_1_inputs = await setup_job_1() + def mock_customers_paginate( + class_self, + path: str = "", + method: Any = "GET", + params: Optional[dict[str, Any]] = None, + json: Optional[dict[str, Any]] = None, + auth: Optional[Any] = None, + paginator: Optional[Any] = None, + data_selector: Optional[Any] = None, + hooks: Optional[Any] = None, + ): + return iter( + [ + { + "id": "cus_123", + "name": "John Doe", + } + ] + ) + with ( - mock.patch("stripe.Customer.list") as mock_customer_list, + mock.patch.object(RESTClient, "paginate", mock_customers_paginate), override_settings( BUCKET_URL=f"s3://{BUCKET_NAME}", AIRBYTE_BUCKET_KEY=settings.OBJECT_STORAGE_ACCESS_KEY_ID, @@ -452,15 +481,6 @@ async def setup_job_1(): return_value={"clickhouse": {"id": "string", "name": "string"}}, ), ): - mock_customer_list.return_value = { - "data": [ - { - "id": "cus_123", - "name": "John Doe", - } - ], - "has_more": True, - } await asyncio.gather( activity_environment.run(import_data_activity, job_1_inputs), ) @@ -487,7 +507,7 @@ async def setup_job_1(): team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) new_job: ExternalDataJob = await sync_to_async(ExternalDataJob.objects.create)( @@ -503,7 +523,7 @@ async def setup_job_1(): inputs = ImportDataActivityInputs( team_id=team.id, - run_id=new_job.pk, + run_id=str(new_job.pk), source_id=new_source.pk, schema_id=customer_schema.id, ) @@ -512,8 +532,28 @@ async def setup_job_1(): job_1, job_1_inputs = await setup_job_1() + def mock_customers_paginate( + class_self, + path: str = "", + method: Any = "GET", + params: Optional[dict[str, Any]] = None, + json: Optional[dict[str, Any]] = None, + auth: Optional[Any] = None, + paginator: Optional[Any] = None, + data_selector: Optional[Any] = None, + hooks: Optional[Any] = None, + ): + return iter( + [ + { + "id": "cus_123", + "name": "John Doe", + } + ] + ) + with ( - mock.patch("stripe.Customer.list") as mock_customer_list, + mock.patch.object(RESTClient, "paginate", mock_customers_paginate), mock.patch("posthog.temporal.data_imports.pipelines.helpers.CHUNK_SIZE", 0), override_settings( BUCKET_URL=f"s3://{BUCKET_NAME}", @@ -525,15 +565,6 @@ async def setup_job_1(): return_value={"clickhouse": {"id": "string", "name": "string"}}, ), ): - mock_customer_list.return_value = { - "data": [ - { - "id": "cus_123", - "name": "John Doe", - } - ], - "has_more": False, - } await asyncio.gather( activity_environment.run(import_data_activity, job_1_inputs), ) @@ -561,7 +592,7 @@ async def test_external_data_job_workflow_with_schema(team, **kwargs): team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) schema = await sync_to_async(ExternalDataSchema.objects.create)( @@ -657,7 +688,7 @@ async def setup_job_1(): posthog_test_schema = await _create_schema("posthog_test", new_source, team) inputs = ImportDataActivityInputs( - team_id=team.id, run_id=new_job.pk, source_id=new_source.pk, schema_id=posthog_test_schema.id + team_id=team.id, run_id=str(new_job.pk), source_id=new_source.pk, schema_id=posthog_test_schema.id ) return new_job, inputs @@ -689,7 +720,7 @@ async def test_check_schedule_activity_with_schema_id(activity_environment, team team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) test_1_schema = await _create_schema("test-1", new_source, team) @@ -716,7 +747,7 @@ async def test_check_schedule_activity_with_missing_schema_id_but_with_schedule( team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) await sync_to_async(ExternalDataSchema.objects.create)( @@ -760,7 +791,7 @@ async def test_check_schedule_activity_with_missing_schema_id_and_no_schedule(ac team=team, status="running", source_type="Stripe", - job_inputs={"stripe_secret_key": "test-key"}, + job_inputs={"stripe_secret_key": "test-key", "stripe_account_id": "acct_id"}, ) await sync_to_async(ExternalDataSchema.objects.create)( diff --git a/posthog/test/test_migration_0410.py b/posthog/test/test_migration_0410.py deleted file mode 100644 index 07e33eaf756de..0000000000000 --- a/posthog/test/test_migration_0410.py +++ /dev/null @@ -1,67 +0,0 @@ -from posthog.test.base import NonAtomicTestMigrations - -from posthog.models.action import Action -from posthog.models.action.action_step import ActionStep -from posthog.models.team import Team -from posthog.models.organization import Organization - - -class TestActionStepsJSONMigration(NonAtomicTestMigrations): - migrate_from = "0409_action_steps_json_alter_actionstep_action" - migrate_to = "0410_action_steps_population" - - CLASS_DATA_LEVEL_SETUP = False - - def setUpBeforeMigration(self, apps): - org = Organization.objects.create(name="o1") - team = Team.objects.create(name="t1", organization=org) - - # Create a lot of actions - - for i in range(1000): - # We create this with sql as it won't have the new fields - sql = f"""INSERT INTO posthog_action (name, team_id, description, created_at, updated_at, deleted, post_to_slack, slack_message_format, is_calculating, last_calculated_at) - VALUES ('action{i}', {team.pk}, '', '2022-01-01', '2022-01-01', FALSE, FALSE, '', FALSE, '2022-01-01') RETURNING id; - """ - - action = Action.objects.raw(sql)[0] - - # We create this with sql as it won't have the new fields - sql = f"""INSERT INTO posthog_actionstep (action_id, tag_name, text, text_matching, href, href_matching, selector, url, url_matching, event, properties) - VALUES ({action.pk}, 'tag1', 'text1', 'exact', 'href1', 'exact', 'selector1', 'url1', 'exact', 'event1', '{{"key1": "value1"}}') RETURNING id; - """ - - ActionStep.objects.raw(sql)[0] - - def test_migrate_action_steps(self): - apps = self.apps - if apps is None: - # obey mypy - raise Exception("apps is None") - - all_actions = Action.objects.prefetch_related("action_steps").all() - - assert len(all_actions) == 1000 - - for action in all_actions: - assert action.steps_json == [ - { - "tag_name": "tag1", - "text": "text1", - "text_matching": "exact", - "href": "href1", - "href_matching": "exact", - "selector": "selector1", - "url": "url1", - "url_matching": "exact", - "event": "event1", - "properties": {"key1": "value1"}, - } - ], action - - def tearDown(self): - # Clean up all the steps and actions - ActionStep.objects.raw("DELETE FROM posthog_actionstep") - Action.objects.raw("DELETE FROM posthog_action") - - super().tearDown() diff --git a/posthog/test/test_schema_helpers.py b/posthog/test/test_schema_helpers.py index 012bf21d0c3d9..a0ca3ec5d789d 100644 --- a/posthog/test/test_schema_helpers.py +++ b/posthog/test/test_schema_helpers.py @@ -35,8 +35,8 @@ def test_serializes_to_differing_json_for_default_value(self): equal property filters can be distinguished. """ - q1 = EventPropertyFilter(key="abc", operator=PropertyOperator.gt) - q2 = PersonPropertyFilter(key="abc", operator=PropertyOperator.gt) + q1 = EventPropertyFilter(key="abc", operator=PropertyOperator.GT) + q2 = PersonPropertyFilter(key="abc", operator=PropertyOperator.GT) self.assertNotEqual(to_dict(q1), to_dict(q2)) self.assertIn("'type': 'event'", str(to_dict(q1))) @@ -50,7 +50,7 @@ def test_serializes_to_same_json_for_default_value(self): q1 = EventPropertyFilter(key="abc") q2 = EventPropertyFilter(key="abc", operator=None) - q3 = EventPropertyFilter(key="abc", operator=PropertyOperator.exact) + q3 = EventPropertyFilter(key="abc", operator=PropertyOperator.EXACT) self.assertEqual(to_dict(q1), to_dict(q2)) self.assertEqual(to_dict(q2), to_dict(q3)) @@ -149,29 +149,29 @@ def test_serializes_trends_filter(self, f1, f2, num_keys): (None, {}, 0), # general: ordering of keys ( - {"funnelVizType": FunnelVizType.time_to_convert, "funnelOrderType": StepOrderValue.strict}, - {"funnelOrderType": StepOrderValue.strict, "funnelVizType": FunnelVizType.time_to_convert}, + {"funnelVizType": FunnelVizType.TIME_TO_CONVERT, "funnelOrderType": StepOrderValue.STRICT}, + {"funnelOrderType": StepOrderValue.STRICT, "funnelVizType": FunnelVizType.TIME_TO_CONVERT}, 2, ), # binCount # ({}, {"binCount": 4}, 0), ( - {"binCount": 4, "funnelVizType": FunnelVizType.time_to_convert}, - {"binCount": 4, "funnelVizType": FunnelVizType.time_to_convert}, + {"binCount": 4, "funnelVizType": FunnelVizType.TIME_TO_CONVERT}, + {"binCount": 4, "funnelVizType": FunnelVizType.TIME_TO_CONVERT}, 2, ), # breakdownAttributionType - ({}, {"breakdownAttributionType": BreakdownAttributionType.first_touch}, 0), + ({}, {"breakdownAttributionType": BreakdownAttributionType.FIRST_TOUCH}, 0), ( - {"breakdownAttributionType": BreakdownAttributionType.last_touch}, - {"breakdownAttributionType": BreakdownAttributionType.last_touch}, + {"breakdownAttributionType": BreakdownAttributionType.LAST_TOUCH}, + {"breakdownAttributionType": BreakdownAttributionType.LAST_TOUCH}, 1, ), # breakdownAttributionValue # ({}, {"breakdownAttributionValue": 2}, 0), ( - {"breakdownAttributionType": BreakdownAttributionType.step, "breakdownAttributionValue": 2}, - {"breakdownAttributionType": BreakdownAttributionType.step, "breakdownAttributionValue": 2}, + {"breakdownAttributionType": BreakdownAttributionType.STEP, "breakdownAttributionValue": 2}, + {"breakdownAttributionType": BreakdownAttributionType.STEP, "breakdownAttributionValue": 2}, 2, ), # exclusions @@ -187,35 +187,35 @@ def test_serializes_trends_filter(self, f1, f2, num_keys): # funnelFromStep and funnelToStep ({"funnelFromStep": 1, "funnelToStep": 2}, {"funnelFromStep": 1, "funnelToStep": 2}, 2), # funnelOrderType - ({}, {"funnelOrderType": StepOrderValue.ordered}, 0), - ({"funnelOrderType": StepOrderValue.strict}, {"funnelOrderType": StepOrderValue.strict}, 1), + ({}, {"funnelOrderType": StepOrderValue.ORDERED}, 0), + ({"funnelOrderType": StepOrderValue.STRICT}, {"funnelOrderType": StepOrderValue.STRICT}, 1), # funnelStepReference - ({}, {"funnelStepReference": FunnelStepReference.total}, 0), + ({}, {"funnelStepReference": FunnelStepReference.TOTAL}, 0), ( - {"funnelStepReference": FunnelStepReference.previous}, - {"funnelStepReference": FunnelStepReference.previous}, + {"funnelStepReference": FunnelStepReference.PREVIOUS}, + {"funnelStepReference": FunnelStepReference.PREVIOUS}, 1, ), # funnelVizType - ({}, {"funnelVizType": FunnelVizType.steps}, 0), - ({"funnelVizType": FunnelVizType.trends}, {"funnelVizType": FunnelVizType.trends}, 1), + ({}, {"funnelVizType": FunnelVizType.STEPS}, 0), + ({"funnelVizType": FunnelVizType.TRENDS}, {"funnelVizType": FunnelVizType.TRENDS}, 1), # funnelWindowInterval ({}, {"funnelWindowInterval": 14}, 0), ({"funnelWindowInterval": 12}, {"funnelWindowInterval": 12}, 1), # funnelWindowIntervalUnit - ({}, {"funnelWindowIntervalUnit": FunnelConversionWindowTimeUnit.day}, 0), + ({}, {"funnelWindowIntervalUnit": FunnelConversionWindowTimeUnit.DAY}, 0), ( - {"funnelWindowIntervalUnit": FunnelConversionWindowTimeUnit.week}, - {"funnelWindowIntervalUnit": FunnelConversionWindowTimeUnit.week}, + {"funnelWindowIntervalUnit": FunnelConversionWindowTimeUnit.WEEK}, + {"funnelWindowIntervalUnit": FunnelConversionWindowTimeUnit.WEEK}, 1, ), # hidden_legend_breakdowns # ({}, {"hidden_legend_breakdowns": []}, 0), # layout - ({}, {"breakdownAttributionType": BreakdownAttributionType.first_touch}, 0), + ({}, {"breakdownAttributionType": BreakdownAttributionType.FIRST_TOUCH}, 0), ( - {"breakdownAttributionType": BreakdownAttributionType.last_touch}, - {"breakdownAttributionType": BreakdownAttributionType.last_touch}, + {"breakdownAttributionType": BreakdownAttributionType.LAST_TOUCH}, + {"breakdownAttributionType": BreakdownAttributionType.LAST_TOUCH}, 1, ), ] diff --git a/posthog/test/test_team.py b/posthog/test/test_team.py index 1268c476a2adf..44705cac3ca2d 100644 --- a/posthog/test/test_team.py +++ b/posthog/test/test_team.py @@ -139,7 +139,7 @@ def test_team_on_cloud_uses_feature_flag_to_determine_person_on_events(self, moc with override_instance_config("PERSON_ON_EVENTS_ENABLED", False): team = Team.objects.create_with_data(organization=self.organization) self.assertEqual( - team.person_on_events_mode, PersonsOnEventsMode.person_id_override_properties_on_events + team.person_on_events_mode, PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS ) # called more than once when evaluating hogql mock_feature_enabled.assert_called_with( @@ -162,7 +162,7 @@ def test_team_on_self_hosted_uses_instance_setting_to_determine_person_on_events with override_instance_config("PERSON_ON_EVENTS_V2_ENABLED", True): team = Team.objects.create_with_data(organization=self.organization) self.assertEqual( - team.person_on_events_mode, PersonsOnEventsMode.person_id_override_properties_on_events + team.person_on_events_mode, PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS ) for args_list in mock_feature_enabled.call_args_list: # It is ok if we check other feature flags, just not `persons-on-events-v2-reads-enabled` @@ -170,7 +170,7 @@ def test_team_on_self_hosted_uses_instance_setting_to_determine_person_on_events with override_instance_config("PERSON_ON_EVENTS_V2_ENABLED", False): team = Team.objects.create_with_data(organization=self.organization) - self.assertEqual(team.person_on_events_mode, PersonsOnEventsMode.disabled) + self.assertEqual(team.person_on_events_mode, PersonsOnEventsMode.DISABLED) for args_list in mock_feature_enabled.call_args_list: # It is ok if we check other feature flags, just not `persons-on-events-v2-reads-enabled` assert args_list[0][0] != "persons-on-events-v2-reads-enabled" diff --git a/posthog/types.py b/posthog/types.py index 466b537bdea6b..c5b42dd8de896 100644 --- a/posthog/types.py +++ b/posthog/types.py @@ -23,7 +23,7 @@ HogQLPropertyFilter, InsightActorsQuery, PersonPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, SessionPropertyFilter, TrendsQuery, FunnelsQuery, @@ -53,7 +53,7 @@ ElementPropertyFilter, SessionPropertyFilter, CohortPropertyFilter, - RecordingDurationFilter, + RecordingPropertyFilter, GroupPropertyFilter, FeaturePropertyFilter, HogQLPropertyFilter, diff --git a/posthog/warehouse/api/external_data_source.py b/posthog/warehouse/api/external_data_source.py index c53717207f7fd..3b129ed275c97 100644 --- a/posthog/warehouse/api/external_data_source.py +++ b/posthog/warehouse/api/external_data_source.py @@ -12,7 +12,6 @@ from posthog.api.routing import TeamAndOrgViewSetMixin from posthog.warehouse.data_load.service import ( sync_external_data_job_workflow, - trigger_external_data_workflow, delete_external_data_schedule, cancel_external_data_workflow, delete_data_import_folder, @@ -78,8 +77,18 @@ class Meta: "prefix", "last_run_at", "schemas", + "sync_frequency", + ] + read_only_fields = [ + "id", + "created_by", + "created_at", + "status", + "source_type", + "last_run_at", + "schemas", + "prefix", ] - read_only_fields = ["id", "created_by", "created_at", "status", "source_type", "last_run_at", "schemas"] def get_last_run_at(self, instance: ExternalDataSource) -> str: latest_completed_run = ( @@ -116,6 +125,12 @@ def get_schemas(self, instance: ExternalDataSource): schemas = instance.schemas.order_by("name").all() return ExternalDataSchemaSerializer(schemas, many=True, read_only=True, context=self.context).data + def update(self, instance: ExternalDataSource, validated_data: Any) -> Any: + updated_source: ExternalDataSource = super().update(instance, validated_data) + updated_source.update_schemas() + + return updated_source + class SimpleExternalDataSourceSerializers(serializers.ModelSerializer): class Meta: @@ -448,7 +463,7 @@ def destroy(self, request: Request, *args: Any, **kwargs: Any) -> Response: @action(methods=["POST"], detail=True) def reload(self, request: Request, *args: Any, **kwargs: Any): - instance = self.get_object() + instance: ExternalDataSource = self.get_object() if is_any_external_data_job_paused(self.team_id): return Response( @@ -461,17 +476,7 @@ def reload(self, request: Request, *args: Any, **kwargs: Any): except temporalio.service.RPCError: # if the source schedule has been removed - trigger the schema schedules - for schema in ExternalDataSchema.objects.filter( - team_id=self.team_id, source_id=instance.id, should_sync=True - ).all(): - try: - trigger_external_data_workflow(schema) - except temporalio.service.RPCError as e: - if e.status == temporalio.service.RPCStatusCode.NOT_FOUND: - sync_external_data_job_workflow(schema, create=True) - - except Exception as e: - logger.exception(f"Could not trigger external data job for schema {schema.name}", exc_info=e) + instance.reload_schemas() except Exception as e: logger.exception("Could not trigger external data job", exc_info=e) diff --git a/posthog/warehouse/api/table.py b/posthog/warehouse/api/table.py index e460175620fe8..58d6296cb2ae3 100644 --- a/posthog/warehouse/api/table.py +++ b/posthog/warehouse/api/table.py @@ -200,7 +200,7 @@ def update_schema(self, request: request.Request, *args: Any, **kwargs: Any) -> for key, value in updates.items(): try: - DatabaseSerializedFieldType[value] + DatabaseSerializedFieldType[value.upper()] except: return response.Response( status=status.HTTP_400_BAD_REQUEST, diff --git a/posthog/warehouse/api/test/test_external_data_source.py b/posthog/warehouse/api/test/test_external_data_source.py index 1c23807b82328..da7fb63d7e105 100644 --- a/posthog/warehouse/api/test/test_external_data_source.py +++ b/posthog/warehouse/api/test/test_external_data_source.py @@ -6,10 +6,14 @@ from posthog.temporal.data_imports.pipelines.schemas import ( PIPELINE_TYPE_SCHEMA_DEFAULT_MAPPING, ) +from posthog.warehouse.data_load.service import get_sync_schedule from django.test import override_settings from django.conf import settings from posthog.models import Team import psycopg +from rest_framework import status + +import datetime class TestSavedQuery(APIBaseTest): @@ -102,7 +106,17 @@ def test_get_external_data_source_with_schema(self): self.assertEqual(response.status_code, 200) self.assertListEqual( list(payload.keys()), - ["id", "created_at", "created_by", "status", "source_type", "prefix", "last_run_at", "schemas"], + [ + "id", + "created_at", + "created_by", + "status", + "source_type", + "prefix", + "last_run_at", + "schemas", + "sync_frequency", + ], ) self.assertEqual( payload["schemas"], @@ -280,3 +294,36 @@ def test_internal_postgres(self, patch_get_postgres_schemas): ) self.assertEqual(response.status_code, 400) self.assertEqual(response.json(), {"message": "Cannot use internal Postgres database"}) + + @patch("posthog.warehouse.data_load.service.sync_external_data_job_workflow") + def test_update_source_sync_frequency(self, _patch_sync_external_data_job_workflow): + source = self._create_external_data_source() + schema = self._create_external_data_schema(source.pk) + + self.assertEqual(source.sync_frequency, ExternalDataSource.SyncFrequency.DAILY) + # test schedule + schedule = get_sync_schedule(schema) + self.assertEqual( + schedule.spec.intervals[0].every, + datetime.timedelta(days=1), + ) + + # test api + response = self.client.patch( + f"/api/projects/{self.team.id}/external_data_sources/{source.pk}/", + data={"sync_frequency": ExternalDataSource.SyncFrequency.WEEKLY}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + source.refresh_from_db() + schema.refresh_from_db() + + self.assertEqual(source.sync_frequency, ExternalDataSource.SyncFrequency.WEEKLY) + self.assertEqual(_patch_sync_external_data_job_workflow.call_count, 1) + + # test schedule + schedule = get_sync_schedule(schema) + self.assertEqual( + schedule.spec.intervals[0].every, + datetime.timedelta(days=7), + ) diff --git a/posthog/warehouse/data_load/service.py b/posthog/warehouse/data_load/service.py index 688ee42b788b5..c3f97406c56cf 100644 --- a/posthog/warehouse/data_load/service.py +++ b/posthog/warehouse/data_load/service.py @@ -46,6 +46,8 @@ def get_sync_schedule(external_data_schema: ExternalDataSchema): external_data_source_id=external_data_schema.source_id, ) + sync_frequency = get_sync_frequency(external_data_schema) + return Schedule( action=ScheduleActionStartWorkflow( "external-data-job", @@ -55,9 +57,7 @@ def get_sync_schedule(external_data_schema: ExternalDataSchema): ), spec=ScheduleSpec( intervals=[ - ScheduleIntervalSpec( - every=timedelta(hours=24), offset=timedelta(hours=external_data_schema.created_at.hour) - ) + ScheduleIntervalSpec(every=sync_frequency, offset=timedelta(hours=external_data_schema.created_at.hour)) ], jitter=timedelta(hours=2), ), @@ -66,6 +66,17 @@ def get_sync_schedule(external_data_schema: ExternalDataSchema): ) +def get_sync_frequency(external_data_schema: ExternalDataSchema): + if external_data_schema.source.sync_frequency == ExternalDataSource.SyncFrequency.DAILY: + return timedelta(days=1) + elif external_data_schema.source.sync_frequency == ExternalDataSource.SyncFrequency.WEEKLY: + return timedelta(weeks=1) + elif external_data_schema.source.sync_frequency == ExternalDataSource.SyncFrequency.MONTHLY: + return timedelta(days=30) + else: + raise ValueError(f"Unknown sync frequency: {external_data_schema.source.sync_frequency}") + + def sync_external_data_job_workflow( external_data_schema: ExternalDataSchema, create: bool = False ) -> ExternalDataSchema: diff --git a/posthog/warehouse/models/external_data_source.py b/posthog/warehouse/models/external_data_source.py index 3b0c8f45aaf6b..58aa891c34db0 100644 --- a/posthog/warehouse/models/external_data_source.py +++ b/posthog/warehouse/models/external_data_source.py @@ -6,6 +6,11 @@ from posthog.warehouse.util import database_sync_to_async from uuid import UUID +import structlog +import temporalio + +logger = structlog.get_logger(__name__) + class ExternalDataSource(CreatedMetaFields, UUIDModel): class Type(models.TextChoices): @@ -22,11 +27,21 @@ class Status(models.TextChoices): COMPLETED = "Completed", "Completed" CANCELLED = "Cancelled", "Cancelled" + class SyncFrequency(models.TextChoices): + DAILY = "day", "Daily" + WEEKLY = "week", "Weekly" + MONTHLY = "month", "Monthly" + # TODO provide flexible schedule definition + source_id: models.CharField = models.CharField(max_length=400) connection_id: models.CharField = models.CharField(max_length=400) destination_id: models.CharField = models.CharField(max_length=400, null=True, blank=True) team: models.ForeignKey = models.ForeignKey(Team, on_delete=models.CASCADE) + sync_frequency: models.CharField = models.CharField( + max_length=128, choices=SyncFrequency.choices, default=SyncFrequency.DAILY, blank=True + ) + # `status` is deprecated in favour of external_data_schema.status status: models.CharField = models.CharField(max_length=400) source_type: models.CharField = models.CharField(max_length=128, choices=Type.choices) @@ -38,6 +53,31 @@ class Status(models.TextChoices): __repr__ = sane_repr("id") + def reload_schemas(self): + from posthog.warehouse.models.external_data_schema import ExternalDataSchema + from posthog.warehouse.data_load.service import sync_external_data_job_workflow, trigger_external_data_workflow + + for schema in ExternalDataSchema.objects.filter( + team_id=self.team.pk, source_id=self.id, should_sync=True + ).all(): + try: + trigger_external_data_workflow(schema) + except temporalio.service.RPCError as e: + if e.status == temporalio.service.RPCStatusCode.NOT_FOUND: + sync_external_data_job_workflow(schema, create=True) + + except Exception as e: + logger.exception(f"Could not trigger external data job for schema {schema.name}", exc_info=e) + + def update_schemas(self): + from posthog.warehouse.models.external_data_schema import ExternalDataSchema + from posthog.warehouse.data_load.service import sync_external_data_job_workflow + + for schema in ExternalDataSchema.objects.filter( + team_id=self.team.pk, source_id=self.id, should_sync=True + ).all(): + sync_external_data_job_workflow(schema, create=False) + @database_sync_to_async def get_external_data_source(source_id: UUID) -> ExternalDataSource: diff --git a/posthog/warehouse/models/external_table_definitions.py b/posthog/warehouse/models/external_table_definitions.py index 7030e3ccb7874..d0e4c57e35c89 100644 --- a/posthog/warehouse/models/external_table_definitions.py +++ b/posthog/warehouse/models/external_table_definitions.py @@ -638,7 +638,3 @@ def get_dlt_mapping_for_external_table(table): for _, field in external_tables[table].items() if type(field) != ast.ExpressionField } - - -def get_imported_fields_for_table(table): - return [field.name for _, field in external_tables[table].items() if type(field) != ast.ExpressionField] diff --git a/posthog/warehouse/models/table.py b/posthog/warehouse/models/table.py index f8893768c03b4..d52adb48b123b 100644 --- a/posthog/warehouse/models/table.py +++ b/posthog/warehouse/models/table.py @@ -35,14 +35,14 @@ from .external_table_definitions import external_tables SERIALIZED_FIELD_TO_CLICKHOUSE_MAPPING: dict[DatabaseSerializedFieldType, str] = { - DatabaseSerializedFieldType.integer: "Int64", - DatabaseSerializedFieldType.float: "Float64", - DatabaseSerializedFieldType.string: "String", - DatabaseSerializedFieldType.datetime: "DateTime64", - DatabaseSerializedFieldType.date: "Date", - DatabaseSerializedFieldType.boolean: "Bool", - DatabaseSerializedFieldType.array: "Array", - DatabaseSerializedFieldType.json: "Map", + DatabaseSerializedFieldType.INTEGER: "Int64", + DatabaseSerializedFieldType.FLOAT: "Float64", + DatabaseSerializedFieldType.STRING: "String", + DatabaseSerializedFieldType.DATETIME: "DateTime64", + DatabaseSerializedFieldType.DATE: "Date", + DatabaseSerializedFieldType.BOOLEAN: "Bool", + DatabaseSerializedFieldType.ARRAY: "Array", + DatabaseSerializedFieldType.JSON: "Map", } CLICKHOUSE_HOGQL_MAPPING = { diff --git a/production.Dockerfile b/production.Dockerfile index 4f58ac88554ec..1e3eb2d11551f 100644 --- a/production.Dockerfile +++ b/production.Dockerfile @@ -315,4 +315,4 @@ EXPOSE 8000 EXPOSE 8001 COPY unit.json.tpl /docker-entrypoint.d/unit.json.tpl USER root -CMD ["./bin/docker"] +CMD ["./bin/docker"] \ No newline at end of file diff --git a/requirements-dev.in b/requirements-dev.in index 691e790df2b35..35ad7044d22dd 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -21,6 +21,7 @@ django-stubs==4.2.7 Faker==17.5.0 fakeredis[lua]==2.11.0 freezegun==1.2.2 +inline-snapshot==0.10.2 packaging==23.1 black~=23.9.1 boto3-stubs[s3] diff --git a/requirements-dev.txt b/requirements-dev.txt index 6a0b72290d43b..ded32c00acb9f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,9 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile requirements-dev.in -o requirements-dev.txt +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --output-file=requirements-dev.txt requirements-dev.in +# aiohttp==3.9.3 # via # -c requirements.txt @@ -20,6 +24,8 @@ asgiref==3.7.2 # via # -c requirements.txt # django +asttokens==2.4.1 + # via inline-snapshot async-timeout==4.0.2 # via # -c requirements.txt @@ -33,10 +39,10 @@ attrs==23.2.0 # referencing black==23.9.1 # via - # -c requirements.txt # -r requirements-dev.in # datamodel-code-generator -boto3-stubs==1.34.84 + # inline-snapshot +boto3-stubs[s3]==1.34.84 # via -r requirements-dev.in botocore-stubs==1.34.84 # via boto3-stubs @@ -58,9 +64,10 @@ click==8.1.7 # via # -c requirements.txt # black + # inline-snapshot colorama==0.4.4 # via pytest-watch -coverage==5.5 +coverage[toml]==5.5 # via pytest-cov cryptography==37.0.2 # via @@ -95,9 +102,11 @@ exceptiongroup==1.2.1 # pytest execnet==2.1.1 # via pytest-xdist +executing==2.0.1 + # via inline-snapshot faker==17.5.0 # via -r requirements-dev.in -fakeredis==2.11.0 +fakeredis[lua]==2.11.0 # via -r requirements-dev.in flaky==3.7.0 # via -r requirements-dev.in @@ -122,6 +131,8 @@ inflect==5.6.2 # via datamodel-code-generator iniconfig==1.1.1 # via pytest +inline-snapshot==0.10.2 + # via -r requirements-dev.in isort==5.2.2 # via datamodel-code-generator jinja2==3.1.4 @@ -142,8 +153,12 @@ lazy-object-proxy==1.10.0 # via openapi-spec-validator lupa==1.14.1 # via fakeredis +markdown-it-py==3.0.0 + # via rich markupsafe==2.1.5 # via jinja2 +mdurl==0.1.2 + # via markdown-it-py multidict==6.0.2 # via # -c requirements.txt @@ -157,7 +172,6 @@ mypy-boto3-s3==1.34.65 # via boto3-stubs mypy-extensions==1.0.0 # via - # -c requirements.txt # -r requirements-dev.in # black # mypy @@ -178,9 +192,7 @@ parameterized==0.9.0 pathable==0.4.3 # via jsonschema-path pathspec==0.12.1 - # via - # -c requirements.txt - # black + # via black platformdirs==3.11.0 # via # -c requirements.txt @@ -195,7 +207,7 @@ pycparser==2.20 # via # -c requirements.txt # cffi -pydantic==2.5.3 +pydantic[email]==2.5.3 # via # -c requirements.txt # datamodel-code-generator @@ -203,6 +215,8 @@ pydantic-core==2.14.6 # via # -c requirements.txt # pydantic +pygments==2.18.0 + # via rich pytest==7.4.4 # via # -r requirements-dev.in @@ -267,6 +281,8 @@ responses==0.23.1 # via -r requirements-dev.in rfc3339-validator==0.1.4 # via openapi-schema-validator +rich==13.7.1 + # via inline-snapshot rpds-py==0.16.2 # via # -c requirements.txt @@ -281,6 +297,7 @@ ruff==0.4.3 six==1.16.0 # via # -c requirements.txt + # asttokens # prance # python-dateutil # rfc3339-validator @@ -294,13 +311,13 @@ sqlparse==0.4.4 # django syrupy==4.6.0 # via -r requirements-dev.in -toml==0.10.1 +toml==0.10.2 # via # coverage # datamodel-code-generator + # inline-snapshot tomli==2.0.1 # via - # -c requirements.txt # black # django-stubs # mypy @@ -336,6 +353,8 @@ types-retry==0.9.9.4 # via -r requirements-dev.in types-s3transfer==0.10.1 # via boto3-stubs +types-toml==0.10.8.20240310 + # via inline-snapshot types-tzlocal==5.1.0.1 # via -r requirements-dev.in typing-extensions==4.7.1 diff --git a/requirements.in b/requirements.in index 72dba7f70ba2e..2b7efd33c6ddb 100644 --- a/requirements.in +++ b/requirements.in @@ -93,5 +93,5 @@ phonenumberslite==8.13.6 openai==1.10.0 tiktoken==0.6.0 nh3==0.2.14 -hogql-parser==1.0.11 +hogql-parser==1.0.12 zxcvbn==4.4.28 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dcc57107014fb..3ab55989b8abe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -276,7 +276,7 @@ h11==0.13.0 # wsproto hexbytes==1.0.0 # via dlt -hogql-parser==1.0.11 +hogql-parser==1.0.12 # via -r requirements.in httpcore==1.0.2 # via httpx diff --git a/rust/.github/workflows/rust.yml b/rust/.github/workflows/rust.yml deleted file mode 100644 index ff028773ab724..0000000000000 --- a/rust/.github/workflows/rust.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Rust - -on: - workflow_dispatch: - push: - branches: [main] - pull_request: - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - runs-on: depot-ubuntu-22.04-4 - - steps: - - uses: actions/checkout@v3 - - - name: Install rust - uses: dtolnay/rust-toolchain@1.77 - - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} - - - name: Run cargo build - run: cargo build --all --locked --release && find target/release/ -maxdepth 1 -executable -type f | xargs strip - - test: - runs-on: depot-ubuntu-22.04-4 - timeout-minutes: 10 - - steps: - - uses: actions/checkout@v3 - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: posthog - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Setup dependencies - run: | - docker compose up kafka redis db echo_server -d --wait - docker compose up setup_test_db - echo "127.0.0.1 kafka" | sudo tee -a /etc/hosts - - - name: Install rust - uses: dtolnay/rust-toolchain@1.77 - - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${ runner.os }-cargo-debug-${{ hashFiles('**/Cargo.lock') }} - - - name: Run cargo test - run: cargo test --all-features - - linting: - runs-on: depot-ubuntu-22.04-4 - - steps: - - uses: actions/checkout@v3 - - - name: Install rust - uses: dtolnay/rust-toolchain@1.77 - with: - components: clippy,rustfmt - - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-debug-${{ hashFiles('**/Cargo.lock') }} - - - name: Check format - run: cargo fmt -- --check - - - name: Run clippy - run: cargo clippy -- -D warnings - - - name: Run cargo check - run: cargo check --all-features - - shear: - runs-on: depot-ubuntu-22.04-4 - steps: - - uses: actions/checkout@v3 - - - name: Install cargo-binstall - uses: cargo-bins/cargo-binstall@main - - - name: Install cargo-shear - run: cargo binstall --no-confirm cargo-shear - - - run: cargo shear diff --git a/rust/Dockerfile.migrate b/rust/Dockerfile.migrate index 47790778f4396..e7fc120360b79 100644 --- a/rust/Dockerfile.migrate +++ b/rust/Dockerfile.migrate @@ -6,8 +6,8 @@ RUN cargo install sqlx-cli@0.7.3 --no-default-features --features native-tls,pos FROM debian:bullseye-20230320-slim AS runtime WORKDIR /sqlx -ADD bin /sqlx/bin/ -ADD migrations /sqlx/migrations/ +COPY bin /sqlx/bin/ +COPY migrations /sqlx/migrations/ COPY --from=builder /app/target/release/bin/sqlx /usr/local/bin diff --git a/rust/depot.json b/rust/depot.json index 24c71eb973113..316dd6ef94564 100644 --- a/rust/depot.json +++ b/rust/depot.json @@ -1 +1 @@ -{ "id": "zcszdgwzsw" } +{ "id": "x19jffd9zf" } From b5341ad9fa0d4cf6f822a9679926df088dadced3 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Wed, 12 Jun 2024 10:56:41 +0100 Subject: [PATCH 249/249] chore: merge master --- docker/livestream/configs-dev.yml | 14 + ee/api/test/test_billing.py | 222 +- ee/billing/billing_manager.py | 104 +- .../queries/funnels/funnel_correlation.py | 25 +- ee/clickhouse/queries/groups_join_query.py | 4 +- ...ilters-universalfilters--default--dark.png | Bin 0 -> 7286 bytes ...lters-universalfilters--default--light.png | Bin 0 -> 7318 bytes ...el-left-to-right-breakdown-edit--light.png | Bin 177354 -> 179111 bytes ...el-top-to-bottom-breakdown-edit--light.png | Bin 181038 -> 179239 bytes ...app-pipeline--pipeline-node-logs--dark.png | Bin 58579 -> 55899 bytes ...pp-pipeline--pipeline-node-logs--light.png | Bin 58887 -> 55938 bytes ...-pipeline-node-logs-batch-export--dark.png | Bin 72578 -> 72671 bytes ...pipeline-node-logs-batch-export--light.png | Bin 72252 -> 72376 bytes frontend/src/lib/api.ts | 7 + .../TaxonomicFilter/InfiniteSelectResults.tsx | 2 +- frontend/src/lib/constants.tsx | 1 + frontend/src/scenes/billing/Billing.tsx | 2 +- .../src/scenes/billing/BillingProduct.tsx | 4 +- .../billing/BillingProductPricingTable.tsx | 5 +- .../error-tracking/ErrorTrackingScene.tsx | 46 + .../error-tracking/errorTrackingSceneLogic.ts | 47 + .../src/scenes/pipeline/PipelineNodeLogs.tsx | 15 +- .../hogfunctions/HogFunctionInputs.tsx | 287 ++ .../hogfunctions/HogFunctionInputsEditor.tsx | 116 + .../PipelineHogFunctionConfiguration.tsx | 247 ++ .../pipelineHogFunctionConfigurationLogic.tsx | 250 ++ .../hogfunctions/templates/hog-templates.tsx | 58 + .../scenes/pipeline/pipelineNodeLogsLogic.tsx | 126 +- .../filters/RecordingsUniversalFilters.tsx | 138 + .../filters/ReplayTaxonomicFilters.tsx | 109 + frontend/src/scenes/settings/SettingsMap.tsx | 3 +- frontend/src/scenes/settings/settingsLogic.ts | 20 +- frontend/src/scenes/settings/types.ts | 14 +- .../scenes/surveys/QuestionBranchingInput.tsx | 68 + hogql_parser/HogQLParser.cpp | 2757 ++++++++-------- hogql_parser/HogQLParser.h | 86 +- hogql_parser/HogQLParser.interp | 8 +- hogql_parser/HogQLParserBaseVisitor.h | 14 +- hogql_parser/HogQLParserVisitor.h | 10 +- hogql_parser/parser.cpp | 4 +- hogvm/__tests__/__snapshots__/functions.hoge | 15 +- hogvm/__tests__/arrays.hog | 22 +- hogvm/__tests__/dicts.hog | 22 +- hogvm/__tests__/functions.hog | 44 +- hogvm/__tests__/ifElse.hog | 18 +- hogvm/__tests__/json.hog | 12 +- hogvm/__tests__/loops.hog | 10 +- hogvm/__tests__/mandelbrot.hog | 56 +- hogvm/__tests__/mandelbrot.hoge | 8 - hogvm/__tests__/operations.hog | 134 +- hogvm/__tests__/properties.hog | 72 +- hogvm/__tests__/stl.hog | 8 +- hogvm/__tests__/tuples.hog | 18 +- hogvm/__tests__/variables.hog | 20 +- hogvm/python/execute.py | 3 + hogvm/typescript/src/index.ts | 3 + mypy-baseline.txt | 88 +- .../src/cdp/cdp-processed-events-consumer.ts | 278 ++ plugin-server/src/cdp/hog-executor.ts | 325 ++ plugin-server/src/cdp/hog-function-manager.ts | 132 + plugin-server/src/cdp/types.ts | 161 + plugin-server/src/cdp/utils.ts | 93 + plugin-server/src/utils/db/db.ts | 14 +- plugin-server/src/utils/event.ts | 11 +- .../src/worker/ingestion/person-state.ts | 17 + .../cdp/cdp-processed-events-consumer.test.ts | 220 ++ plugin-server/tests/cdp/examples.ts | 161 + plugin-server/tests/cdp/fixtures.ts | 93 + plugin-server/tests/cdp/hog-executor.test.ts | 123 + .../worker/ingestion/person-state.test.ts | 10 +- posthog/api/hog_function.py | 182 ++ posthog/api/log_entries.py | 121 + posthog/api/test/test_hog_function.py | 287 ++ posthog/clickhouse/client/limit.py | 84 + .../client/test/test_execute_async.py | 2 +- posthog/hogql/ast.py | 2 +- posthog/hogql/autocomplete.py | 2 +- posthog/hogql/bytecode.py | 2 + .../schema/util/where_clause_extractor.py | 6 + posthog/hogql/grammar/HogQLParser.g4 | 26 +- posthog/hogql/grammar/HogQLParser.interp | 8 +- posthog/hogql/grammar/HogQLParser.py | 2768 +++++++++-------- posthog/hogql/grammar/HogQLParserVisitor.py | 22 +- posthog/hogql/parser.py | 20 +- posthog/hogql/printer.py | 4 +- posthog/hogql/resolver.py | 2 +- posthog/hogql/test/_test_parser.py | 6 +- posthog/hogql/test/test_printer.py | 13 +- posthog/hogql/test/test_query.py | 6 +- posthog/hogql/visitor.py | 6 +- .../hogql_queries/insights/funnels/base.py | 40 +- .../insights/funnels/funnel_strict.py | 4 +- .../insights/funnels/funnel_unordered.py | 2 +- posthog/migrations/0425_hogfunction.py | 47 + .../0426_externaldatasource_sync_frequency.py | 22 + posthog/models/hog_functions/__init__.py | 1 + posthog/models/hog_functions/hog_function.py | 118 + posthog/models/hog_functions/utils.py | 66 + posthog/models/person/person.py | 17 +- posthog/models/test/test_hog_function.py | 283 ++ posthog/queries/breakdown_props.py | 6 +- posthog/queries/event_query/event_query.py | 4 +- posthog/queries/funnels/base.py | 9 +- .../groups_join_query/groups_join_query.py | 3 +- posthog/queries/trends/breakdown.py | 3 +- posthog/queries/util.py | 15 +- .../data_imports/pipelines/helpers.py | 14 +- .../pipelines/rest_source/__init__.py | 370 +++ .../pipelines/rest_source/config_setup.py | 455 +++ .../pipelines/rest_source/exceptions.py | 5 + .../pipelines/rest_source/typing.py | 254 ++ .../pipelines/rest_source/utils.py | 36 + .../data_imports/pipelines/stripe/__init__.py | 16 +- .../pipelines/test/test_pipeline.py | 9 +- .../workflow_activities/import_data.py | 8 +- .../external_data/test_external_data_job.py | 3 +- 116 files changed, 8781 insertions(+), 3602 deletions(-) create mode 100644 docker/livestream/configs-dev.yml create mode 100644 frontend/__snapshots__/filters-universalfilters--default--dark.png create mode 100644 frontend/__snapshots__/filters-universalfilters--default--light.png create mode 100644 frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx create mode 100644 frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts create mode 100644 frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx create mode 100644 frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputsEditor.tsx create mode 100644 frontend/src/scenes/pipeline/hogfunctions/PipelineHogFunctionConfiguration.tsx create mode 100644 frontend/src/scenes/pipeline/hogfunctions/pipelineHogFunctionConfigurationLogic.tsx create mode 100644 frontend/src/scenes/pipeline/hogfunctions/templates/hog-templates.tsx create mode 100644 frontend/src/scenes/session-recordings/filters/RecordingsUniversalFilters.tsx create mode 100644 frontend/src/scenes/session-recordings/filters/ReplayTaxonomicFilters.tsx create mode 100644 frontend/src/scenes/surveys/QuestionBranchingInput.tsx delete mode 100644 hogvm/__tests__/mandelbrot.hoge create mode 100644 hogvm/typescript/src/index.ts create mode 100644 plugin-server/src/cdp/cdp-processed-events-consumer.ts create mode 100644 plugin-server/src/cdp/hog-executor.ts create mode 100644 plugin-server/src/cdp/hog-function-manager.ts create mode 100644 plugin-server/src/cdp/types.ts create mode 100644 plugin-server/src/cdp/utils.ts create mode 100644 plugin-server/tests/cdp/cdp-processed-events-consumer.test.ts create mode 100644 plugin-server/tests/cdp/examples.ts create mode 100644 plugin-server/tests/cdp/fixtures.ts create mode 100644 plugin-server/tests/cdp/hog-executor.test.ts create mode 100644 posthog/api/hog_function.py create mode 100644 posthog/api/log_entries.py create mode 100644 posthog/api/test/test_hog_function.py create mode 100644 posthog/clickhouse/client/limit.py create mode 100644 posthog/migrations/0425_hogfunction.py create mode 100644 posthog/migrations/0426_externaldatasource_sync_frequency.py create mode 100644 posthog/models/hog_functions/__init__.py create mode 100644 posthog/models/hog_functions/hog_function.py create mode 100644 posthog/models/hog_functions/utils.py create mode 100644 posthog/models/test/test_hog_function.py create mode 100644 posthog/temporal/data_imports/pipelines/rest_source/__init__.py create mode 100644 posthog/temporal/data_imports/pipelines/rest_source/config_setup.py create mode 100644 posthog/temporal/data_imports/pipelines/rest_source/exceptions.py create mode 100644 posthog/temporal/data_imports/pipelines/rest_source/typing.py create mode 100644 posthog/temporal/data_imports/pipelines/rest_source/utils.py diff --git a/docker/livestream/configs-dev.yml b/docker/livestream/configs-dev.yml new file mode 100644 index 0000000000000..5f7daf3ac5287 --- /dev/null +++ b/docker/livestream/configs-dev.yml @@ -0,0 +1,14 @@ +prod: false +tailscale: + controlUrl: + hostname: 'live-events-dev' +kafka: + brokers: 'kafka:9092' + topic: 'events_plugin_ingestion' + group_id: 'livestream-dev' +mmdb: + path: 'GeoLite2-City.mmdb' +jwt: + token: '' +postgres: + url: 'postgres://posthog:posthog@db:5432/posthog' diff --git a/ee/api/test/test_billing.py b/ee/api/test/test_billing.py index d4586149f16c3..2b4d38dd85bd8 100644 --- a/ee/api/test/test_billing.py +++ b/ee/api/test/test_billing.py @@ -375,19 +375,11 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "unit_amount_usd": "0.00", "up_to": 1000000, "current_amount_usd": "0.00", - "current_usage": 0, - "flat_amount_usd": "0", - "projected_amount_usd": "None", - "projected_usage": None, }, { "unit_amount_usd": "0.00045", "up_to": 2000000, "current_amount_usd": "0.00", - "current_usage": 0, - "flat_amount_usd": "0", - "projected_amount_usd": "None", - "projected_usage": None, }, ], "tiered": True, @@ -418,19 +410,11 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "tiers": [ { "current_amount_usd": "0.00", - "current_usage": 0, - "flat_amount_usd": "0", - "projected_amount_usd": "None", - "projected_usage": None, "unit_amount_usd": "0.00", "up_to": 1000000, }, { "current_amount_usd": "0.00", - "current_usage": 0, - "flat_amount_usd": "0", - "projected_amount_usd": "None", - "projected_usage": None, "unit_amount_usd": "0.0000135", "up_to": 2000000, }, @@ -512,7 +496,7 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma ], "current_usage": 0, "percentage_usage": 0, - "current_amount_usd": "0.00", + "current_amount_usd": 0.0, "has_exceeded_limit": False, "projected_amount_usd": 0.0, "projected_amount": 0, @@ -525,7 +509,7 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma "usage_key": "events", "addons": [ { - "current_amount_usd": "0.00", + "current_amount_usd": 0.0, "current_usage": 0, "description": "Test Addon", "free_allocation": 10000, @@ -758,217 +742,15 @@ def mock_implementation(url: str, headers: Any = None, params: Any = None) -> Ma res_json = res.json() # Should update product usage to reflect today's usage assert res_json["products"][0]["current_usage"] == 1101000 - assert res_json["products"][0]["current_amount_usd"] == "45.45" - assert res_json["products"][0]["tiers"][0]["current_usage"] == 1000000 - assert res_json["products"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["tiers"][1]["current_usage"] == 101000 - assert res_json["products"][0]["tiers"][1]["current_amount_usd"] == "45.45" - - assert res_json["products"][0]["addons"][0]["current_usage"] == 1101000 - assert res_json["products"][0]["addons"][0]["current_amount_usd"] == "1.36" - assert res_json["products"][0]["addons"][0]["tiers"][0]["current_usage"] == 1000000 - assert res_json["products"][0]["addons"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["addons"][0]["tiers"][1]["current_usage"] == 101000 - assert res_json["products"][0]["addons"][0]["tiers"][1]["current_amount_usd"] == "1.36" - - # Now test when there is a usage_limit. - def mock_implementation_with_limit(url: str, headers: Any = None, params: Any = None) -> MagicMock: - mock = MagicMock() - mock.status_code = 404 - - if "api/billing/portal" in url: - mock.status_code = 200 - mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"} - elif "api/billing" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_response( - customer=create_billing_customer(has_active_subscription=True), - ) - mock.json.return_value["customer"]["usage_summary"]["events"]["usage"] = 1000000 - mock.json.return_value["customer"]["usage_summary"]["events"]["limit"] = 1000000 - elif "api/products" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_products_response() - - return mock - - mock_request.side_effect = mock_implementation_with_limit - self.organization.usage = {"events": {"limit": 1000000, "usage": 1000000, "todays_usage": 100}} - self.organization.save() - - res = self.client.get("/api/billing") - assert res.status_code == 200 - res_json = res.json() - # Should update product usage to reflect today's usage - assert res_json["products"][0]["current_usage"] == 1000100 - assert res_json["products"][0]["current_amount_usd"] == "0.04" - assert res_json["products"][0]["tiers"][0]["current_usage"] == 1000000 - assert res_json["products"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["tiers"][1]["current_usage"] == 100 - assert res_json["products"][0]["tiers"][1]["current_amount_usd"] == "0.04" - - assert res_json["products"][0]["addons"][0]["current_usage"] == 1000100 - assert res_json["products"][0]["addons"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["addons"][0]["tiers"][0]["current_usage"] == 1000000 - assert res_json["products"][0]["addons"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["addons"][0]["tiers"][1]["current_usage"] == 100 - assert res_json["products"][0]["addons"][0]["tiers"][1]["current_amount_usd"] == "0.00" - - def mock_implementation_exceeds_limit(url: str, headers: Any = None, params: Any = None) -> MagicMock: - mock = MagicMock() - mock.status_code = 404 - - if "api/billing/portal" in url: - mock.status_code = 200 - mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"} - elif "api/billing" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_response( - customer=create_billing_customer(has_active_subscription=True), - ) - mock.json.return_value["customer"]["usage_summary"]["events"]["usage"] = 1100000 - mock.json.return_value["customer"]["usage_summary"]["events"]["limit"] = 1000000 - elif "api/products" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_products_response() - - return mock - - mock_request.side_effect = mock_implementation_exceeds_limit - self.organization.usage = {"events": {"limit": 1000000, "usage": 1100000, "todays_usage": 1000}} - self.organization.save() - - res = self.client.get("/api/billing") - assert res.status_code == 200 - res_json = res.json() - # Should update product usage to reflect today's usage - assert res_json["products"][0]["current_usage"] == 1101000 - assert res_json["products"][0]["current_amount_usd"] == "45.00" - assert res_json["products"][0]["tiers"][0]["current_usage"] == 1000000 - assert res_json["products"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["tiers"][1]["current_usage"] == 100000 - assert res_json["products"][0]["tiers"][1]["current_amount_usd"] == "45.00" - - assert res_json["products"][0]["addons"][0]["current_usage"] == 1101000 - assert res_json["products"][0]["addons"][0]["current_amount_usd"] == "1.35" - assert res_json["products"][0]["addons"][0]["tiers"][0]["current_usage"] == 1000000 - assert res_json["products"][0]["addons"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["addons"][0]["tiers"][1]["current_usage"] == 100000 - assert res_json["products"][0]["addons"][0]["tiers"][1]["current_amount_usd"] == "1.35" - - # Test when the customer has no usage. Ensure that the tiered current_usage isn't set to the usage limit. - def mock_implementation_with_limit_no_usage(url: str, headers: Any = None, params: Any = None) -> MagicMock: - mock = MagicMock() - mock.status_code = 404 - - if "api/billing/portal" in url: - mock.status_code = 200 - mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"} - elif "api/billing" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_response( - customer=create_billing_customer(has_active_subscription=True), - ) - mock.json.return_value["customer"]["usage_summary"]["events"]["usage"] = 0 - mock.json.return_value["customer"]["usage_summary"]["events"]["limit"] = 1000000 - elif "api/products" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_products_response() - - return mock - - mock_request.side_effect = mock_implementation_with_limit_no_usage - - self.organization.usage = {"events": {"limit": 1000000, "usage": 0, "todays_usage": 0}} - self.organization.save() - - res = self.client.get("/api/billing") - assert res.status_code == 200 - res_json = res.json() - # Should update product usage to reflect today's usage - assert res_json["products"][0]["current_usage"] == 0 assert res_json["products"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["tiers"][0]["current_usage"] == 0 assert res_json["products"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["tiers"][1]["current_usage"] == 0 assert res_json["products"][0]["tiers"][1]["current_amount_usd"] == "0.00" assert res_json["products"][0]["addons"][0]["current_usage"] == 0 assert res_json["products"][0]["addons"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["addons"][0]["tiers"][0]["current_usage"] == 0 assert res_json["products"][0]["addons"][0]["tiers"][0]["current_amount_usd"] == "0.00" - assert res_json["products"][0]["addons"][0]["tiers"][1]["current_usage"] == 0 assert res_json["products"][0]["addons"][0]["tiers"][1]["current_amount_usd"] == "0.00" - def mock_implementation_missing_customer(url: str, headers: Any = None, params: Any = None) -> MagicMock: - mock = MagicMock() - mock.status_code = 404 - - if "api/billing/portal" in url: - mock.status_code = 200 - mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"} - elif "api/billing" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_response(customer=create_missing_billing_customer()) - elif "api/products" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_products_response() - - return mock - - mock_request.side_effect = mock_implementation_missing_customer - - # Test unsubscribed config - res = self.client.get("/api/billing") - self.organization.refresh_from_db() - assert self.organization.usage == { - "events": { - "limit": None, - "todays_usage": 0, - "usage": 0, - }, - "recordings": { - "limit": None, - "todays_usage": 0, - "usage": 0, - }, - "rows_synced": { - "limit": None, - "todays_usage": 0, - "usage": 0, - }, - "period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"], - } - assert self.organization.customer_id == "cus_123" - - # Now test when there is a tiered product in the response that isn't in the usage dict - def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock: - mock = MagicMock() - mock.status_code = 404 - - if "api/billing/portal" in url: - mock.status_code = 200 - mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"} - elif "api/billing" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_response( - customer=create_billing_customer(has_active_subscription=True), - ) - mock.json.return_value["customer"]["products"][0]["usage_key"] = "feature_flag_requests" - mock.json.return_value["customer"]["usage_summary"]["events"]["usage"] = 1000 - elif "api/products" in url: - mock.status_code = 200 - mock.json.return_value = create_billing_products_response() - - return mock - - mock_request.side_effect = mock_implementation - self.organization.usage = {"events": {"limit": 1000000, "usage": 1000, "todays_usage": 1100000}} - self.organization.save() - - res = self.client.get("/api/billing") - assert res.status_code == 200 - @patch("ee.api.billing.requests.get") def test_organization_usage_count_with_demo_project(self, mock_request, *args): def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock: diff --git a/ee/billing/billing_manager.py b/ee/billing/billing_manager.py index 2f35a7be3b92e..4f735d4890b3d 100644 --- a/ee/billing/billing_manager.py +++ b/ee/billing/billing_manager.py @@ -1,7 +1,6 @@ from datetime import datetime, timedelta -from decimal import Decimal from enum import Enum -from typing import Any, Optional, Union, cast +from typing import Any, Optional, cast import jwt import requests @@ -11,7 +10,7 @@ from rest_framework.exceptions import NotAuthenticated from sentry_sdk import capture_exception -from ee.billing.billing_types import BillingStatus, Tier +from ee.billing.billing_types import BillingStatus from ee.billing.quota_limiting import set_org_usage_summary, sync_org_quota_limits from ee.models import License from ee.settings import BILLING_SERVICE_URL @@ -58,61 +57,6 @@ def handle_billing_service_error(res: requests.Response, valid_codes=(200, 404, raise Exception(f"Billing service returned bad status code: {res.status_code}", f"body:", res.text) -def compute_usage_per_tier(limited_usage: int, projected_usage: int, tiers): - remaining_usage = limited_usage - remaining_projected_usage = projected_usage or 0 - previous_tier: Optional[dict[str, Any]] = None - tier_max_usage: Union[int, float] = 0 - - result: list[Tier] = [] - for tier in tiers: - if previous_tier and previous_tier.get("up_to"): - previous_tier_up_to = previous_tier["up_to"] - else: - previous_tier_up_to = 0 - - if tier.get("up_to"): - tier_max_usage = tier["up_to"] - previous_tier_up_to - else: - tier_max_usage = float("inf") - - flat_amount_usd = Decimal(tier.get("flat_amount_usd") or 0) - unit_amount_usd = Decimal(tier.get("unit_amount_usd") or 0) - usage_this_tier = int(min(remaining_usage, tier_max_usage)) - remaining_usage -= usage_this_tier - current_amount_usd = Decimal(unit_amount_usd * usage_this_tier + flat_amount_usd).quantize(Decimal("0.01")) - previous_tier = tier - if projected_usage: - projected_usage_this_tier = int(min(remaining_projected_usage, tier_max_usage)) - remaining_projected_usage -= projected_usage_this_tier - projected_amount_usd = Decimal(unit_amount_usd * projected_usage_this_tier + flat_amount_usd).quantize( - Decimal("0.01") - ) - else: - projected_usage_this_tier = None - projected_amount_usd = None - - result.append( - Tier( - flat_amount_usd=str(flat_amount_usd), - unit_amount_usd=str(unit_amount_usd), - up_to=tier.get("up_to", None), - current_amount_usd=str(current_amount_usd), - current_usage=usage_this_tier, - projected_usage=projected_usage_this_tier, - projected_amount_usd=str(projected_amount_usd), - ) - ) - return result - - -def sum_total_across_tiers(tiers): - total = Decimal(0) - for tier in tiers: - total += Decimal(tier["current_amount_usd"]) - return total - - class BillingManager: license: Optional[License] @@ -164,50 +108,6 @@ def get_billing(self, organization: Optional[Organization], plan_keys: Optional[ product["current_usage"] = current_usage product["percentage_usage"] = current_usage / usage_limit if usage_limit else 0 - - # Also update the tiers - if product.get("tiers"): - usage_limit = product_usage.get("limit") - limited_usage = 0 - # If the usage has already exceeded the billing limit, don't increment - # today's usage - if usage_limit is not None and billing_reported_usage > usage_limit: - limited_usage = billing_reported_usage - else: - limited_usage = current_usage - - product["tiers"] = compute_usage_per_tier( - limited_usage, product["projected_usage"], product["tiers"] - ) - product["current_amount_usd"] = str(sum_total_across_tiers(product["tiers"])) - - # Update the add on tiers - # TODO: enhanced_persons: make sure this updates properly for addons with different usage keys - for addon in product.get("addons"): - if not addon.get("subscribed"): - continue - addon_usage_key = addon.get("usage_key") - if not usage_key: - continue - if addon_usage_key != usage_key: - usage = response.get("usage_summary", {}).get(addon_usage_key, {}) - usage_limit = usage.get("limit") - billing_reported_usage = usage.get("usage") or 0 - if product_usage.get("todays_usage"): - todays_usage = product_usage["todays_usage"] - current_usage = billing_reported_usage + todays_usage - addon["current_usage"] = current_usage - - limited_usage = 0 - # If the usage has already exceeded the billing limit, don't increment - # today's usage - if usage_limit is not None and billing_reported_usage > usage_limit: - limited_usage = billing_reported_usage - else: - # Otherwise, do increment toady's usage - limited_usage = current_usage - addon["tiers"] = compute_usage_per_tier(limited_usage, addon["projected_usage"], addon["tiers"]) - addon["current_amount_usd"] = str(sum_total_across_tiers(addon["tiers"])) else: products = self.get_default_products(organization) response = { diff --git a/ee/clickhouse/queries/funnels/funnel_correlation.py b/ee/clickhouse/queries/funnels/funnel_correlation.py index efa84347730d7..9bf35aaa67b8c 100644 --- a/ee/clickhouse/queries/funnels/funnel_correlation.py +++ b/ee/clickhouse/queries/funnels/funnel_correlation.py @@ -29,7 +29,7 @@ from posthog.queries.insight import insight_sync_execute from posthog.queries.person_distinct_id_query import get_team_distinct_ids_query from posthog.queries.person_query import PersonQuery -from posthog.queries.util import correct_result_for_sampling +from posthog.queries.util import alias_poe_mode_for_legacy, correct_result_for_sampling from posthog.schema import PersonsOnEventsMode from posthog.utils import generate_short_id @@ -152,7 +152,7 @@ def __init__( def properties_to_include(self) -> list[str]: props_to_include = [] if ( - self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED + alias_poe_mode_for_legacy(self._team.person_on_events_mode) != PersonsOnEventsMode.DISABLED and self._filter.correlation_type == FunnelCorrelationType.PROPERTIES ): # When dealing with properties, make sure funnel response comes with properties @@ -499,7 +499,10 @@ def _get_events_join_query(self) -> str: def _get_aggregation_join_query(self): if self._filter.aggregation_group_type_index is None: - if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED and groups_on_events_querying_enabled(): + if ( + alias_poe_mode_for_legacy(self._team.person_on_events_mode) != PersonsOnEventsMode.DISABLED + and groups_on_events_querying_enabled() + ): return "", {} person_query, person_query_params = PersonQuery( @@ -519,7 +522,10 @@ def _get_aggregation_join_query(self): return GroupsJoinQuery(self._filter, self._team.pk, join_key="funnel_actors.actor_id").get_join_query() def _get_properties_prop_clause(self): - if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED and groups_on_events_querying_enabled(): + if ( + alias_poe_mode_for_legacy(self._team.person_on_events_mode) != PersonsOnEventsMode.DISABLED + and groups_on_events_querying_enabled() + ): group_properties_field = f"group{self._filter.aggregation_group_type_index}_properties" aggregation_properties_alias = ( "person_properties" if self._filter.aggregation_group_type_index is None else group_properties_field @@ -546,7 +552,9 @@ def _get_properties_prop_clause(self): param_name = f"property_name_{index}" if self._filter.aggregation_group_type_index is not None: expression, _ = get_property_string_expr( - "groups" if self._team.person_on_events_mode == PersonsOnEventsMode.DISABLED else "events", + "groups" + if alias_poe_mode_for_legacy(self._team.person_on_events_mode) == PersonsOnEventsMode.DISABLED + else "events", property_name, f"%({param_name})s", aggregation_properties_alias, @@ -554,13 +562,16 @@ def _get_properties_prop_clause(self): ) else: expression, _ = get_property_string_expr( - "person" if self._team.person_on_events_mode == PersonsOnEventsMode.DISABLED else "events", + "person" + if alias_poe_mode_for_legacy(self._team.person_on_events_mode) == PersonsOnEventsMode.DISABLED + else "events", property_name, f"%({param_name})s", aggregation_properties_alias, materialised_table_column=( aggregation_properties_alias - if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED + if alias_poe_mode_for_legacy(self._team.person_on_events_mode) + != PersonsOnEventsMode.DISABLED else "properties" ), ) diff --git a/ee/clickhouse/queries/groups_join_query.py b/ee/clickhouse/queries/groups_join_query.py index ddb7d193d6d9b..70334d0f2df1d 100644 --- a/ee/clickhouse/queries/groups_join_query.py +++ b/ee/clickhouse/queries/groups_join_query.py @@ -8,7 +8,7 @@ from posthog.models.filters.utils import GroupTypeIndex from posthog.models.property.util import parse_prop_grouped_clauses from posthog.models.team.team import groups_on_events_querying_enabled -from posthog.queries.util import PersonPropertiesMode +from posthog.queries.util import PersonPropertiesMode, alias_poe_mode_for_legacy from posthog.schema import PersonsOnEventsMode @@ -33,7 +33,7 @@ def __init__( self._team_id = team_id self._column_optimizer = column_optimizer or EnterpriseColumnOptimizer(self._filter, self._team_id) self._join_key = join_key - self._person_on_events_mode = person_on_events_mode + self._person_on_events_mode = alias_poe_mode_for_legacy(person_on_events_mode) def get_join_query(self) -> tuple[str, dict]: join_queries, params = [], {} diff --git a/frontend/__snapshots__/filters-universalfilters--default--dark.png b/frontend/__snapshots__/filters-universalfilters--default--dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9d5153cb3fe5c231cad336f1b41bde2c807885a5 GIT binary patch literal 7286 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 literal 0 HcmV?d00001 diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--light.png index 87e964fc8ec9ca9b0ca4d5ee12b162115e17b18b..47914be1ffc6ad8a960db5917a592648f297465d 100644 GIT binary patch literal 179111 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 a{xkExN_^FEbS|hUVDNPHb6Mw<&;$UC_u0$< literal 177354 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­Dq?MxX~s(u`OnzLC_`#Ae$59WK<{k{0U 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 G9{xYF*9r{) diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--light.png index f6a722836577c0a37b45716e395eaf6da5195edf..4c84d73723bed9b625ae5b6dae8d0d2cd0484da6 100644 GIT binary patch delta 145743 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

($|ih=}~z-A$7S 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 delta 147542 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 DbG$!T diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs--dark.png index 1b60d7a49a3ccac609ef11f16cd502721c68562b..4f4f10f435fcafab45a178d21749a6f2c5a84659 100644 GIT binary patch literal 55899 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 qb@10y>3B3fcAV`%Okfwqn(h0pIy@g&v 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 Q1SXR7dHHkkXKz0EUwbqbiU0rr literal 58887 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%0Dlo|uG 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 diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--dark.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-logs-batch-export--dark.png index e25ebfaf3dca94c34aa20760c476e33173abaa82..d368a3388deecb83f44cdbae71afedff3dcf22ec 100644 GIT binary patch delta 55016 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 p+KfgeSyd^MGK<%ts>(C|DWKR7v;Ed1r-^UU8z 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 G;NJksvrgCm delta 54760 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: Promise { return await new ApiRequest().hogFunction(id).update({ data }) }, + async searchLogs( + id: HogFunctionType['id'], + params: Record = {} + ): Promise> { + return await new ApiRequest().hogFunction(id).withAction('logs').withQueryString(params).get() + }, }, annotations: { diff --git a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx index f9579316b153f..89bf8bbd9b2f3 100644 --- a/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx +++ b/frontend/src/lib/components/TaxonomicFilter/InfiniteSelectResults.tsx @@ -68,7 +68,7 @@ export function InfiniteSelectResults({ selectItem(activeTaxonomicGroup, newValue, item)} + onChange={(newValue) => selectItem(activeTaxonomicGroup, newValue, newValue)} /> ) : ( diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index 07e8c6d51b86f..12cb6ea2814ec 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -205,6 +205,7 @@ export const FEATURE_FLAGS = { LIVE_EVENTS: 'live-events', // owner: @zach or @jams SESSION_REPLAY_NETWORK_VIEW: 'session-replay-network-view', // owner: #team-replay SETTINGS_PERSONS_JOIN_MODE: 'settings-persons-join-mode', // owner: @robbie-c + SETTINGS_PERSONS_ON_EVENTS_HIDDEN: 'settings-persons-on-events-hidden', // owner: @Twixes HOG: 'hog', // owner: @mariusandra HOG_FUNCTIONS: 'hog-functions', // owner: #team-cdp PERSONLESS_EVENTS_NOT_SUPPORTED: 'personless-events-not-supported', // owner: @raquelmsmith diff --git a/frontend/src/scenes/billing/Billing.tsx b/frontend/src/scenes/billing/Billing.tsx index 8bd8d5305432a..c6cb267ffd255 100644 --- a/frontend/src/scenes/billing/Billing.tsx +++ b/frontend/src/scenes/billing/Billing.tsx @@ -145,7 +145,7 @@ export function Billing(): JSX.Element { {billing?.has_active_subscription && ( <> Current bill total diff --git a/frontend/src/scenes/billing/BillingProduct.tsx b/frontend/src/scenes/billing/BillingProduct.tsx index be58d1a8eb168..215ddcd1efe84 100644 --- a/frontend/src/scenes/billing/BillingProduct.tsx +++ b/frontend/src/scenes/billing/BillingProduct.tsx @@ -208,7 +208,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): billing?.discount_percent ? 'discounted ' : '' }amount you have been billed for this ${ billing?.billing_period?.interval - } so far.`} + } so far. This number updates once daily.`} >

@@ -235,7 +235,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }): billing?.discount_percent ? ', discounts on your account,' : '' - } and the remaining time left in this billing period.`} + } and the remaining time left in this billing period. This number updates once daily.`} >
diff --git a/frontend/src/scenes/billing/BillingProductPricingTable.tsx b/frontend/src/scenes/billing/BillingProductPricingTable.tsx index 344e6077598c7..196551d3a2ba5 100644 --- a/frontend/src/scenes/billing/BillingProductPricingTable.tsx +++ b/frontend/src/scenes/billing/BillingProductPricingTable.tsx @@ -1,5 +1,5 @@ import { IconArrowRightDown, IconInfo } from '@posthog/icons' -import { LemonTable, LemonTableColumns, Link } from '@posthog/lemon-ui' +import { LemonBanner, LemonTable, LemonTableColumns, Link } from '@posthog/lemon-ui' import { useValues } from 'kea' import { compactNumber } from 'lib/utils' @@ -192,6 +192,9 @@ export const BillingProductPricingTable = ({ .

)} + + Tier breakdowns are updated once daily and may differ from the gauge above. + ) : ( a.occurrences - b.occurrences, + }, + { + title: 'Sessions', + dataIndex: 'uniqueSessions', + sorter: (a, b) => a.uniqueSessions - b.uniqueSessions, + }, + ]} + loading={errorGroupsLoading} + dataSource={errorGroups} + expandable={{ + expandedRowRender: function renderExpand(group: ErrorTrackingGroup) { + return + }, + noIndent: true, + }} + /> + ) +} diff --git a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts new file mode 100644 index 0000000000000..8cca4639c94d0 --- /dev/null +++ b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts @@ -0,0 +1,47 @@ +import { afterMount, kea, path } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' + +import { HogQLQuery, NodeKind } from '~/queries/schema' +import { hogql } from '~/queries/utils' +import { ErrorTrackingGroup } from '~/types' + +import type { errorTrackingSceneLogicType } from './errorTrackingSceneLogicType' + +export const errorTrackingSceneLogic = kea([ + path(['scenes', 'error-tracking', 'errorTrackingSceneLogic']), + + loaders(() => ({ + errorGroups: [ + [] as ErrorTrackingGroup[], + { + loadErrorGroups: async () => { + const query: HogQLQuery = { + kind: NodeKind.HogQLQuery, + query: hogql`SELECT first_value(properties), count(), count(distinct properties.$session_id) + FROM events e + WHERE event = '$exception' + -- grouping by message for now, will eventually be predefined $exception_group_id + GROUP BY properties.$exception_message`, + } + + const res = await api.query(query) + + return res.results.map((r) => { + const eventProperties = JSON.parse(r[0]) + return { + title: eventProperties['$exception_message'] || 'No message', + sampleEventProperties: eventProperties, + occurrences: r[2], + uniqueSessions: r[3], + } + }) + }, + }, + ], + })), + + afterMount(({ actions }) => { + actions.loadErrorGroups() + }), +]) diff --git a/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx b/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx index cdb72aecf8d29..d72f01f5a5852 100644 --- a/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx +++ b/frontend/src/scenes/pipeline/PipelineNodeLogs.tsx @@ -1,4 +1,5 @@ -import { LemonButton, LemonCheckbox, LemonInput, LemonTable } from '@posthog/lemon-ui' +import { IconSearch } from '@posthog/icons' +import { LemonButton, LemonCheckbox, LemonInput, LemonSnack, LemonTable } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { LOGS_PORTION_LIMIT } from 'lib/constants' import { pluralize } from 'lib/utils' @@ -9,8 +10,9 @@ import { PipelineLogLevel, pipelineNodeLogsLogic } from './pipelineNodeLogsLogic export function PipelineNodeLogs({ id, stage }: PipelineNodeLogicProps): JSX.Element { const logic = pipelineNodeLogsLogic({ id, stage }) - const { logs, logsLoading, backgroundLogs, columns, isThereMoreToLoad, selectedLogLevels } = useValues(logic) - const { revealBackground, loadMoreLogs, setSelectedLogLevels, setSearchTerm } = useActions(logic) + const { logs, logsLoading, backgroundLogs, columns, isThereMoreToLoad, selectedLogLevels, instanceId } = + useValues(logic) + const { revealBackground, loadMoreLogs, setSelectedLogLevels, setSearchTerm, setInstanceId } = useActions(logic) return (
@@ -20,6 +22,13 @@ export function PipelineNodeLogs({ id, stage }: PipelineNodeLogicProps): JSX.Ele fullWidth onChange={setSearchTerm} allowClear + prefix={ + <> + + + {instanceId && setInstanceId('')}>{instanceId}} + + } />
Show logs of level: diff --git a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx new file mode 100644 index 0000000000000..2f3cc63865729 --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputs.tsx @@ -0,0 +1,287 @@ +import { Monaco } from '@monaco-editor/react' +import { IconPencil, IconPlus, IconX } from '@posthog/icons' +import { LemonButton, LemonCheckbox, LemonInput, LemonSelect } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { CodeEditor } from 'lib/components/CodeEditors' +import { languages } from 'monaco-editor' +import { useEffect, useMemo, useState } from 'react' + +import { groupsModel } from '~/models/groupsModel' +import { HogFunctionInputSchemaType } from '~/types' + +export type HogFunctionInputProps = { + schema: HogFunctionInputSchemaType + value?: any + onChange?: (value: any) => void + disabled?: boolean +} + +const SECRET_FIELD_VALUE = '********' + +function useAutocompleteOptions(): languages.CompletionItem[] { + const { groupTypes } = useValues(groupsModel) + + return useMemo(() => { + const options = [ + ['event', 'The entire event payload as a JSON object'], + ['event.name', 'The name of the event e.g. $pageview'], + ['event.distinct_id', 'The distinct_id of the event'], + ['event.timestamp', 'The timestamp of the event'], + ['event.url', 'URL to the event in PostHog'], + ['event.properties', 'Properties of the event'], + ['event.properties.', 'The individual property of the event'], + ['person', 'The entire person payload as a JSON object'], + ['project.uuid', 'The UUID of the Person in PostHog'], + ['person.url', 'URL to the person in PostHog'], + ['person.properties', 'Properties of the person'], + ['person.properties.', 'The individual property of the person'], + ['project.id', 'ID of the project in PostHog'], + ['project.name', 'Name of the project'], + ['project.url', 'URL to the project in PostHog'], + ['source.name', 'Name of the source of this message'], + ['source.url', 'URL to the source of this message in PostHog'], + ] + + groupTypes.forEach((groupType) => { + options.push([`groups.${groupType.group_type}`, `The entire group payload as a JSON object`]) + options.push([`groups.${groupType.group_type}.id`, `The ID or 'key' of the group`]) + options.push([`groups.${groupType.group_type}.url`, `URL to the group in PostHog`]) + options.push([`groups.${groupType.group_type}.properties`, `Properties of the group`]) + options.push([`groups.${groupType.group_type}.properties.`, `The individual property of the group`]) + options.push([`groups.${groupType.group_type}.index`, `Index of the group`]) + }) + + const items: languages.CompletionItem[] = options.map(([key, value]) => { + return { + label: key, + kind: languages.CompletionItemKind.Variable, + detail: value, + insertText: key, + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: 0, + endColumn: 0, + }, + } + }) + + return items + }, [groupTypes]) +} + +function JsonConfigField(props: { + onChange?: (value: string) => void + className: string + autoFocus: boolean + value?: string | object +}): JSX.Element { + const suggestions = useAutocompleteOptions() + const [monaco, setMonaco] = useState() + + useEffect(() => { + if (!monaco) { + return + } + monaco.languages.setLanguageConfiguration('json', { + wordPattern: /[a-zA-Z0-9_\-.]+/, + }) + + const provider = monaco.languages.registerCompletionItemProvider('json', { + triggerCharacters: ['{', '{{'], + provideCompletionItems: async (model, position) => { + const word = model.getWordUntilPosition(position) + + const wordWithTrigger = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: 0, + endLineNumber: position.lineNumber, + endColumn: position.column, + }) + + if (wordWithTrigger.indexOf('{') === -1) { + return { suggestions: [] } + } + + const localSuggestions = suggestions.map((x) => ({ + ...x, + insertText: x.insertText, + range: { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }, + })) + + return { + suggestions: localSuggestions, + incomplete: false, + } + }, + }) + + return () => provider.dispose() + }, [suggestions, monaco]) + + return ( + props.onChange?.(v ?? '')} + options={{ + lineNumbers: 'off', + minimap: { + enabled: false, + }, + quickSuggestions: { + other: true, + strings: true, + }, + suggest: { + showWords: false, + showFields: false, + showKeywords: false, + }, + scrollbar: { + vertical: 'hidden', + verticalScrollbarSize: 0, + }, + }} + onMount={(_editor, monaco) => { + setMonaco(monaco) + }} + /> + ) +} + +function DictionaryField({ onChange, value }: { onChange?: (value: any) => void; value: any }): JSX.Element { + const [entries, setEntries] = useState<[string, string][]>(Object.entries(value ?? {})) + + useEffect(() => { + // NOTE: Filter out all empty entries as fetch will throw if passed in + const val = Object.fromEntries(entries.filter(([key, val]) => key.trim() !== '' || val.trim() !== '')) + onChange?.(val) + }, [entries]) + + return ( +
+ {entries.map(([key, val], index) => ( +
+ { + const newEntries = [...entries] + newEntries[index] = [key, newEntries[index][1]] + setEntries(newEntries) + }} + placeholder="Key" + /> + + { + const newEntries = [...entries] + newEntries[index] = [newEntries[index][0], val] + setEntries(newEntries) + }} + placeholder="Value" + /> + + } + size="small" + onClick={() => { + const newEntries = [...entries] + newEntries.splice(index, 1) + setEntries(newEntries) + }} + /> +
+ ))} + } + size="small" + type="secondary" + onClick={() => { + setEntries([...entries, ['', '']]) + }} + > + Add entry + +
+ ) +} + +export function HogFunctionInput({ value, onChange, schema, disabled }: HogFunctionInputProps): JSX.Element { + const [editingSecret, setEditingSecret] = useState(false) + if ( + schema.secret && + !editingSecret && + value && + (value === SECRET_FIELD_VALUE || value.name === SECRET_FIELD_VALUE) + ) { + return ( + } + onClick={() => { + onChange?.(schema.default || '') + setEditingSecret(true) + }} + disabled={disabled} + > + Reset secret variable + + ) + } + + switch (schema.type) { + case 'string': + return ( + + ) + case 'json': + return ( + + ) + case 'choice': + return ( + + ) + case 'dictionary': + return + + case 'boolean': + return onChange?.(checked)} disabled={disabled} /> + default: + return ( + + Unknown field type "{schema.type}". +
+ You may need to upgrade PostHog! +
+ ) + } +} diff --git a/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputsEditor.tsx b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputsEditor.tsx new file mode 100644 index 0000000000000..59bc0fbfffaa1 --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/HogFunctionInputsEditor.tsx @@ -0,0 +1,116 @@ +import { IconPlus, IconX } from '@posthog/icons' +import { LemonButton, LemonCheckbox, LemonInput, LemonInputSelect, LemonSelect } from '@posthog/lemon-ui' +import { capitalizeFirstLetter } from 'kea-forms' +import { useEffect, useState } from 'react' + +import { HogFunctionInputSchemaType } from '~/types' + +const typeList = ['string', 'boolean', 'dictionary', 'choice', 'json'] as const + +export type HogFunctionInputsEditorProps = { + value?: HogFunctionInputSchemaType[] + onChange?: (value: HogFunctionInputSchemaType[]) => void +} + +export function HogFunctionInputsEditor({ value, onChange }: HogFunctionInputsEditorProps): JSX.Element { + const [inputs, setInputs] = useState(value ?? []) + + useEffect(() => { + onChange?.(inputs) + }, [inputs]) + + return ( +
+ {inputs.map((input, index) => { + const _onChange = (data: Partial): void => { + setInputs((inputs) => { + const newInputs = [...inputs] + newInputs[index] = { ...newInputs[index], ...data } + return newInputs + }) + } + + return ( +
+
+ _onChange({ key })} + placeholder="Variable name" + /> + ({ + label: capitalizeFirstLetter(type), + value: type, + }))} + value={input.type} + className="w-30" + onChange={(type) => _onChange({ type })} + /> + + _onChange({ label })} + placeholder="Display label" + /> + _onChange({ required })} + label="Required" + bordered + /> + _onChange({ secret })} + label="Secret" + bordered + /> + {input.type === 'choice' && ( + choice.value)} + onChange={(choices) => + _onChange({ choices: choices.map((value) => ({ label: value, value })) }) + } + placeholder="Choices" + /> + )} +
+ } + size="small" + onClick={() => { + const newInputs = [...inputs] + newInputs.splice(index, 1) + setInputs(newInputs) + }} + /> +
+ ) + })} + +
+ } + size="small" + type="secondary" + onClick={() => { + setInputs([ + ...inputs, + { type: 'string', key: `input_${inputs.length + 1}`, label: '', required: false }, + ]) + }} + > + Add input variable + +
+
+ ) +} diff --git a/frontend/src/scenes/pipeline/hogfunctions/PipelineHogFunctionConfiguration.tsx b/frontend/src/scenes/pipeline/hogfunctions/PipelineHogFunctionConfiguration.tsx new file mode 100644 index 0000000000000..caeda41d63eef --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/PipelineHogFunctionConfiguration.tsx @@ -0,0 +1,247 @@ +import { LemonButton, LemonInput, LemonSwitch, LemonTextArea, SpinnerOverlay } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { Form } from 'kea-forms' +import { NotFound } from 'lib/components/NotFound' +import { PageHeader } from 'lib/components/PageHeader' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import { TestAccountFilterSwitch } from 'lib/components/TestAccountFiltersSwitch' +import { useFeatureFlag } from 'lib/hooks/useFeatureFlag' +import { LemonField } from 'lib/lemon-ui/LemonField' +import { HogQueryEditor } from 'scenes/debug/HogDebug' +import { ActionFilter } from 'scenes/insights/filters/ActionFilter/ActionFilter' +import { MathAvailability } from 'scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow' + +import { groupsModel } from '~/models/groupsModel' +import { NodeKind } from '~/queries/schema' +import { EntityTypes } from '~/types' + +import { HogFunctionInput } from './HogFunctionInputs' +import { HogFunctionInputsEditor } from './HogFunctionInputsEditor' +import { pipelineHogFunctionConfigurationLogic } from './pipelineHogFunctionConfigurationLogic' + +export function PipelineHogFunctionConfiguration({ + templateId, + id, +}: { + templateId?: string + id?: string +}): JSX.Element { + const logicProps = { templateId, id } + const logic = pipelineHogFunctionConfigurationLogic(logicProps) + const { isConfigurationSubmitting, configurationChanged, showSource, configuration, loading, loaded } = + useValues(logic) + const { submitConfiguration, resetForm, setShowSource } = useActions(logic) + + const hogFunctionsEnabled = !!useFeatureFlag('HOG_FUNCTIONS') + const { groupsTaxonomicTypes } = useValues(groupsModel) + + if (loading && !loaded) { + return + } + + if (!loaded) { + return + } + + if (!hogFunctionsEnabled && !id) { + return ( +
+
+

Feature not enabled

+

Hog functions are not enabled for you yet. If you think they should be, contact support.

+
+
+ ) + } + const buttons = ( + <> + resetForm()} + disabledReason={ + !configurationChanged ? 'No changes' : isConfigurationSubmitting ? 'Saving in progress…' : undefined + } + > + Clear changes + + + {templateId ? 'Create' : 'Save'} + + + ) + + return ( +
+ +
+
+
+
+
+ 🦔 +
+ Hog Function +
+ + + {({ value, onChange }) => ( + onChange(!value)} + checked={value} + disabled={loading} + bordered + /> + )} + +
+ + + + + + +
+ +
+ + {({ value, onChange }) => ( + <> + onChange({ ...value, filter_test_accounts: val })} + fullWidth + /> + { + onChange({ + ...payload, + filter_test_accounts: value?.filter_test_accounts, + }) + }} + typeKey="plugin-filters" + mathAvailability={MathAvailability.None} + hideRename + hideDuplicate + showNestedArrow={false} + actionsTaxonomicGroupTypes={[ + TaxonomicFilterGroupType.Events, + TaxonomicFilterGroupType.Actions, + ]} + propertiesTaxonomicGroupTypes={[ + TaxonomicFilterGroupType.EventProperties, + TaxonomicFilterGroupType.EventFeatureFlags, + TaxonomicFilterGroupType.Elements, + TaxonomicFilterGroupType.PersonProperties, + TaxonomicFilterGroupType.HogQLExpression, + ...groupsTaxonomicTypes, + ]} + propertyFiltersPopover + addFilterDefaultOptions={{ + id: '$pageview', + name: '$pageview', + type: EntityTypes.EVENTS, + }} + buttonCopy="Add event filter" + /> + + )} + + +

+ This destination will be triggered if any of the above filters match. +

+
+
+ +
+
+
+

Function configuration

+ + setShowSource(!showSource)}> + {showSource ? 'Hide source code' : 'Show source code'} + +
+ + {showSource ? ( +
+ + + + + + {({ value, onChange }) => ( + // TODO: Fix this so we don't have to click "update and run" + { + onChange(q.code) + }} + /> + )} + +
+ ) : ( +
+ {configuration?.inputs_schema?.length ? ( + configuration?.inputs_schema.map((schema) => { + return ( +
+ + {({ value, onChange }) => { + return ( + onChange({ value: val })} + /> + ) + }} + +
+ ) + }) + ) : ( + + This function does not require any input variables. + + )} +
+ )} +
+
{buttons}
+
+
+
+
+ ) +} diff --git a/frontend/src/scenes/pipeline/hogfunctions/pipelineHogFunctionConfigurationLogic.tsx b/frontend/src/scenes/pipeline/hogfunctions/pipelineHogFunctionConfigurationLogic.tsx new file mode 100644 index 0000000000000..8c90cb6e93334 --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/pipelineHogFunctionConfigurationLogic.tsx @@ -0,0 +1,250 @@ +import { actions, afterMount, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' +import { router } from 'kea-router' +import { subscriptions } from 'kea-subscriptions' +import api from 'lib/api' +import { urls } from 'scenes/urls' + +import { + FilterType, + HogFunctionTemplateType, + HogFunctionType, + PipelineNodeTab, + PipelineStage, + PluginConfigFilters, + PluginConfigTypeNew, +} from '~/types' + +import type { pipelineHogFunctionConfigurationLogicType } from './pipelineHogFunctionConfigurationLogicType' +import { HOG_FUNCTION_TEMPLATES } from './templates/hog-templates' + +export interface PipelineHogFunctionConfigurationLogicProps { + templateId?: string + id?: string +} + +function sanitizeFilters(filters?: FilterType): PluginConfigTypeNew['filters'] { + if (!filters) { + return null + } + const sanitized: PluginConfigFilters = {} + + if (filters.events) { + sanitized.events = filters.events.map((f) => ({ + id: f.id, + type: 'events', + name: f.name, + order: f.order, + properties: f.properties, + })) + } + + if (filters.actions) { + sanitized.actions = filters.actions.map((f) => ({ + id: f.id, + type: 'actions', + name: f.name, + order: f.order, + properties: f.properties, + })) + } + + if (filters.filter_test_accounts) { + sanitized.filter_test_accounts = filters.filter_test_accounts + } + + return Object.keys(sanitized).length > 0 ? sanitized : undefined +} + +// Should likely be somewhat similar to pipelineBatchExportConfigurationLogic +export const pipelineHogFunctionConfigurationLogic = kea([ + props({} as PipelineHogFunctionConfigurationLogicProps), + key(({ id, templateId }: PipelineHogFunctionConfigurationLogicProps) => { + return id ?? templateId ?? 'new' + }), + path((id) => ['scenes', 'pipeline', 'pipelineHogFunctionConfigurationLogic', id]), + actions({ + setShowSource: (showSource: boolean) => ({ showSource }), + resetForm: true, + }), + reducers({ + showSource: [ + false, + { + setShowSource: (_, { showSource }) => showSource, + }, + ], + }), + loaders(({ props }) => ({ + template: [ + null as HogFunctionTemplateType | null, + { + loadTemplate: async () => { + if (!props.templateId) { + return null + } + const res = HOG_FUNCTION_TEMPLATES.find((template) => template.id === props.templateId) + + if (!res) { + throw new Error('Template not found') + } + return res + }, + }, + ], + + hogFunction: [ + null as HogFunctionType | null, + { + loadHogFunction: async () => { + if (!props.id) { + return null + } + + return await api.hogFunctions.get(props.id) + }, + }, + ], + })), + forms(({ values, props, actions }) => ({ + configuration: { + defaults: {} as HogFunctionType, + alwaysShowErrors: true, + errors: (data) => { + return { + name: !data.name ? 'Name is required' : null, + ...values.inputFormErrors, + } + }, + submit: async (data) => { + const sanitizedInputs = {} + + data.inputs_schema?.forEach((input) => { + if (input.type === 'json' && typeof data.inputs[input.key].value === 'string') { + try { + sanitizedInputs[input.key] = { + value: JSON.parse(data.inputs[input.key].value), + } + } catch (e) { + // Ignore + } + } else { + sanitizedInputs[input.key] = { + value: data.inputs[input.key].value, + } + } + }) + + const payload = { + ...data, + filters: data.filters ? sanitizeFilters(data.filters) : null, + inputs: sanitizedInputs, + } + + try { + if (!props.id) { + return await api.hogFunctions.create(payload) + } + return await api.hogFunctions.update(props.id, payload) + } catch (e) { + const maybeValidationError = (e as any).data + if (maybeValidationError?.type === 'validation_error') { + if (maybeValidationError.attr.includes('inputs__')) { + actions.setConfigurationManualErrors({ + inputs: { + [maybeValidationError.attr.split('__')[1]]: maybeValidationError.detail, + }, + }) + } else { + actions.setConfigurationManualErrors({ + [maybeValidationError.attr]: maybeValidationError.detail, + }) + } + } + throw e + } + }, + }, + })), + selectors(() => ({ + loading: [ + (s) => [s.hogFunctionLoading, s.templateLoading], + (hogFunctionLoading, templateLoading) => hogFunctionLoading || templateLoading, + ], + loaded: [(s) => [s.hogFunction, s.template], (hogFunction, template) => !!hogFunction || !!template], + + inputFormErrors: [ + (s) => [s.configuration], + (configuration) => { + const inputs = configuration.inputs ?? {} + const inputErrors = {} + + configuration.inputs_schema?.forEach((input) => { + if (input.required && !inputs[input.key]) { + inputErrors[input.key] = 'This field is required' + } + + if (input.type === 'json' && typeof inputs[input.key] === 'string') { + try { + JSON.parse(inputs[input.key].value) + } catch (e) { + inputErrors[input.key] = 'Invalid JSON' + } + } + }) + + return Object.keys(inputErrors).length > 0 + ? { + inputs: inputErrors, + } + : null + }, + ], + })), + + listeners(({ actions, values, cache, props }) => ({ + loadTemplateSuccess: () => actions.resetForm(), + loadHogFunctionSuccess: () => actions.resetForm(), + resetForm: () => { + const savedValue = values.hogFunction ?? values.template + actions.resetConfiguration({ + ...savedValue, + inputs: (savedValue as any)?.inputs ?? {}, + ...(cache.configFromUrl || {}), + }) + }, + + submitConfigurationSuccess: ({ configuration }) => { + if (!props.id) { + router.actions.replace( + urls.pipelineNode( + PipelineStage.Destination, + `hog-${configuration.id}`, + PipelineNodeTab.Configuration + ) + ) + } + }, + })), + afterMount(({ props, actions, cache }) => { + if (props.templateId) { + cache.configFromUrl = router.values.hashParams.configuration + actions.loadTemplate() // comes with plugin info + } else if (props.id) { + actions.loadHogFunction() + } + }), + + subscriptions(({ props, cache }) => ({ + configuration: (configuration) => { + if (props.templateId) { + // Sync state to the URL bar if new + cache.ignoreUrlChange = true + router.actions.replace(router.values.location.pathname, undefined, { + configuration, + }) + } + }, + })), +]) diff --git a/frontend/src/scenes/pipeline/hogfunctions/templates/hog-templates.tsx b/frontend/src/scenes/pipeline/hogfunctions/templates/hog-templates.tsx new file mode 100644 index 0000000000000..294159998ab2b --- /dev/null +++ b/frontend/src/scenes/pipeline/hogfunctions/templates/hog-templates.tsx @@ -0,0 +1,58 @@ +import { HogFunctionTemplateType } from '~/types' + +export const HOG_FUNCTION_TEMPLATES: HogFunctionTemplateType[] = [ + { + id: 'template-webhook', + name: 'HogHook', + description: 'Sends a webhook templated by the incoming event data', + hog: "fetch(inputs.url, {\n 'headers': inputs.headers,\n 'body': inputs.payload,\n 'method': inputs.method\n});", + inputs_schema: [ + { + key: 'url', + type: 'string', + label: 'Webhook URL', + secret: false, + required: true, + }, + { + key: 'method', + type: 'choice', + label: 'Method', + secret: false, + choices: [ + { + label: 'POST', + value: 'POST', + }, + { + label: 'PUT', + value: 'PUT', + }, + { + label: 'GET', + value: 'GET', + }, + { + label: 'DELETE', + value: 'DELETE', + }, + ], + required: false, + }, + { + key: 'payload', + type: 'json', + label: 'JSON Payload', + secret: false, + required: false, + }, + { + key: 'headers', + type: 'dictionary', + label: 'Headers', + secret: false, + required: false, + }, + ], + }, +] diff --git a/frontend/src/scenes/pipeline/pipelineNodeLogsLogic.tsx b/frontend/src/scenes/pipeline/pipelineNodeLogsLogic.tsx index 053ba517c17d3..2488b6777d991 100644 --- a/frontend/src/scenes/pipeline/pipelineNodeLogsLogic.tsx +++ b/frontend/src/scenes/pipeline/pipelineNodeLogsLogic.tsx @@ -1,4 +1,5 @@ -import { LemonTableColumns } from '@posthog/lemon-ui' +import { TZLabel } from '@posthog/apps-common' +import { LemonTableColumns, Link } from '@posthog/lemon-ui' import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import { LOGS_PORTION_LIMIT } from 'lib/constants' @@ -28,13 +29,14 @@ export const pipelineNodeLogsLogic = kea([ key(({ id }) => id), path((key) => ['scenes', 'pipeline', 'pipelineNodeLogsLogic', key]), connect((props: PipelineNodeLogicProps) => ({ - values: [teamLogic(), ['currentTeamId'], pipelineNodeLogic(props), ['nodeBackend']], + values: [teamLogic(), ['currentTeamId'], pipelineNodeLogic(props), ['node']], })), actions({ setSelectedLogLevels: (levels: PipelineLogLevel[]) => ({ levels, }), setSearchTerm: (searchTerm: string) => ({ searchTerm }), + setInstanceId: (instanceId: string) => ({ instanceId }), clearBackgroundLogs: true, markLogsEnd: true, }), @@ -44,15 +46,24 @@ export const pipelineNodeLogsLogic = kea([ { loadLogs: async () => { let results: LogEntry[] - if (values.nodeBackend === PipelineBackend.BatchExport) { + if (values.node.backend === PipelineBackend.BatchExport) { results = await api.batchExportLogs.search( - id as string, + values.node.id, values.searchTerm, values.selectedLogLevels ) + } else if (values.node.backend === PipelineBackend.HogFunction) { + const res = await api.hogFunctions.searchLogs(values.node.id, { + search: values.searchTerm, + levels: values.selectedLogLevels, + limit: LOGS_PORTION_LIMIT, + instance_id: values.instanceId, + }) + + results = res.results } else { results = await api.pluginLogs.search( - id as number, + values.node.id, values.searchTerm, logLevelsToTypeFilters(values.selectedLogLevels) ) @@ -66,13 +77,23 @@ export const pipelineNodeLogsLogic = kea([ }, loadMoreLogs: async () => { let results: LogEntry[] - if (values.nodeBackend === PipelineBackend.BatchExport) { + if (values.node.backend === PipelineBackend.BatchExport) { results = await api.batchExportLogs.search( id as string, values.searchTerm, values.selectedLogLevels, values.trailingEntry as BatchExportLogEntry | null ) + } else if (values.node.backend === PipelineBackend.HogFunction) { + const res = await api.hogFunctions.searchLogs(values.node.id, { + search: values.searchTerm, + levels: values.selectedLogLevels, + limit: LOGS_PORTION_LIMIT, + before: values.trailingEntry?.timestamp, + instance_id: values.instanceId, + }) + + results = res.results } else { results = await api.pluginLogs.search( id as number, @@ -105,7 +126,7 @@ export const pipelineNodeLogsLogic = kea([ } let results: LogEntry[] - if (values.nodeBackend === PipelineBackend.BatchExport) { + if (values.node.backend === PipelineBackend.BatchExport) { results = await api.batchExportLogs.search( id as string, values.searchTerm, @@ -113,6 +134,16 @@ export const pipelineNodeLogsLogic = kea([ null, values.leadingEntry as BatchExportLogEntry | null ) + } else if (values.node.backend === PipelineBackend.HogFunction) { + const res = await api.hogFunctions.searchLogs(values.node.id, { + search: values.searchTerm, + levels: values.selectedLogLevels, + limit: LOGS_PORTION_LIMIT, + after: values.leadingEntry?.timestamp, + instance_id: values.instanceId, + }) + + results = res.results } else { results = await api.pluginLogs.search( id as number, @@ -147,6 +178,12 @@ export const pipelineNodeLogsLogic = kea([ setSearchTerm: (_, { searchTerm }) => searchTerm, }, ], + instanceId: [ + '', + { + setInstanceId: (_, { instanceId }) => instanceId, + }, + ], isThereMoreToLoad: [ true, { @@ -155,7 +192,7 @@ export const pipelineNodeLogsLogic = kea([ }, ], }), - selectors({ + selectors(({ actions }) => ({ leadingEntry: [ (s) => [s.logs, s.backgroundLogs], (logs: LogEntry[], backgroundLogs: LogEntry[]): LogEntry | null => { @@ -181,26 +218,76 @@ export const pipelineNodeLogsLogic = kea([ }, ], columns: [ - (s) => [s.nodeBackend], - (nodeBackend): LemonTableColumns => { + (s) => [s.node], + (node): LemonTableColumns => { return [ { title: 'Timestamp', key: 'timestamp', dataIndex: 'timestamp', sorter: (a: LogEntry, b: LogEntry) => dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(), - render: (timestamp: string) => dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss.SSS UTC'), + render: (timestamp: string) => , + width: 0, }, { - title: nodeBackend === PipelineBackend.BatchExport ? 'Run Id' : 'Source', - dataIndex: nodeBackend === PipelineBackend.BatchExport ? 'run_id' : 'source', - key: nodeBackend === PipelineBackend.BatchExport ? 'run_id' : 'source', + width: 0, + title: + node.backend == PipelineBackend.HogFunction + ? 'Invocation' + : node.backend == PipelineBackend.BatchExport + ? 'Run Id' + : 'Source', + dataIndex: + node.backend == PipelineBackend.HogFunction + ? 'instance_id' + : node.backend == PipelineBackend.BatchExport + ? 'run_id' + : 'source', + key: + node.backend == PipelineBackend.HogFunction + ? 'instance_id' + : node.backend == PipelineBackend.BatchExport + ? 'run_id' + : 'source', + + render: (instanceId: string) => ( + + {node.backend === PipelineBackend.HogFunction ? ( + { + actions.setInstanceId(instanceId) + }} + > + {instanceId} + + ) : ( + instanceId + )} + + ), }, { + width: 100, title: 'Level', - key: nodeBackend === PipelineBackend.BatchExport ? 'level' : 'type', - dataIndex: nodeBackend === PipelineBackend.BatchExport ? 'level' : 'type', - render: nodeBackend === PipelineBackend.BatchExport ? LogLevelDisplay : LogTypeDisplay, + key: + node.backend == PipelineBackend.HogFunction + ? 'level' + : node.backend == PipelineBackend.BatchExport + ? 'level' + : 'type', + dataIndex: + node.backend == PipelineBackend.HogFunction + ? 'level' + : node.backend == PipelineBackend.BatchExport + ? 'level' + : 'type', + render: + node.backend == PipelineBackend.HogFunction + ? LogLevelDisplay + : node.backend == PipelineBackend.BatchExport + ? LogLevelDisplay + : LogTypeDisplay, }, { title: 'Message', @@ -211,7 +298,7 @@ export const pipelineNodeLogsLogic = kea([ ] as LemonTableColumns }, ], - }), + })), listeners(({ actions }) => ({ setSelectedLogLevels: () => { actions.loadLogs() @@ -222,6 +309,9 @@ export const pipelineNodeLogsLogic = kea([ } actions.loadLogs() }, + setInstanceId: async () => { + actions.loadLogs() + }, })), events(({ actions, cache }) => ({ afterMount: () => { diff --git a/frontend/src/scenes/session-recordings/filters/RecordingsUniversalFilters.tsx b/frontend/src/scenes/session-recordings/filters/RecordingsUniversalFilters.tsx new file mode 100644 index 0000000000000..2625549c1b53d --- /dev/null +++ b/frontend/src/scenes/session-recordings/filters/RecordingsUniversalFilters.tsx @@ -0,0 +1,138 @@ +import { useActions, useMountedLogic, useValues } from 'kea' +import { DateFilter } from 'lib/components/DateFilter/DateFilter' +import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' +import UniversalFilters from 'lib/components/UniversalFilters/UniversalFilters' +import { universalFiltersLogic } from 'lib/components/UniversalFilters/universalFiltersLogic' +import { isUniversalGroupFilterLike } from 'lib/components/UniversalFilters/utils' +import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' + +import { actionsModel } from '~/models/actionsModel' +import { cohortsModel } from '~/models/cohortsModel' +import { AndOrFilterSelect } from '~/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect' + +import { sessionRecordingsPlaylistLogic } from '../playlist/sessionRecordingsPlaylistLogic' +import { DurationFilter } from './DurationFilter' + +export const RecordingsUniversalFilters = (): JSX.Element => { + useMountedLogic(cohortsModel) + useMountedLogic(actionsModel) + const { universalFilters } = useValues(sessionRecordingsPlaylistLogic) + const { setUniversalFilters } = useActions(sessionRecordingsPlaylistLogic) + + const durationFilter = universalFilters.duration[0] + + return ( +
+
+
+ { + setUniversalFilters({ + ...universalFilters, + date_from: changedDateFrom, + date_to: changedDateTo, + }) + }} + dateOptions={[ + { key: 'Custom', values: [] }, + { key: 'Last 24 hours', values: ['-24h'] }, + { key: 'Last 3 days', values: ['-3d'] }, + { key: 'Last 7 days', values: ['-7d'] }, + { key: 'Last 30 days', values: ['-30d'] }, + { key: 'All time', values: ['-90d'] }, + ]} + dropdownPlacement="bottom-start" + size="small" + /> + { + setUniversalFilters({ + duration: [{ ...newRecordingDurationFilter, key: newDurationType }], + }) + }} + recordingDurationFilter={durationFilter} + durationTypeFilter={durationFilter.key} + pageKey="session-recordings" + /> + + setUniversalFilters({ + ...universalFilters, + filter_test_accounts: testFilters.filter_test_accounts, + }) + } + /> +
+
+ { + setUniversalFilters({ + ...universalFilters, + filter_group: { + type: type, + values: universalFilters.filter_group.values, + }, + }) + }} + disabledReason="'Or' filtering is not supported yet" + topLevelFilter={true} + suffix={['filter', 'filters']} + /> +
+
+
+ { + setUniversalFilters({ + ...universalFilters, + filter_group: filterGroup, + }) + }} + > + + +
+
+ ) +} + +const RecordingsUniversalFilterGroup = (): JSX.Element => { + const { filterGroup } = useValues(universalFiltersLogic) + const { replaceGroupValue, removeGroupValue } = useActions(universalFiltersLogic) + + return ( + <> + {filterGroup.values.map((filterOrGroup, index) => { + return isUniversalGroupFilterLike(filterOrGroup) ? ( + + + + + ) : ( + removeGroupValue(index)} + onChange={(value) => replaceGroupValue(index, value)} + /> + ) + })} + + ) +} diff --git a/frontend/src/scenes/session-recordings/filters/ReplayTaxonomicFilters.tsx b/frontend/src/scenes/session-recordings/filters/ReplayTaxonomicFilters.tsx new file mode 100644 index 0000000000000..345f66b1c90b6 --- /dev/null +++ b/frontend/src/scenes/session-recordings/filters/ReplayTaxonomicFilters.tsx @@ -0,0 +1,109 @@ +import { IconTrash } from '@posthog/icons' +import { LemonButton, Popover } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' +import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter' +import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' +import { universalFiltersLogic } from 'lib/components/UniversalFilters/universalFiltersLogic' +import { useState } from 'react' + +import { PropertyFilterType } from '~/types' + +import { playerSettingsLogic } from '../player/playerSettingsLogic' + +export interface ReplayTaxonomicFiltersProps { + onChange: (value: TaxonomicFilterValue, item?: any) => void +} + +export function ReplayTaxonomicFilters({ onChange }: ReplayTaxonomicFiltersProps): JSX.Element { + const { + filterGroup: { values: filters }, + } = useValues(universalFiltersLogic) + + const hasConsoleLogLevelFilter = filters.find( + (f) => f.type === PropertyFilterType.Recording && f.key === 'console_log_level' + ) + const hasConsoleLogQueryFilter = filters.find( + (f) => f.type === PropertyFilterType.Recording && f.key === 'console_log_query' + ) + + return ( +
+
+
Session properties
+
    + onChange('console_log_level', {})} + disabledReason={hasConsoleLogLevelFilter ? 'Log level filter already added' : undefined} + > + Console log level + + onChange('console_log_query', {})} + disabledReason={hasConsoleLogQueryFilter ? 'Log text filter already added' : undefined} + > + Console log text + +
+
+ + +
+ ) +} + +const PersonProperties = ({ onChange }: { onChange: ReplayTaxonomicFiltersProps['onChange'] }): JSX.Element => { + const { quickFilterProperties: properties } = useValues(playerSettingsLogic) + const { setQuickFilterProperties } = useActions(playerSettingsLogic) + + const [showPropertySelector, setShowPropertySelector] = useState(false) + + return ( +
+
Person properties
+
    + {properties.map((property) => ( + { + const newProperties = properties.filter((p) => p != property) + setQuickFilterProperties(newProperties) + }, + icon: , + }} + onClick={() => onChange(property, { propertyFilterType: PropertyFilterType.Person })} + > + + + ))} + setShowPropertySelector(false)} + placement="right-start" + overlay={ + { + properties.push(value as string) + setQuickFilterProperties([...properties]) + setShowPropertySelector(false) + }} + taxonomicGroupTypes={[TaxonomicFilterGroupType.PersonProperties]} + excludedProperties={{ [TaxonomicFilterGroupType.PersonProperties]: properties }} + /> + } + > + setShowPropertySelector(!showPropertySelector)} fullWidth> + Add property + + +
+
+ ) +} diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index 62a94eaed4342..ee7c6c3b363b5 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -126,8 +126,9 @@ export const SettingsMap: SettingSection[] = [ }, { id: 'persons-on-events', - title: 'Event person filtering behavior', + title: 'Person properties mode', component: , + flag: '!SETTINGS_PERSONS_ON_EVENTS_HIDDEN', // Setting hidden for Cloud orgs created since June 2024 }, { id: 'correlation-analysis', diff --git a/frontend/src/scenes/settings/settingsLogic.ts b/frontend/src/scenes/settings/settingsLogic.ts index 248fd4e627425..f00d8e523aab5 100644 --- a/frontend/src/scenes/settings/settingsLogic.ts +++ b/frontend/src/scenes/settings/settingsLogic.ts @@ -62,7 +62,14 @@ export const settingsLogic = kea([ sections: [ (s) => [s.featureFlags], (featureFlags): SettingSection[] => { - return SettingsMap.filter((x) => (x.flag ? featureFlags[FEATURE_FLAGS[x.flag]] : true)) + return SettingsMap.filter((x) => { + const isFlagConditionMet = !x.flag + ? true // No flag condition + : x.flag.startsWith('!') + ? !featureFlags[FEATURE_FLAGS[x.flag.slice(1)]] // Negated flag condition (!-prefixed) + : featureFlags[FEATURE_FLAGS[x.flag]] // Regular flag condition + return isFlagConditionMet + }) }, ], selectedSection: [ @@ -96,14 +103,17 @@ export const settingsLogic = kea([ } return settings.filter((x) => { + const isFlagConditionMet = !x.flag + ? true // No flag condition + : x.flag.startsWith('!') + ? !featureFlags[FEATURE_FLAGS[x.flag.slice(1)]] // Negated flag condition (!-prefixed) + : featureFlags[FEATURE_FLAGS[x.flag]] // Regular flag condition if (x.flag && x.features) { - return ( - x.features.some((feat) => hasAvailableFeature(feat)) || featureFlags[FEATURE_FLAGS[x.flag]] - ) + return x.features.some((feat) => hasAvailableFeature(feat)) || isFlagConditionMet } else if (x.features) { return x.features.some((feat) => hasAvailableFeature(feat)) } else if (x.flag) { - return featureFlags[FEATURE_FLAGS[x.flag]] + return isFlagConditionMet } return true diff --git a/frontend/src/scenes/settings/types.ts b/frontend/src/scenes/settings/types.ts index bc44d6808f276..d63d797ef536a 100644 --- a/frontend/src/scenes/settings/types.ts +++ b/frontend/src/scenes/settings/types.ts @@ -83,12 +83,18 @@ export type SettingId = | 'persons-join-mode' | 'bounce-rate-page-view-mode' +type FeatureFlagKey = keyof typeof FEATURE_FLAGS + export type Setting = { id: SettingId title: string description?: JSX.Element | string component: JSX.Element - flag?: keyof typeof FEATURE_FLAGS + /** + * Feature flag to gate the setting being shown. + * If prefixed with !, the condition is inverted - the setting will only be shown if the is flag false. + */ + flag?: FeatureFlagKey | `!${FeatureFlagKey}` features?: AvailableFeature[] } @@ -97,6 +103,10 @@ export type SettingSection = { title: string level: SettingLevelId settings: Setting[] - flag?: keyof typeof FEATURE_FLAGS + /** + * Feature flag to gate the section being shown. + * If prefixed with !, the condition is inverted - the section will only be shown if the is flag false. + */ + flag?: FeatureFlagKey | `!${FeatureFlagKey}` minimumAccessLevel?: EitherMembershipLevel } diff --git a/frontend/src/scenes/surveys/QuestionBranchingInput.tsx b/frontend/src/scenes/surveys/QuestionBranchingInput.tsx new file mode 100644 index 0000000000000..96c6ea55912d6 --- /dev/null +++ b/frontend/src/scenes/surveys/QuestionBranchingInput.tsx @@ -0,0 +1,68 @@ +import './EditSurvey.scss' + +import { LemonSelect } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { LemonField } from 'lib/lemon-ui/LemonField' + +import { MultipleSurveyQuestion, RatingSurveyQuestion, SurveyQuestionBranchingType } from '~/types' + +import { surveyLogic } from './surveyLogic' + +export function QuestionBranchingInput({ + questionIndex, + question, +}: { + questionIndex: number + question: RatingSurveyQuestion | MultipleSurveyQuestion +}): JSX.Element { + const { survey, getBranchingDropdownValue } = useValues(surveyLogic) + const { setQuestionBranching } = useActions(surveyLogic) + + const availableNextQuestions = survey.questions + .map((question, questionIndex) => ({ + ...question, + questionIndex, + })) + .filter((_, idx) => questionIndex !== idx) + const branchingDropdownValue = getBranchingDropdownValue(questionIndex, question) + + return ( + <> + + setQuestionBranching(questionIndex, value)} + options={[ + ...(questionIndex < survey.questions.length - 1 + ? [ + { + label: 'Next question', + value: SurveyQuestionBranchingType.NextQuestion, + }, + ] + : []), + { + label: 'Confirmation message', + value: SurveyQuestionBranchingType.ConfirmationMessage, + }, + { + label: 'Specific question based on answer', + value: SurveyQuestionBranchingType.ResponseBased, + }, + ...availableNextQuestions.map((question) => ({ + label: `${question.questionIndex + 1}. ${question.question}`, + value: `${SurveyQuestionBranchingType.SpecificQuestion}:${question.questionIndex}`, + })), + ]} + /> + + {branchingDropdownValue === SurveyQuestionBranchingType.ResponseBased && ( +
+ TODO: dropdowns for the response-based branching +
+ )} + + ) +} diff --git a/hogql_parser/HogQLParser.cpp b/hogql_parser/HogQLParser.cpp index 112fd7cb48ae0..3a4cc87f5cbd9 100644 --- a/hogql_parser/HogQLParser.cpp +++ b/hogql_parser/HogQLParser.cpp @@ -52,25 +52,24 @@ void hogqlparserParserInitialize() { #endif auto staticData = std::make_unique( std::vector{ - "program", "declaration", "expression", "varDecl", "varAssignment", - "identifierList", "statement", "exprStmt", "ifStmt", "whileStmt", - "returnStmt", "funcStmt", "emptyStmt", "block", "kvPair", "kvPairList", - "select", "selectUnionStmt", "selectStmtWithParens", "selectStmt", - "withClause", "topClause", "fromClause", "arrayJoinClause", "windowClause", - "prewhereClause", "whereClause", "groupByClause", "havingClause", - "orderByClause", "projectionOrderByClause", "limitAndOffsetClause", - "offsetOnlyClause", "settingsClause", "joinExpr", "joinOp", "joinOpCross", - "joinConstraintClause", "sampleClause", "orderExprList", "orderExpr", - "ratioExpr", "settingExprList", "settingExpr", "windowExpr", "winPartitionByClause", - "winOrderByClause", "winFrameClause", "winFrameExtend", "winFrameBound", - "expr", "columnTypeExpr", "columnExprList", "columnExpr", "columnArgList", - "columnArgExpr", "columnLambdaExpr", "hogqlxTagElement", "hogqlxTagAttribute", - "withExprList", "withExpr", "columnIdentifier", "nestedIdentifier", - "tableExpr", "tableFunctionExpr", "tableIdentifier", "tableArgList", - "databaseIdentifier", "floatingLiteral", "numberLiteral", "literal", - "interval", "keyword", "keywordForAlias", "alias", "identifier", "enumValue", - "placeholder", "string", "templateString", "stringContents", "fullTemplateString", - "stringContentsFull" + "program", "declaration", "expression", "varDecl", "identifierList", + "statement", "returnStmt", "ifStmt", "whileStmt", "funcStmt", "varAssignment", + "exprStmt", "emptyStmt", "block", "kvPair", "kvPairList", "select", + "selectUnionStmt", "selectStmtWithParens", "selectStmt", "withClause", + "topClause", "fromClause", "arrayJoinClause", "windowClause", "prewhereClause", + "whereClause", "groupByClause", "havingClause", "orderByClause", "projectionOrderByClause", + "limitAndOffsetClause", "offsetOnlyClause", "settingsClause", "joinExpr", + "joinOp", "joinOpCross", "joinConstraintClause", "sampleClause", "orderExprList", + "orderExpr", "ratioExpr", "settingExprList", "settingExpr", "windowExpr", + "winPartitionByClause", "winOrderByClause", "winFrameClause", "winFrameExtend", + "winFrameBound", "expr", "columnTypeExpr", "columnExprList", "columnExpr", + "columnArgList", "columnArgExpr", "columnLambdaExpr", "hogqlxTagElement", + "hogqlxTagAttribute", "withExprList", "withExpr", "columnIdentifier", + "nestedIdentifier", "tableExpr", "tableFunctionExpr", "tableIdentifier", + "tableArgList", "databaseIdentifier", "floatingLiteral", "numberLiteral", + "literal", "interval", "keyword", "keywordForAlias", "alias", "identifier", + "enumValue", "placeholder", "string", "templateString", "stringContents", + "fullTemplateString", "stringContentsFull" }, std::vector{ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", @@ -113,7 +112,7 @@ void hogqlparserParserInitialize() { } ); static const int32_t serializedATNSegment[] = { - 4,1,154,1178,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6, + 4,1,154,1179,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6, 2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14, 7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21, 7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28, @@ -126,87 +125,87 @@ void hogqlparserParserInitialize() { 7,70,2,71,7,71,2,72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77, 7,77,2,78,7,78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,1,0,5,0,168,8, 0,10,0,12,0,171,9,0,1,0,1,0,1,1,1,1,3,1,177,8,1,1,2,1,2,1,3,1,3,1,3,1, - 3,1,3,3,3,186,8,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,5,1,5,1,5,5,5,199, - 8,5,10,5,12,5,202,9,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,3,6,213,8,6, - 1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,3,8,225,8,8,1,9,1,9,1,9,1,9,1, - 9,1,9,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,3,11,241,8,11,1,11,1,11, - 1,11,1,12,1,12,1,13,1,13,5,13,250,8,13,10,13,12,13,253,9,13,1,13,1,13, - 1,14,1,14,1,14,1,14,1,15,1,15,1,15,5,15,264,8,15,10,15,12,15,267,9,15, - 1,16,1,16,1,16,3,16,272,8,16,1,16,1,16,1,17,1,17,1,17,1,17,5,17,280,8, - 17,10,17,12,17,283,9,17,1,18,1,18,1,18,1,18,1,18,1,18,3,18,291,8,18,1, - 19,3,19,294,8,19,1,19,1,19,3,19,298,8,19,1,19,3,19,301,8,19,1,19,1,19, - 3,19,305,8,19,1,19,3,19,308,8,19,1,19,3,19,311,8,19,1,19,3,19,314,8,19, - 1,19,3,19,317,8,19,1,19,1,19,3,19,321,8,19,1,19,1,19,3,19,325,8,19,1, - 19,3,19,328,8,19,1,19,3,19,331,8,19,1,19,3,19,334,8,19,1,19,1,19,3,19, - 338,8,19,1,19,3,19,341,8,19,1,20,1,20,1,20,1,21,1,21,1,21,1,21,3,21,350, - 8,21,1,22,1,22,1,22,1,23,3,23,356,8,23,1,23,1,23,1,23,1,23,1,24,1,24, - 1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,5,24,375,8,24, - 10,24,12,24,378,9,24,1,25,1,25,1,25,1,26,1,26,1,26,1,27,1,27,1,27,1,27, - 1,27,1,27,1,27,1,27,3,27,394,8,27,1,28,1,28,1,28,1,29,1,29,1,29,1,29, - 1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31,3,31,411,8,31,1,31,1,31,1,31, - 1,31,3,31,417,8,31,1,31,1,31,1,31,1,31,3,31,423,8,31,1,31,1,31,1,31,1, - 31,1,31,1,31,1,31,1,31,1,31,3,31,434,8,31,3,31,436,8,31,1,32,1,32,1,32, - 1,33,1,33,1,33,1,34,1,34,1,34,3,34,447,8,34,1,34,3,34,450,8,34,1,34,1, - 34,1,34,1,34,3,34,456,8,34,1,34,1,34,1,34,1,34,1,34,1,34,3,34,464,8,34, - 1,34,1,34,1,34,1,34,5,34,470,8,34,10,34,12,34,473,9,34,1,35,3,35,476, - 8,35,1,35,1,35,1,35,3,35,481,8,35,1,35,3,35,484,8,35,1,35,3,35,487,8, - 35,1,35,1,35,3,35,491,8,35,1,35,1,35,3,35,495,8,35,1,35,3,35,498,8,35, - 3,35,500,8,35,1,35,3,35,503,8,35,1,35,1,35,3,35,507,8,35,1,35,1,35,3, - 35,511,8,35,1,35,3,35,514,8,35,3,35,516,8,35,3,35,518,8,35,1,36,1,36, - 1,36,3,36,523,8,36,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,3,37, - 534,8,37,1,38,1,38,1,38,1,38,3,38,540,8,38,1,39,1,39,1,39,5,39,545,8, - 39,10,39,12,39,548,9,39,1,40,1,40,3,40,552,8,40,1,40,1,40,3,40,556,8, - 40,1,40,1,40,3,40,560,8,40,1,41,1,41,1,41,1,41,3,41,566,8,41,3,41,568, - 8,41,1,42,1,42,1,42,5,42,573,8,42,10,42,12,42,576,9,42,1,43,1,43,1,43, - 1,43,1,44,3,44,583,8,44,1,44,3,44,586,8,44,1,44,3,44,589,8,44,1,45,1, - 45,1,45,1,45,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1, - 48,1,48,3,48,608,8,48,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1, - 49,1,49,1,49,3,49,622,8,49,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1, - 51,1,51,1,51,1,51,5,51,636,8,51,10,51,12,51,639,9,51,1,51,1,51,1,51,1, - 51,1,51,1,51,1,51,5,51,648,8,51,10,51,12,51,651,9,51,1,51,1,51,1,51,1, - 51,1,51,1,51,1,51,5,51,660,8,51,10,51,12,51,663,9,51,1,51,1,51,1,51,1, - 51,1,51,3,51,670,8,51,1,51,1,51,3,51,674,8,51,1,52,1,52,1,52,5,52,679, - 8,52,10,52,12,52,682,9,52,1,53,1,53,1,53,3,53,687,8,53,1,53,1,53,1,53, - 1,53,1,53,4,53,694,8,53,11,53,12,53,695,1,53,1,53,3,53,700,8,53,1,53, + 3,1,3,3,3,186,8,3,1,4,1,4,1,4,5,4,191,8,4,10,4,12,4,194,9,4,1,5,1,5,1, + 5,1,5,1,5,1,5,1,5,1,5,3,5,204,8,5,1,6,1,6,3,6,208,8,6,1,6,3,6,211,8,6, + 1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,220,8,7,1,8,1,8,1,8,1,8,1,8,1,8,3,8,228, + 8,8,1,9,1,9,1,9,1,9,3,9,234,8,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10, + 1,11,1,11,3,11,246,8,11,1,12,1,12,1,13,1,13,5,13,252,8,13,10,13,12,13, + 255,9,13,1,13,1,13,1,14,1,14,1,14,1,14,1,15,1,15,1,15,5,15,266,8,15,10, + 15,12,15,269,9,15,1,16,1,16,1,16,3,16,274,8,16,1,16,1,16,1,17,1,17,1, + 17,1,17,5,17,282,8,17,10,17,12,17,285,9,17,1,18,1,18,1,18,1,18,1,18,1, + 18,3,18,293,8,18,1,19,3,19,296,8,19,1,19,1,19,3,19,300,8,19,1,19,3,19, + 303,8,19,1,19,1,19,3,19,307,8,19,1,19,3,19,310,8,19,1,19,3,19,313,8,19, + 1,19,3,19,316,8,19,1,19,3,19,319,8,19,1,19,1,19,3,19,323,8,19,1,19,1, + 19,3,19,327,8,19,1,19,3,19,330,8,19,1,19,3,19,333,8,19,1,19,3,19,336, + 8,19,1,19,1,19,3,19,340,8,19,1,19,3,19,343,8,19,1,20,1,20,1,20,1,21,1, + 21,1,21,1,21,3,21,352,8,21,1,22,1,22,1,22,1,23,3,23,358,8,23,1,23,1,23, + 1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24, + 1,24,5,24,377,8,24,10,24,12,24,380,9,24,1,25,1,25,1,25,1,26,1,26,1,26, + 1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,396,8,27,1,28,1,28,1,28, + 1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31,3,31,413, + 8,31,1,31,1,31,1,31,1,31,3,31,419,8,31,1,31,1,31,1,31,1,31,3,31,425,8, + 31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,3,31,436,8,31,3,31,438, + 8,31,1,32,1,32,1,32,1,33,1,33,1,33,1,34,1,34,1,34,3,34,449,8,34,1,34, + 3,34,452,8,34,1,34,1,34,1,34,1,34,3,34,458,8,34,1,34,1,34,1,34,1,34,1, + 34,1,34,3,34,466,8,34,1,34,1,34,1,34,1,34,5,34,472,8,34,10,34,12,34,475, + 9,34,1,35,3,35,478,8,35,1,35,1,35,1,35,3,35,483,8,35,1,35,3,35,486,8, + 35,1,35,3,35,489,8,35,1,35,1,35,3,35,493,8,35,1,35,1,35,3,35,497,8,35, + 1,35,3,35,500,8,35,3,35,502,8,35,1,35,3,35,505,8,35,1,35,1,35,3,35,509, + 8,35,1,35,1,35,3,35,513,8,35,1,35,3,35,516,8,35,3,35,518,8,35,3,35,520, + 8,35,1,36,1,36,1,36,3,36,525,8,36,1,37,1,37,1,37,1,37,1,37,1,37,1,37, + 1,37,1,37,3,37,536,8,37,1,38,1,38,1,38,1,38,3,38,542,8,38,1,39,1,39,1, + 39,5,39,547,8,39,10,39,12,39,550,9,39,1,40,1,40,3,40,554,8,40,1,40,1, + 40,3,40,558,8,40,1,40,1,40,3,40,562,8,40,1,41,1,41,1,41,1,41,3,41,568, + 8,41,3,41,570,8,41,1,42,1,42,1,42,5,42,575,8,42,10,42,12,42,578,9,42, + 1,43,1,43,1,43,1,43,1,44,3,44,585,8,44,1,44,3,44,588,8,44,1,44,3,44,591, + 8,44,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,48,1,48, + 1,48,1,48,1,48,1,48,3,48,610,8,48,1,49,1,49,1,49,1,49,1,49,1,49,1,49, + 1,49,1,49,1,49,1,49,1,49,3,49,624,8,49,1,50,1,50,1,50,1,51,1,51,1,51, + 1,51,1,51,1,51,1,51,1,51,1,51,5,51,638,8,51,10,51,12,51,641,9,51,1,51, + 1,51,1,51,1,51,1,51,1,51,1,51,5,51,650,8,51,10,51,12,51,653,9,51,1,51, + 1,51,1,51,1,51,1,51,1,51,1,51,5,51,662,8,51,10,51,12,51,665,9,51,1,51, + 1,51,1,51,1,51,1,51,3,51,672,8,51,1,51,1,51,3,51,676,8,51,1,52,1,52,1, + 52,5,52,681,8,52,10,52,12,52,684,9,52,1,53,1,53,1,53,3,53,689,8,53,1, + 53,1,53,1,53,1,53,1,53,4,53,696,8,53,11,53,12,53,697,1,53,1,53,3,53,702, + 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,726,8,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53, + 743,8,53,1,53,1,53,1,53,1,53,3,53,749,8,53,1,53,3,53,752,8,53,1,53,3, + 53,755,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,765,8,53,1,53, + 1,53,1,53,1,53,3,53,771,8,53,1,53,3,53,774,8,53,1,53,3,53,777,8,53,1, + 53,1,53,1,53,1,53,1,53,1,53,3,53,785,8,53,1,53,3,53,788,8,53,1,53,1,53, + 3,53,792,8,53,1,53,3,53,795,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,1,53,3,53,809,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,826,8,53,1,53,1,53,1, + 53,3,53,831,8,53,1,53,1,53,3,53,835,8,53,1,53,1,53,1,53,1,53,3,53,841, + 8,53,1,53,1,53,1,53,1,53,1,53,3,53,848,8,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,3,53,860,8,53,1,53,1,53,3,53,864,8,53,1,53,3, + 53,867,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,876,8,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,890,8,53,1,53, 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,724,8,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,741,8,53, - 1,53,1,53,1,53,1,53,3,53,747,8,53,1,53,3,53,750,8,53,1,53,3,53,753,8, - 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,763,8,53,1,53,1,53,1, - 53,1,53,3,53,769,8,53,1,53,3,53,772,8,53,1,53,3,53,775,8,53,1,53,1,53, - 1,53,1,53,1,53,1,53,3,53,783,8,53,1,53,3,53,786,8,53,1,53,1,53,3,53,790, - 8,53,1,53,3,53,793,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,3,53,807,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,824,8,53,1,53,1,53,1,53,3,53, - 829,8,53,1,53,1,53,3,53,833,8,53,1,53,1,53,1,53,1,53,3,53,839,8,53,1, - 53,1,53,1,53,1,53,1,53,3,53,846,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1, - 53,1,53,1,53,1,53,3,53,858,8,53,1,53,1,53,3,53,862,8,53,1,53,3,53,865, - 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,874,8,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,888,8,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,915,8,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,3,53,924,8,53,5,53,926,8,53,10,53,12,53,929, - 9,53,1,54,1,54,1,54,5,54,934,8,54,10,54,12,54,937,9,54,1,55,1,55,3,55, - 941,8,55,1,56,1,56,1,56,1,56,5,56,947,8,56,10,56,12,56,950,9,56,1,56, - 1,56,1,56,1,56,1,56,5,56,957,8,56,10,56,12,56,960,9,56,3,56,962,8,56, - 1,56,1,56,1,56,1,57,1,57,1,57,5,57,970,8,57,10,57,12,57,973,9,57,1,57, - 1,57,1,57,1,57,1,57,1,57,5,57,981,8,57,10,57,12,57,984,9,57,1,57,1,57, - 3,57,988,8,57,1,57,1,57,1,57,1,57,1,57,3,57,995,8,57,1,58,1,58,1,58,1, - 58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,3,58,1008,8,58,1,59,1,59,1,59,5, - 59,1013,8,59,10,59,12,59,1016,9,59,1,60,1,60,1,60,1,60,1,60,1,60,1,60, - 1,60,1,60,1,60,3,60,1028,8,60,1,61,1,61,1,61,1,61,3,61,1034,8,61,1,61, - 3,61,1037,8,61,1,62,1,62,1,62,5,62,1042,8,62,10,62,12,62,1045,9,62,1, - 63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,3,63,1056,8,63,1,63,1,63,1, - 63,1,63,3,63,1062,8,63,5,63,1064,8,63,10,63,12,63,1067,9,63,1,64,1,64, - 1,64,3,64,1072,8,64,1,64,1,64,1,65,1,65,1,65,3,65,1079,8,65,1,65,1,65, - 1,66,1,66,1,66,5,66,1086,8,66,10,66,12,66,1089,9,66,1,67,1,67,1,68,1, - 68,1,68,1,68,1,68,1,68,3,68,1099,8,68,3,68,1101,8,68,1,69,3,69,1104,8, - 69,1,69,1,69,1,69,1,69,1,69,1,69,3,69,1112,8,69,1,70,1,70,1,70,3,70,1117, - 8,70,1,71,1,71,1,72,1,72,1,73,1,73,1,74,1,74,3,74,1127,8,74,1,75,1,75, - 1,75,3,75,1132,8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,78,1,78, - 3,78,1144,8,78,1,79,1,79,5,79,1148,8,79,10,79,12,79,1151,9,79,1,79,1, - 79,1,80,1,80,1,80,1,80,1,80,3,80,1160,8,80,1,81,1,81,5,81,1164,8,81,10, - 81,12,81,1167,9,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,3,82,1176,8,82, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,917,8,53,1,53, + 1,53,1,53,1,53,1,53,1,53,3,53,925,8,53,5,53,927,8,53,10,53,12,53,930, + 9,53,1,54,1,54,1,54,5,54,935,8,54,10,54,12,54,938,9,54,1,55,1,55,3,55, + 942,8,55,1,56,1,56,1,56,1,56,5,56,948,8,56,10,56,12,56,951,9,56,1,56, + 1,56,1,56,1,56,1,56,5,56,958,8,56,10,56,12,56,961,9,56,3,56,963,8,56, + 1,56,1,56,1,56,1,57,1,57,1,57,5,57,971,8,57,10,57,12,57,974,9,57,1,57, + 1,57,1,57,1,57,1,57,1,57,5,57,982,8,57,10,57,12,57,985,9,57,1,57,1,57, + 3,57,989,8,57,1,57,1,57,1,57,1,57,1,57,3,57,996,8,57,1,58,1,58,1,58,1, + 58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,3,58,1009,8,58,1,59,1,59,1,59,5, + 59,1014,8,59,10,59,12,59,1017,9,59,1,60,1,60,1,60,1,60,1,60,1,60,1,60, + 1,60,1,60,1,60,3,60,1029,8,60,1,61,1,61,1,61,1,61,3,61,1035,8,61,1,61, + 3,61,1038,8,61,1,62,1,62,1,62,5,62,1043,8,62,10,62,12,62,1046,9,62,1, + 63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,3,63,1057,8,63,1,63,1,63,1, + 63,1,63,3,63,1063,8,63,5,63,1065,8,63,10,63,12,63,1068,9,63,1,64,1,64, + 1,64,3,64,1073,8,64,1,64,1,64,1,65,1,65,1,65,3,65,1080,8,65,1,65,1,65, + 1,66,1,66,1,66,5,66,1087,8,66,10,66,12,66,1090,9,66,1,67,1,67,1,68,1, + 68,1,68,1,68,1,68,1,68,3,68,1100,8,68,3,68,1102,8,68,1,69,3,69,1105,8, + 69,1,69,1,69,1,69,1,69,1,69,1,69,3,69,1113,8,69,1,70,1,70,1,70,3,70,1118, + 8,70,1,71,1,71,1,72,1,72,1,73,1,73,1,74,1,74,3,74,1128,8,74,1,75,1,75, + 1,75,3,75,1133,8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,78,1,78, + 3,78,1145,8,78,1,79,1,79,5,79,1149,8,79,10,79,12,79,1152,9,79,1,79,1, + 79,1,80,1,80,1,80,1,80,1,80,3,80,1161,8,80,1,81,1,81,5,81,1165,8,81,10, + 81,12,81,1168,9,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,3,82,1177,8,82, 1,82,0,3,68,106,126,83,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32, 34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78, 80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118, @@ -216,335 +215,336 @@ void hogqlparserParserInitialize() { 0,28,28,47,47,2,0,69,69,74,74,3,0,10,10,48,48,87,87,2,0,39,39,51,51,1, 0,103,104,2,0,114,114,134,134,7,0,20,20,36,36,53,54,68,68,76,76,93,93, 99,99,12,0,1,19,21,28,30,35,37,40,42,49,51,52,56,56,58,67,69,75,77,92, - 94,95,97,98,4,0,19,19,28,28,37,37,46,46,1314,0,169,1,0,0,0,2,176,1,0, - 0,0,4,178,1,0,0,0,6,180,1,0,0,0,8,189,1,0,0,0,10,195,1,0,0,0,12,212,1, - 0,0,0,14,214,1,0,0,0,16,217,1,0,0,0,18,226,1,0,0,0,20,232,1,0,0,0,22, - 236,1,0,0,0,24,245,1,0,0,0,26,247,1,0,0,0,28,256,1,0,0,0,30,260,1,0,0, - 0,32,271,1,0,0,0,34,275,1,0,0,0,36,290,1,0,0,0,38,293,1,0,0,0,40,342, - 1,0,0,0,42,345,1,0,0,0,44,351,1,0,0,0,46,355,1,0,0,0,48,361,1,0,0,0,50, - 379,1,0,0,0,52,382,1,0,0,0,54,385,1,0,0,0,56,395,1,0,0,0,58,398,1,0,0, - 0,60,402,1,0,0,0,62,435,1,0,0,0,64,437,1,0,0,0,66,440,1,0,0,0,68,455, - 1,0,0,0,70,517,1,0,0,0,72,522,1,0,0,0,74,533,1,0,0,0,76,535,1,0,0,0,78, - 541,1,0,0,0,80,549,1,0,0,0,82,567,1,0,0,0,84,569,1,0,0,0,86,577,1,0,0, - 0,88,582,1,0,0,0,90,590,1,0,0,0,92,594,1,0,0,0,94,598,1,0,0,0,96,607, - 1,0,0,0,98,621,1,0,0,0,100,623,1,0,0,0,102,673,1,0,0,0,104,675,1,0,0, - 0,106,832,1,0,0,0,108,930,1,0,0,0,110,940,1,0,0,0,112,961,1,0,0,0,114, - 994,1,0,0,0,116,1007,1,0,0,0,118,1009,1,0,0,0,120,1027,1,0,0,0,122,1036, - 1,0,0,0,124,1038,1,0,0,0,126,1055,1,0,0,0,128,1068,1,0,0,0,130,1078,1, - 0,0,0,132,1082,1,0,0,0,134,1090,1,0,0,0,136,1100,1,0,0,0,138,1103,1,0, - 0,0,140,1116,1,0,0,0,142,1118,1,0,0,0,144,1120,1,0,0,0,146,1122,1,0,0, - 0,148,1126,1,0,0,0,150,1131,1,0,0,0,152,1133,1,0,0,0,154,1137,1,0,0,0, - 156,1143,1,0,0,0,158,1145,1,0,0,0,160,1159,1,0,0,0,162,1161,1,0,0,0,164, - 1175,1,0,0,0,166,168,3,2,1,0,167,166,1,0,0,0,168,171,1,0,0,0,169,167, + 94,95,97,98,4,0,19,19,28,28,37,37,46,46,1317,0,169,1,0,0,0,2,176,1,0, + 0,0,4,178,1,0,0,0,6,180,1,0,0,0,8,187,1,0,0,0,10,203,1,0,0,0,12,205,1, + 0,0,0,14,212,1,0,0,0,16,221,1,0,0,0,18,229,1,0,0,0,20,238,1,0,0,0,22, + 243,1,0,0,0,24,247,1,0,0,0,26,249,1,0,0,0,28,258,1,0,0,0,30,262,1,0,0, + 0,32,273,1,0,0,0,34,277,1,0,0,0,36,292,1,0,0,0,38,295,1,0,0,0,40,344, + 1,0,0,0,42,347,1,0,0,0,44,353,1,0,0,0,46,357,1,0,0,0,48,363,1,0,0,0,50, + 381,1,0,0,0,52,384,1,0,0,0,54,387,1,0,0,0,56,397,1,0,0,0,58,400,1,0,0, + 0,60,404,1,0,0,0,62,437,1,0,0,0,64,439,1,0,0,0,66,442,1,0,0,0,68,457, + 1,0,0,0,70,519,1,0,0,0,72,524,1,0,0,0,74,535,1,0,0,0,76,537,1,0,0,0,78, + 543,1,0,0,0,80,551,1,0,0,0,82,569,1,0,0,0,84,571,1,0,0,0,86,579,1,0,0, + 0,88,584,1,0,0,0,90,592,1,0,0,0,92,596,1,0,0,0,94,600,1,0,0,0,96,609, + 1,0,0,0,98,623,1,0,0,0,100,625,1,0,0,0,102,675,1,0,0,0,104,677,1,0,0, + 0,106,834,1,0,0,0,108,931,1,0,0,0,110,941,1,0,0,0,112,962,1,0,0,0,114, + 995,1,0,0,0,116,1008,1,0,0,0,118,1010,1,0,0,0,120,1028,1,0,0,0,122,1037, + 1,0,0,0,124,1039,1,0,0,0,126,1056,1,0,0,0,128,1069,1,0,0,0,130,1079,1, + 0,0,0,132,1083,1,0,0,0,134,1091,1,0,0,0,136,1101,1,0,0,0,138,1104,1,0, + 0,0,140,1117,1,0,0,0,142,1119,1,0,0,0,144,1121,1,0,0,0,146,1123,1,0,0, + 0,148,1127,1,0,0,0,150,1132,1,0,0,0,152,1134,1,0,0,0,154,1138,1,0,0,0, + 156,1144,1,0,0,0,158,1146,1,0,0,0,160,1160,1,0,0,0,162,1162,1,0,0,0,164, + 1176,1,0,0,0,166,168,3,2,1,0,167,166,1,0,0,0,168,171,1,0,0,0,169,167, 1,0,0,0,169,170,1,0,0,0,170,172,1,0,0,0,171,169,1,0,0,0,172,173,5,0,0, - 1,173,1,1,0,0,0,174,177,3,6,3,0,175,177,3,12,6,0,176,174,1,0,0,0,176, + 1,173,1,1,0,0,0,174,177,3,6,3,0,175,177,3,10,5,0,176,174,1,0,0,0,176, 175,1,0,0,0,177,3,1,0,0,0,178,179,3,106,53,0,179,5,1,0,0,0,180,181,5, 50,0,0,181,185,3,150,75,0,182,183,5,111,0,0,183,184,5,118,0,0,184,186, - 3,4,2,0,185,182,1,0,0,0,185,186,1,0,0,0,186,187,1,0,0,0,187,188,5,145, - 0,0,188,7,1,0,0,0,189,190,3,4,2,0,190,191,5,111,0,0,191,192,5,118,0,0, - 192,193,3,4,2,0,193,194,5,145,0,0,194,9,1,0,0,0,195,200,3,150,75,0,196, - 197,5,112,0,0,197,199,3,150,75,0,198,196,1,0,0,0,199,202,1,0,0,0,200, - 198,1,0,0,0,200,201,1,0,0,0,201,11,1,0,0,0,202,200,1,0,0,0,203,213,3, - 20,10,0,204,213,3,24,12,0,205,213,3,14,7,0,206,213,3,16,8,0,207,213,3, - 18,9,0,208,213,3,22,11,0,209,213,3,8,4,0,210,213,3,20,10,0,211,213,3, - 26,13,0,212,203,1,0,0,0,212,204,1,0,0,0,212,205,1,0,0,0,212,206,1,0,0, - 0,212,207,1,0,0,0,212,208,1,0,0,0,212,209,1,0,0,0,212,210,1,0,0,0,212, - 211,1,0,0,0,213,13,1,0,0,0,214,215,3,4,2,0,215,216,5,145,0,0,216,15,1, - 0,0,0,217,218,5,38,0,0,218,219,5,126,0,0,219,220,3,4,2,0,220,221,5,144, - 0,0,221,224,3,12,6,0,222,223,5,24,0,0,223,225,3,12,6,0,224,222,1,0,0, - 0,224,225,1,0,0,0,225,17,1,0,0,0,226,227,5,96,0,0,227,228,5,126,0,0,228, - 229,3,4,2,0,229,230,5,144,0,0,230,231,3,12,6,0,231,19,1,0,0,0,232,233, - 5,70,0,0,233,234,3,4,2,0,234,235,5,145,0,0,235,21,1,0,0,0,236,237,5,29, - 0,0,237,238,3,150,75,0,238,240,5,126,0,0,239,241,3,10,5,0,240,239,1,0, - 0,0,240,241,1,0,0,0,241,242,1,0,0,0,242,243,5,144,0,0,243,244,3,26,13, - 0,244,23,1,0,0,0,245,246,5,145,0,0,246,25,1,0,0,0,247,251,5,124,0,0,248, - 250,3,2,1,0,249,248,1,0,0,0,250,253,1,0,0,0,251,249,1,0,0,0,251,252,1, - 0,0,0,252,254,1,0,0,0,253,251,1,0,0,0,254,255,5,142,0,0,255,27,1,0,0, - 0,256,257,3,4,2,0,257,258,5,111,0,0,258,259,3,4,2,0,259,29,1,0,0,0,260, - 265,3,28,14,0,261,262,5,112,0,0,262,264,3,28,14,0,263,261,1,0,0,0,264, - 267,1,0,0,0,265,263,1,0,0,0,265,266,1,0,0,0,266,31,1,0,0,0,267,265,1, - 0,0,0,268,272,3,34,17,0,269,272,3,38,19,0,270,272,3,114,57,0,271,268, - 1,0,0,0,271,269,1,0,0,0,271,270,1,0,0,0,272,273,1,0,0,0,273,274,5,0,0, - 1,274,33,1,0,0,0,275,281,3,36,18,0,276,277,5,91,0,0,277,278,5,1,0,0,278, - 280,3,36,18,0,279,276,1,0,0,0,280,283,1,0,0,0,281,279,1,0,0,0,281,282, - 1,0,0,0,282,35,1,0,0,0,283,281,1,0,0,0,284,291,3,38,19,0,285,286,5,126, - 0,0,286,287,3,34,17,0,287,288,5,144,0,0,288,291,1,0,0,0,289,291,3,154, - 77,0,290,284,1,0,0,0,290,285,1,0,0,0,290,289,1,0,0,0,291,37,1,0,0,0,292, - 294,3,40,20,0,293,292,1,0,0,0,293,294,1,0,0,0,294,295,1,0,0,0,295,297, - 5,77,0,0,296,298,5,23,0,0,297,296,1,0,0,0,297,298,1,0,0,0,298,300,1,0, - 0,0,299,301,3,42,21,0,300,299,1,0,0,0,300,301,1,0,0,0,301,302,1,0,0,0, - 302,304,3,104,52,0,303,305,3,44,22,0,304,303,1,0,0,0,304,305,1,0,0,0, - 305,307,1,0,0,0,306,308,3,46,23,0,307,306,1,0,0,0,307,308,1,0,0,0,308, - 310,1,0,0,0,309,311,3,50,25,0,310,309,1,0,0,0,310,311,1,0,0,0,311,313, - 1,0,0,0,312,314,3,52,26,0,313,312,1,0,0,0,313,314,1,0,0,0,314,316,1,0, - 0,0,315,317,3,54,27,0,316,315,1,0,0,0,316,317,1,0,0,0,317,320,1,0,0,0, - 318,319,5,98,0,0,319,321,7,0,0,0,320,318,1,0,0,0,320,321,1,0,0,0,321, - 324,1,0,0,0,322,323,5,98,0,0,323,325,5,86,0,0,324,322,1,0,0,0,324,325, - 1,0,0,0,325,327,1,0,0,0,326,328,3,56,28,0,327,326,1,0,0,0,327,328,1,0, - 0,0,328,330,1,0,0,0,329,331,3,48,24,0,330,329,1,0,0,0,330,331,1,0,0,0, - 331,333,1,0,0,0,332,334,3,58,29,0,333,332,1,0,0,0,333,334,1,0,0,0,334, - 337,1,0,0,0,335,338,3,62,31,0,336,338,3,64,32,0,337,335,1,0,0,0,337,336, - 1,0,0,0,337,338,1,0,0,0,338,340,1,0,0,0,339,341,3,66,33,0,340,339,1,0, - 0,0,340,341,1,0,0,0,341,39,1,0,0,0,342,343,5,98,0,0,343,344,3,118,59, - 0,344,41,1,0,0,0,345,346,5,85,0,0,346,349,5,104,0,0,347,348,5,98,0,0, - 348,350,5,82,0,0,349,347,1,0,0,0,349,350,1,0,0,0,350,43,1,0,0,0,351,352, - 5,32,0,0,352,353,3,68,34,0,353,45,1,0,0,0,354,356,7,1,0,0,355,354,1,0, - 0,0,355,356,1,0,0,0,356,357,1,0,0,0,357,358,5,5,0,0,358,359,5,45,0,0, - 359,360,3,104,52,0,360,47,1,0,0,0,361,362,5,97,0,0,362,363,3,150,75,0, - 363,364,5,6,0,0,364,365,5,126,0,0,365,366,3,88,44,0,366,376,5,144,0,0, - 367,368,5,112,0,0,368,369,3,150,75,0,369,370,5,6,0,0,370,371,5,126,0, - 0,371,372,3,88,44,0,372,373,5,144,0,0,373,375,1,0,0,0,374,367,1,0,0,0, - 375,378,1,0,0,0,376,374,1,0,0,0,376,377,1,0,0,0,377,49,1,0,0,0,378,376, - 1,0,0,0,379,380,5,67,0,0,380,381,3,106,53,0,381,51,1,0,0,0,382,383,5, - 95,0,0,383,384,3,106,53,0,384,53,1,0,0,0,385,386,5,34,0,0,386,393,5,11, - 0,0,387,388,7,0,0,0,388,389,5,126,0,0,389,390,3,104,52,0,390,391,5,144, - 0,0,391,394,1,0,0,0,392,394,3,104,52,0,393,387,1,0,0,0,393,392,1,0,0, - 0,394,55,1,0,0,0,395,396,5,35,0,0,396,397,3,106,53,0,397,57,1,0,0,0,398, - 399,5,62,0,0,399,400,5,11,0,0,400,401,3,78,39,0,401,59,1,0,0,0,402,403, - 5,62,0,0,403,404,5,11,0,0,404,405,3,104,52,0,405,61,1,0,0,0,406,407,5, - 52,0,0,407,410,3,106,53,0,408,409,5,112,0,0,409,411,3,106,53,0,410,408, - 1,0,0,0,410,411,1,0,0,0,411,416,1,0,0,0,412,413,5,98,0,0,413,417,5,82, - 0,0,414,415,5,11,0,0,415,417,3,104,52,0,416,412,1,0,0,0,416,414,1,0,0, - 0,416,417,1,0,0,0,417,436,1,0,0,0,418,419,5,52,0,0,419,422,3,106,53,0, - 420,421,5,98,0,0,421,423,5,82,0,0,422,420,1,0,0,0,422,423,1,0,0,0,423, - 424,1,0,0,0,424,425,5,59,0,0,425,426,3,106,53,0,426,436,1,0,0,0,427,428, - 5,52,0,0,428,429,3,106,53,0,429,430,5,59,0,0,430,433,3,106,53,0,431,432, - 5,11,0,0,432,434,3,104,52,0,433,431,1,0,0,0,433,434,1,0,0,0,434,436,1, - 0,0,0,435,406,1,0,0,0,435,418,1,0,0,0,435,427,1,0,0,0,436,63,1,0,0,0, - 437,438,5,59,0,0,438,439,3,106,53,0,439,65,1,0,0,0,440,441,5,79,0,0,441, - 442,3,84,42,0,442,67,1,0,0,0,443,444,6,34,-1,0,444,446,3,126,63,0,445, - 447,5,27,0,0,446,445,1,0,0,0,446,447,1,0,0,0,447,449,1,0,0,0,448,450, - 3,76,38,0,449,448,1,0,0,0,449,450,1,0,0,0,450,456,1,0,0,0,451,452,5,126, - 0,0,452,453,3,68,34,0,453,454,5,144,0,0,454,456,1,0,0,0,455,443,1,0,0, - 0,455,451,1,0,0,0,456,471,1,0,0,0,457,458,10,3,0,0,458,459,3,72,36,0, - 459,460,3,68,34,4,460,470,1,0,0,0,461,463,10,4,0,0,462,464,3,70,35,0, - 463,462,1,0,0,0,463,464,1,0,0,0,464,465,1,0,0,0,465,466,5,45,0,0,466, - 467,3,68,34,0,467,468,3,74,37,0,468,470,1,0,0,0,469,457,1,0,0,0,469,461, - 1,0,0,0,470,473,1,0,0,0,471,469,1,0,0,0,471,472,1,0,0,0,472,69,1,0,0, - 0,473,471,1,0,0,0,474,476,7,2,0,0,475,474,1,0,0,0,475,476,1,0,0,0,476, - 477,1,0,0,0,477,484,5,42,0,0,478,480,5,42,0,0,479,481,7,2,0,0,480,479, - 1,0,0,0,480,481,1,0,0,0,481,484,1,0,0,0,482,484,7,2,0,0,483,475,1,0,0, - 0,483,478,1,0,0,0,483,482,1,0,0,0,484,518,1,0,0,0,485,487,7,3,0,0,486, - 485,1,0,0,0,486,487,1,0,0,0,487,488,1,0,0,0,488,490,7,4,0,0,489,491,5, - 63,0,0,490,489,1,0,0,0,490,491,1,0,0,0,491,500,1,0,0,0,492,494,7,4,0, - 0,493,495,5,63,0,0,494,493,1,0,0,0,494,495,1,0,0,0,495,497,1,0,0,0,496, - 498,7,3,0,0,497,496,1,0,0,0,497,498,1,0,0,0,498,500,1,0,0,0,499,486,1, - 0,0,0,499,492,1,0,0,0,500,518,1,0,0,0,501,503,7,5,0,0,502,501,1,0,0,0, - 502,503,1,0,0,0,503,504,1,0,0,0,504,506,5,33,0,0,505,507,5,63,0,0,506, - 505,1,0,0,0,506,507,1,0,0,0,507,516,1,0,0,0,508,510,5,33,0,0,509,511, - 5,63,0,0,510,509,1,0,0,0,510,511,1,0,0,0,511,513,1,0,0,0,512,514,7,5, - 0,0,513,512,1,0,0,0,513,514,1,0,0,0,514,516,1,0,0,0,515,502,1,0,0,0,515, - 508,1,0,0,0,516,518,1,0,0,0,517,483,1,0,0,0,517,499,1,0,0,0,517,515,1, - 0,0,0,518,71,1,0,0,0,519,520,5,16,0,0,520,523,5,45,0,0,521,523,5,112, - 0,0,522,519,1,0,0,0,522,521,1,0,0,0,523,73,1,0,0,0,524,525,5,60,0,0,525, - 534,3,104,52,0,526,527,5,92,0,0,527,528,5,126,0,0,528,529,3,104,52,0, - 529,530,5,144,0,0,530,534,1,0,0,0,531,532,5,92,0,0,532,534,3,104,52,0, - 533,524,1,0,0,0,533,526,1,0,0,0,533,531,1,0,0,0,534,75,1,0,0,0,535,536, - 5,75,0,0,536,539,3,82,41,0,537,538,5,59,0,0,538,540,3,82,41,0,539,537, - 1,0,0,0,539,540,1,0,0,0,540,77,1,0,0,0,541,546,3,80,40,0,542,543,5,112, - 0,0,543,545,3,80,40,0,544,542,1,0,0,0,545,548,1,0,0,0,546,544,1,0,0,0, - 546,547,1,0,0,0,547,79,1,0,0,0,548,546,1,0,0,0,549,551,3,106,53,0,550, - 552,7,6,0,0,551,550,1,0,0,0,551,552,1,0,0,0,552,555,1,0,0,0,553,554,5, - 58,0,0,554,556,7,7,0,0,555,553,1,0,0,0,555,556,1,0,0,0,556,559,1,0,0, - 0,557,558,5,15,0,0,558,560,5,106,0,0,559,557,1,0,0,0,559,560,1,0,0,0, - 560,81,1,0,0,0,561,568,3,154,77,0,562,565,3,138,69,0,563,564,5,146,0, - 0,564,566,3,138,69,0,565,563,1,0,0,0,565,566,1,0,0,0,566,568,1,0,0,0, - 567,561,1,0,0,0,567,562,1,0,0,0,568,83,1,0,0,0,569,574,3,86,43,0,570, - 571,5,112,0,0,571,573,3,86,43,0,572,570,1,0,0,0,573,576,1,0,0,0,574,572, - 1,0,0,0,574,575,1,0,0,0,575,85,1,0,0,0,576,574,1,0,0,0,577,578,3,150, - 75,0,578,579,5,118,0,0,579,580,3,140,70,0,580,87,1,0,0,0,581,583,3,90, - 45,0,582,581,1,0,0,0,582,583,1,0,0,0,583,585,1,0,0,0,584,586,3,92,46, - 0,585,584,1,0,0,0,585,586,1,0,0,0,586,588,1,0,0,0,587,589,3,94,47,0,588, - 587,1,0,0,0,588,589,1,0,0,0,589,89,1,0,0,0,590,591,5,65,0,0,591,592,5, - 11,0,0,592,593,3,104,52,0,593,91,1,0,0,0,594,595,5,62,0,0,595,596,5,11, - 0,0,596,597,3,78,39,0,597,93,1,0,0,0,598,599,7,8,0,0,599,600,3,96,48, - 0,600,95,1,0,0,0,601,608,3,98,49,0,602,603,5,9,0,0,603,604,3,98,49,0, - 604,605,5,2,0,0,605,606,3,98,49,0,606,608,1,0,0,0,607,601,1,0,0,0,607, - 602,1,0,0,0,608,97,1,0,0,0,609,610,5,18,0,0,610,622,5,73,0,0,611,612, - 5,90,0,0,612,622,5,66,0,0,613,614,5,90,0,0,614,622,5,30,0,0,615,616,3, - 138,69,0,616,617,5,66,0,0,617,622,1,0,0,0,618,619,3,138,69,0,619,620, - 5,30,0,0,620,622,1,0,0,0,621,609,1,0,0,0,621,611,1,0,0,0,621,613,1,0, - 0,0,621,615,1,0,0,0,621,618,1,0,0,0,622,99,1,0,0,0,623,624,3,106,53,0, - 624,625,5,0,0,1,625,101,1,0,0,0,626,674,3,150,75,0,627,628,3,150,75,0, - 628,629,5,126,0,0,629,630,3,150,75,0,630,637,3,102,51,0,631,632,5,112, - 0,0,632,633,3,150,75,0,633,634,3,102,51,0,634,636,1,0,0,0,635,631,1,0, - 0,0,636,639,1,0,0,0,637,635,1,0,0,0,637,638,1,0,0,0,638,640,1,0,0,0,639, - 637,1,0,0,0,640,641,5,144,0,0,641,674,1,0,0,0,642,643,3,150,75,0,643, - 644,5,126,0,0,644,649,3,152,76,0,645,646,5,112,0,0,646,648,3,152,76,0, - 647,645,1,0,0,0,648,651,1,0,0,0,649,647,1,0,0,0,649,650,1,0,0,0,650,652, - 1,0,0,0,651,649,1,0,0,0,652,653,5,144,0,0,653,674,1,0,0,0,654,655,3,150, - 75,0,655,656,5,126,0,0,656,661,3,102,51,0,657,658,5,112,0,0,658,660,3, - 102,51,0,659,657,1,0,0,0,660,663,1,0,0,0,661,659,1,0,0,0,661,662,1,0, - 0,0,662,664,1,0,0,0,663,661,1,0,0,0,664,665,5,144,0,0,665,674,1,0,0,0, - 666,667,3,150,75,0,667,669,5,126,0,0,668,670,3,104,52,0,669,668,1,0,0, - 0,669,670,1,0,0,0,670,671,1,0,0,0,671,672,5,144,0,0,672,674,1,0,0,0,673, - 626,1,0,0,0,673,627,1,0,0,0,673,642,1,0,0,0,673,654,1,0,0,0,673,666,1, - 0,0,0,674,103,1,0,0,0,675,680,3,106,53,0,676,677,5,112,0,0,677,679,3, - 106,53,0,678,676,1,0,0,0,679,682,1,0,0,0,680,678,1,0,0,0,680,681,1,0, - 0,0,681,105,1,0,0,0,682,680,1,0,0,0,683,684,6,53,-1,0,684,686,5,12,0, - 0,685,687,3,106,53,0,686,685,1,0,0,0,686,687,1,0,0,0,687,693,1,0,0,0, - 688,689,5,94,0,0,689,690,3,106,53,0,690,691,5,81,0,0,691,692,3,106,53, - 0,692,694,1,0,0,0,693,688,1,0,0,0,694,695,1,0,0,0,695,693,1,0,0,0,695, - 696,1,0,0,0,696,699,1,0,0,0,697,698,5,24,0,0,698,700,3,106,53,0,699,697, - 1,0,0,0,699,700,1,0,0,0,700,701,1,0,0,0,701,702,5,25,0,0,702,833,1,0, - 0,0,703,704,5,13,0,0,704,705,5,126,0,0,705,706,3,106,53,0,706,707,5,6, - 0,0,707,708,3,102,51,0,708,709,5,144,0,0,709,833,1,0,0,0,710,711,5,19, - 0,0,711,833,5,106,0,0,712,713,5,43,0,0,713,714,3,106,53,0,714,715,3,142, - 71,0,715,833,1,0,0,0,716,717,5,80,0,0,717,718,5,126,0,0,718,719,3,106, - 53,0,719,720,5,32,0,0,720,723,3,106,53,0,721,722,5,31,0,0,722,724,3,106, - 53,0,723,721,1,0,0,0,723,724,1,0,0,0,724,725,1,0,0,0,725,726,5,144,0, - 0,726,833,1,0,0,0,727,728,5,83,0,0,728,833,5,106,0,0,729,730,5,88,0,0, - 730,731,5,126,0,0,731,732,7,9,0,0,732,733,3,156,78,0,733,734,5,32,0,0, - 734,735,3,106,53,0,735,736,5,144,0,0,736,833,1,0,0,0,737,738,3,150,75, - 0,738,740,5,126,0,0,739,741,3,104,52,0,740,739,1,0,0,0,740,741,1,0,0, - 0,741,742,1,0,0,0,742,743,5,144,0,0,743,752,1,0,0,0,744,746,5,126,0,0, - 745,747,5,23,0,0,746,745,1,0,0,0,746,747,1,0,0,0,747,749,1,0,0,0,748, - 750,3,108,54,0,749,748,1,0,0,0,749,750,1,0,0,0,750,751,1,0,0,0,751,753, - 5,144,0,0,752,744,1,0,0,0,752,753,1,0,0,0,753,754,1,0,0,0,754,755,5,64, - 0,0,755,756,5,126,0,0,756,757,3,88,44,0,757,758,5,144,0,0,758,833,1,0, - 0,0,759,760,3,150,75,0,760,762,5,126,0,0,761,763,3,104,52,0,762,761,1, - 0,0,0,762,763,1,0,0,0,763,764,1,0,0,0,764,765,5,144,0,0,765,774,1,0,0, - 0,766,768,5,126,0,0,767,769,5,23,0,0,768,767,1,0,0,0,768,769,1,0,0,0, - 769,771,1,0,0,0,770,772,3,108,54,0,771,770,1,0,0,0,771,772,1,0,0,0,772, - 773,1,0,0,0,773,775,5,144,0,0,774,766,1,0,0,0,774,775,1,0,0,0,775,776, - 1,0,0,0,776,777,5,64,0,0,777,778,3,150,75,0,778,833,1,0,0,0,779,785,3, - 150,75,0,780,782,5,126,0,0,781,783,3,104,52,0,782,781,1,0,0,0,782,783, - 1,0,0,0,783,784,1,0,0,0,784,786,5,144,0,0,785,780,1,0,0,0,785,786,1,0, - 0,0,786,787,1,0,0,0,787,789,5,126,0,0,788,790,5,23,0,0,789,788,1,0,0, - 0,789,790,1,0,0,0,790,792,1,0,0,0,791,793,3,108,54,0,792,791,1,0,0,0, - 792,793,1,0,0,0,793,794,1,0,0,0,794,795,5,144,0,0,795,833,1,0,0,0,796, - 833,3,114,57,0,797,833,3,158,79,0,798,833,3,140,70,0,799,800,5,114,0, - 0,800,833,3,106,53,19,801,802,5,56,0,0,802,833,3,106,53,13,803,804,3, - 130,65,0,804,805,5,116,0,0,805,807,1,0,0,0,806,803,1,0,0,0,806,807,1, - 0,0,0,807,808,1,0,0,0,808,833,5,108,0,0,809,810,5,126,0,0,810,811,3,34, - 17,0,811,812,5,144,0,0,812,833,1,0,0,0,813,814,5,126,0,0,814,815,3,106, - 53,0,815,816,5,144,0,0,816,833,1,0,0,0,817,818,5,126,0,0,818,819,3,104, - 52,0,819,820,5,144,0,0,820,833,1,0,0,0,821,823,5,125,0,0,822,824,3,104, - 52,0,823,822,1,0,0,0,823,824,1,0,0,0,824,825,1,0,0,0,825,833,5,143,0, - 0,826,828,5,124,0,0,827,829,3,30,15,0,828,827,1,0,0,0,828,829,1,0,0,0, - 829,830,1,0,0,0,830,833,5,142,0,0,831,833,3,122,61,0,832,683,1,0,0,0, - 832,703,1,0,0,0,832,710,1,0,0,0,832,712,1,0,0,0,832,716,1,0,0,0,832,727, - 1,0,0,0,832,729,1,0,0,0,832,737,1,0,0,0,832,759,1,0,0,0,832,779,1,0,0, - 0,832,796,1,0,0,0,832,797,1,0,0,0,832,798,1,0,0,0,832,799,1,0,0,0,832, - 801,1,0,0,0,832,806,1,0,0,0,832,809,1,0,0,0,832,813,1,0,0,0,832,817,1, - 0,0,0,832,821,1,0,0,0,832,826,1,0,0,0,832,831,1,0,0,0,833,927,1,0,0,0, - 834,838,10,18,0,0,835,839,5,108,0,0,836,839,5,146,0,0,837,839,5,133,0, - 0,838,835,1,0,0,0,838,836,1,0,0,0,838,837,1,0,0,0,839,840,1,0,0,0,840, - 926,3,106,53,19,841,845,10,17,0,0,842,846,5,134,0,0,843,846,5,114,0,0, - 844,846,5,113,0,0,845,842,1,0,0,0,845,843,1,0,0,0,845,844,1,0,0,0,846, - 847,1,0,0,0,847,926,3,106,53,18,848,873,10,16,0,0,849,874,5,117,0,0,850, - 874,5,118,0,0,851,874,5,129,0,0,852,874,5,127,0,0,853,874,5,128,0,0,854, - 874,5,119,0,0,855,874,5,120,0,0,856,858,5,56,0,0,857,856,1,0,0,0,857, - 858,1,0,0,0,858,859,1,0,0,0,859,861,5,40,0,0,860,862,5,14,0,0,861,860, - 1,0,0,0,861,862,1,0,0,0,862,874,1,0,0,0,863,865,5,56,0,0,864,863,1,0, - 0,0,864,865,1,0,0,0,865,866,1,0,0,0,866,874,7,10,0,0,867,874,5,140,0, - 0,868,874,5,141,0,0,869,874,5,131,0,0,870,874,5,122,0,0,871,874,5,123, - 0,0,872,874,5,130,0,0,873,849,1,0,0,0,873,850,1,0,0,0,873,851,1,0,0,0, - 873,852,1,0,0,0,873,853,1,0,0,0,873,854,1,0,0,0,873,855,1,0,0,0,873,857, - 1,0,0,0,873,864,1,0,0,0,873,867,1,0,0,0,873,868,1,0,0,0,873,869,1,0,0, - 0,873,870,1,0,0,0,873,871,1,0,0,0,873,872,1,0,0,0,874,875,1,0,0,0,875, - 926,3,106,53,17,876,877,10,14,0,0,877,878,5,132,0,0,878,926,3,106,53, - 15,879,880,10,12,0,0,880,881,5,2,0,0,881,926,3,106,53,13,882,883,10,11, - 0,0,883,884,5,61,0,0,884,926,3,106,53,12,885,887,10,10,0,0,886,888,5, - 56,0,0,887,886,1,0,0,0,887,888,1,0,0,0,888,889,1,0,0,0,889,890,5,9,0, - 0,890,891,3,106,53,0,891,892,5,2,0,0,892,893,3,106,53,11,893,926,1,0, - 0,0,894,895,10,9,0,0,895,896,5,135,0,0,896,897,3,106,53,0,897,898,5,111, - 0,0,898,899,3,106,53,9,899,926,1,0,0,0,900,901,10,22,0,0,901,902,5,125, - 0,0,902,903,3,106,53,0,903,904,5,143,0,0,904,926,1,0,0,0,905,906,10,21, - 0,0,906,907,5,116,0,0,907,926,5,104,0,0,908,909,10,20,0,0,909,910,5,116, - 0,0,910,926,3,150,75,0,911,912,10,15,0,0,912,914,5,44,0,0,913,915,5,56, - 0,0,914,913,1,0,0,0,914,915,1,0,0,0,915,916,1,0,0,0,916,926,5,57,0,0, - 917,923,10,8,0,0,918,924,3,148,74,0,919,920,5,6,0,0,920,924,3,150,75, - 0,921,922,5,6,0,0,922,924,5,106,0,0,923,918,1,0,0,0,923,919,1,0,0,0,923, - 921,1,0,0,0,924,926,1,0,0,0,925,834,1,0,0,0,925,841,1,0,0,0,925,848,1, - 0,0,0,925,876,1,0,0,0,925,879,1,0,0,0,925,882,1,0,0,0,925,885,1,0,0,0, - 925,894,1,0,0,0,925,900,1,0,0,0,925,905,1,0,0,0,925,908,1,0,0,0,925,911, - 1,0,0,0,925,917,1,0,0,0,926,929,1,0,0,0,927,925,1,0,0,0,927,928,1,0,0, - 0,928,107,1,0,0,0,929,927,1,0,0,0,930,935,3,110,55,0,931,932,5,112,0, - 0,932,934,3,110,55,0,933,931,1,0,0,0,934,937,1,0,0,0,935,933,1,0,0,0, - 935,936,1,0,0,0,936,109,1,0,0,0,937,935,1,0,0,0,938,941,3,112,56,0,939, - 941,3,106,53,0,940,938,1,0,0,0,940,939,1,0,0,0,941,111,1,0,0,0,942,943, - 5,126,0,0,943,948,3,150,75,0,944,945,5,112,0,0,945,947,3,150,75,0,946, - 944,1,0,0,0,947,950,1,0,0,0,948,946,1,0,0,0,948,949,1,0,0,0,949,951,1, - 0,0,0,950,948,1,0,0,0,951,952,5,144,0,0,952,962,1,0,0,0,953,958,3,150, - 75,0,954,955,5,112,0,0,955,957,3,150,75,0,956,954,1,0,0,0,957,960,1,0, - 0,0,958,956,1,0,0,0,958,959,1,0,0,0,959,962,1,0,0,0,960,958,1,0,0,0,961, - 942,1,0,0,0,961,953,1,0,0,0,962,963,1,0,0,0,963,964,5,107,0,0,964,965, - 3,106,53,0,965,113,1,0,0,0,966,967,5,128,0,0,967,971,3,150,75,0,968,970, - 3,116,58,0,969,968,1,0,0,0,970,973,1,0,0,0,971,969,1,0,0,0,971,972,1, - 0,0,0,972,974,1,0,0,0,973,971,1,0,0,0,974,975,5,146,0,0,975,976,5,120, - 0,0,976,995,1,0,0,0,977,978,5,128,0,0,978,982,3,150,75,0,979,981,3,116, - 58,0,980,979,1,0,0,0,981,984,1,0,0,0,982,980,1,0,0,0,982,983,1,0,0,0, - 983,985,1,0,0,0,984,982,1,0,0,0,985,987,5,120,0,0,986,988,3,114,57,0, - 987,986,1,0,0,0,987,988,1,0,0,0,988,989,1,0,0,0,989,990,5,128,0,0,990, - 991,5,146,0,0,991,992,3,150,75,0,992,993,5,120,0,0,993,995,1,0,0,0,994, - 966,1,0,0,0,994,977,1,0,0,0,995,115,1,0,0,0,996,997,3,150,75,0,997,998, - 5,118,0,0,998,999,3,156,78,0,999,1008,1,0,0,0,1000,1001,3,150,75,0,1001, - 1002,5,118,0,0,1002,1003,5,124,0,0,1003,1004,3,106,53,0,1004,1005,5,142, - 0,0,1005,1008,1,0,0,0,1006,1008,3,150,75,0,1007,996,1,0,0,0,1007,1000, - 1,0,0,0,1007,1006,1,0,0,0,1008,117,1,0,0,0,1009,1014,3,120,60,0,1010, - 1011,5,112,0,0,1011,1013,3,120,60,0,1012,1010,1,0,0,0,1013,1016,1,0,0, - 0,1014,1012,1,0,0,0,1014,1015,1,0,0,0,1015,119,1,0,0,0,1016,1014,1,0, - 0,0,1017,1018,3,150,75,0,1018,1019,5,6,0,0,1019,1020,5,126,0,0,1020,1021, - 3,34,17,0,1021,1022,5,144,0,0,1022,1028,1,0,0,0,1023,1024,3,106,53,0, - 1024,1025,5,6,0,0,1025,1026,3,150,75,0,1026,1028,1,0,0,0,1027,1017,1, - 0,0,0,1027,1023,1,0,0,0,1028,121,1,0,0,0,1029,1037,3,154,77,0,1030,1031, - 3,130,65,0,1031,1032,5,116,0,0,1032,1034,1,0,0,0,1033,1030,1,0,0,0,1033, - 1034,1,0,0,0,1034,1035,1,0,0,0,1035,1037,3,124,62,0,1036,1029,1,0,0,0, - 1036,1033,1,0,0,0,1037,123,1,0,0,0,1038,1043,3,150,75,0,1039,1040,5,116, - 0,0,1040,1042,3,150,75,0,1041,1039,1,0,0,0,1042,1045,1,0,0,0,1043,1041, - 1,0,0,0,1043,1044,1,0,0,0,1044,125,1,0,0,0,1045,1043,1,0,0,0,1046,1047, - 6,63,-1,0,1047,1056,3,130,65,0,1048,1056,3,128,64,0,1049,1050,5,126,0, - 0,1050,1051,3,34,17,0,1051,1052,5,144,0,0,1052,1056,1,0,0,0,1053,1056, - 3,114,57,0,1054,1056,3,154,77,0,1055,1046,1,0,0,0,1055,1048,1,0,0,0,1055, - 1049,1,0,0,0,1055,1053,1,0,0,0,1055,1054,1,0,0,0,1056,1065,1,0,0,0,1057, - 1061,10,3,0,0,1058,1062,3,148,74,0,1059,1060,5,6,0,0,1060,1062,3,150, - 75,0,1061,1058,1,0,0,0,1061,1059,1,0,0,0,1062,1064,1,0,0,0,1063,1057, - 1,0,0,0,1064,1067,1,0,0,0,1065,1063,1,0,0,0,1065,1066,1,0,0,0,1066,127, - 1,0,0,0,1067,1065,1,0,0,0,1068,1069,3,150,75,0,1069,1071,5,126,0,0,1070, - 1072,3,132,66,0,1071,1070,1,0,0,0,1071,1072,1,0,0,0,1072,1073,1,0,0,0, - 1073,1074,5,144,0,0,1074,129,1,0,0,0,1075,1076,3,134,67,0,1076,1077,5, - 116,0,0,1077,1079,1,0,0,0,1078,1075,1,0,0,0,1078,1079,1,0,0,0,1079,1080, - 1,0,0,0,1080,1081,3,150,75,0,1081,131,1,0,0,0,1082,1087,3,106,53,0,1083, - 1084,5,112,0,0,1084,1086,3,106,53,0,1085,1083,1,0,0,0,1086,1089,1,0,0, - 0,1087,1085,1,0,0,0,1087,1088,1,0,0,0,1088,133,1,0,0,0,1089,1087,1,0, - 0,0,1090,1091,3,150,75,0,1091,135,1,0,0,0,1092,1101,5,102,0,0,1093,1094, - 5,116,0,0,1094,1101,7,11,0,0,1095,1096,5,104,0,0,1096,1098,5,116,0,0, - 1097,1099,7,11,0,0,1098,1097,1,0,0,0,1098,1099,1,0,0,0,1099,1101,1,0, - 0,0,1100,1092,1,0,0,0,1100,1093,1,0,0,0,1100,1095,1,0,0,0,1101,137,1, - 0,0,0,1102,1104,7,12,0,0,1103,1102,1,0,0,0,1103,1104,1,0,0,0,1104,1111, - 1,0,0,0,1105,1112,3,136,68,0,1106,1112,5,103,0,0,1107,1112,5,104,0,0, - 1108,1112,5,105,0,0,1109,1112,5,41,0,0,1110,1112,5,55,0,0,1111,1105,1, - 0,0,0,1111,1106,1,0,0,0,1111,1107,1,0,0,0,1111,1108,1,0,0,0,1111,1109, - 1,0,0,0,1111,1110,1,0,0,0,1112,139,1,0,0,0,1113,1117,3,138,69,0,1114, - 1117,5,106,0,0,1115,1117,5,57,0,0,1116,1113,1,0,0,0,1116,1114,1,0,0,0, - 1116,1115,1,0,0,0,1117,141,1,0,0,0,1118,1119,7,13,0,0,1119,143,1,0,0, - 0,1120,1121,7,14,0,0,1121,145,1,0,0,0,1122,1123,7,15,0,0,1123,147,1,0, - 0,0,1124,1127,5,101,0,0,1125,1127,3,146,73,0,1126,1124,1,0,0,0,1126,1125, - 1,0,0,0,1127,149,1,0,0,0,1128,1132,5,101,0,0,1129,1132,3,142,71,0,1130, - 1132,3,144,72,0,1131,1128,1,0,0,0,1131,1129,1,0,0,0,1131,1130,1,0,0,0, - 1132,151,1,0,0,0,1133,1134,3,156,78,0,1134,1135,5,118,0,0,1135,1136,3, - 138,69,0,1136,153,1,0,0,0,1137,1138,5,124,0,0,1138,1139,3,150,75,0,1139, - 1140,5,142,0,0,1140,155,1,0,0,0,1141,1144,5,106,0,0,1142,1144,3,158,79, - 0,1143,1141,1,0,0,0,1143,1142,1,0,0,0,1144,157,1,0,0,0,1145,1149,5,137, - 0,0,1146,1148,3,160,80,0,1147,1146,1,0,0,0,1148,1151,1,0,0,0,1149,1147, - 1,0,0,0,1149,1150,1,0,0,0,1150,1152,1,0,0,0,1151,1149,1,0,0,0,1152,1153, - 5,139,0,0,1153,159,1,0,0,0,1154,1155,5,152,0,0,1155,1156,3,106,53,0,1156, - 1157,5,142,0,0,1157,1160,1,0,0,0,1158,1160,5,151,0,0,1159,1154,1,0,0, - 0,1159,1158,1,0,0,0,1160,161,1,0,0,0,1161,1165,5,138,0,0,1162,1164,3, - 164,82,0,1163,1162,1,0,0,0,1164,1167,1,0,0,0,1165,1163,1,0,0,0,1165,1166, - 1,0,0,0,1166,1168,1,0,0,0,1167,1165,1,0,0,0,1168,1169,5,0,0,1,1169,163, - 1,0,0,0,1170,1171,5,154,0,0,1171,1172,3,106,53,0,1172,1173,5,142,0,0, - 1173,1176,1,0,0,0,1174,1176,5,153,0,0,1175,1170,1,0,0,0,1175,1174,1,0, - 0,0,1176,165,1,0,0,0,141,169,176,185,200,212,224,240,251,265,271,281, - 290,293,297,300,304,307,310,313,316,320,324,327,330,333,337,340,349,355, - 376,393,410,416,422,433,435,446,449,455,463,469,471,475,480,483,486,490, - 494,497,499,502,506,510,513,515,517,522,533,539,546,551,555,559,565,567, - 574,582,585,588,607,621,637,649,661,669,673,680,686,695,699,723,740,746, - 749,752,762,768,771,774,782,785,789,792,806,823,828,832,838,845,857,861, - 864,873,887,914,923,925,927,935,940,948,958,961,971,982,987,994,1007, - 1014,1027,1033,1036,1043,1055,1061,1065,1071,1078,1087,1098,1100,1103, - 1111,1116,1126,1131,1143,1149,1159,1165,1175 + 3,4,2,0,185,182,1,0,0,0,185,186,1,0,0,0,186,7,1,0,0,0,187,192,3,150,75, + 0,188,189,5,112,0,0,189,191,3,150,75,0,190,188,1,0,0,0,191,194,1,0,0, + 0,192,190,1,0,0,0,192,193,1,0,0,0,193,9,1,0,0,0,194,192,1,0,0,0,195,204, + 3,12,6,0,196,204,3,14,7,0,197,204,3,16,8,0,198,204,3,18,9,0,199,204,3, + 20,10,0,200,204,3,22,11,0,201,204,3,24,12,0,202,204,3,26,13,0,203,195, + 1,0,0,0,203,196,1,0,0,0,203,197,1,0,0,0,203,198,1,0,0,0,203,199,1,0,0, + 0,203,200,1,0,0,0,203,201,1,0,0,0,203,202,1,0,0,0,204,11,1,0,0,0,205, + 207,5,70,0,0,206,208,3,4,2,0,207,206,1,0,0,0,207,208,1,0,0,0,208,210, + 1,0,0,0,209,211,5,145,0,0,210,209,1,0,0,0,210,211,1,0,0,0,211,13,1,0, + 0,0,212,213,5,38,0,0,213,214,5,126,0,0,214,215,3,4,2,0,215,216,5,144, + 0,0,216,219,3,10,5,0,217,218,5,24,0,0,218,220,3,10,5,0,219,217,1,0,0, + 0,219,220,1,0,0,0,220,15,1,0,0,0,221,222,5,96,0,0,222,223,5,126,0,0,223, + 224,3,4,2,0,224,225,5,144,0,0,225,227,3,10,5,0,226,228,5,145,0,0,227, + 226,1,0,0,0,227,228,1,0,0,0,228,17,1,0,0,0,229,230,5,29,0,0,230,231,3, + 150,75,0,231,233,5,126,0,0,232,234,3,8,4,0,233,232,1,0,0,0,233,234,1, + 0,0,0,234,235,1,0,0,0,235,236,5,144,0,0,236,237,3,26,13,0,237,19,1,0, + 0,0,238,239,3,4,2,0,239,240,5,111,0,0,240,241,5,118,0,0,241,242,3,4,2, + 0,242,21,1,0,0,0,243,245,3,4,2,0,244,246,5,145,0,0,245,244,1,0,0,0,245, + 246,1,0,0,0,246,23,1,0,0,0,247,248,5,145,0,0,248,25,1,0,0,0,249,253,5, + 124,0,0,250,252,3,2,1,0,251,250,1,0,0,0,252,255,1,0,0,0,253,251,1,0,0, + 0,253,254,1,0,0,0,254,256,1,0,0,0,255,253,1,0,0,0,256,257,5,142,0,0,257, + 27,1,0,0,0,258,259,3,4,2,0,259,260,5,111,0,0,260,261,3,4,2,0,261,29,1, + 0,0,0,262,267,3,28,14,0,263,264,5,112,0,0,264,266,3,28,14,0,265,263,1, + 0,0,0,266,269,1,0,0,0,267,265,1,0,0,0,267,268,1,0,0,0,268,31,1,0,0,0, + 269,267,1,0,0,0,270,274,3,34,17,0,271,274,3,38,19,0,272,274,3,114,57, + 0,273,270,1,0,0,0,273,271,1,0,0,0,273,272,1,0,0,0,274,275,1,0,0,0,275, + 276,5,0,0,1,276,33,1,0,0,0,277,283,3,36,18,0,278,279,5,91,0,0,279,280, + 5,1,0,0,280,282,3,36,18,0,281,278,1,0,0,0,282,285,1,0,0,0,283,281,1,0, + 0,0,283,284,1,0,0,0,284,35,1,0,0,0,285,283,1,0,0,0,286,293,3,38,19,0, + 287,288,5,126,0,0,288,289,3,34,17,0,289,290,5,144,0,0,290,293,1,0,0,0, + 291,293,3,154,77,0,292,286,1,0,0,0,292,287,1,0,0,0,292,291,1,0,0,0,293, + 37,1,0,0,0,294,296,3,40,20,0,295,294,1,0,0,0,295,296,1,0,0,0,296,297, + 1,0,0,0,297,299,5,77,0,0,298,300,5,23,0,0,299,298,1,0,0,0,299,300,1,0, + 0,0,300,302,1,0,0,0,301,303,3,42,21,0,302,301,1,0,0,0,302,303,1,0,0,0, + 303,304,1,0,0,0,304,306,3,104,52,0,305,307,3,44,22,0,306,305,1,0,0,0, + 306,307,1,0,0,0,307,309,1,0,0,0,308,310,3,46,23,0,309,308,1,0,0,0,309, + 310,1,0,0,0,310,312,1,0,0,0,311,313,3,50,25,0,312,311,1,0,0,0,312,313, + 1,0,0,0,313,315,1,0,0,0,314,316,3,52,26,0,315,314,1,0,0,0,315,316,1,0, + 0,0,316,318,1,0,0,0,317,319,3,54,27,0,318,317,1,0,0,0,318,319,1,0,0,0, + 319,322,1,0,0,0,320,321,5,98,0,0,321,323,7,0,0,0,322,320,1,0,0,0,322, + 323,1,0,0,0,323,326,1,0,0,0,324,325,5,98,0,0,325,327,5,86,0,0,326,324, + 1,0,0,0,326,327,1,0,0,0,327,329,1,0,0,0,328,330,3,56,28,0,329,328,1,0, + 0,0,329,330,1,0,0,0,330,332,1,0,0,0,331,333,3,48,24,0,332,331,1,0,0,0, + 332,333,1,0,0,0,333,335,1,0,0,0,334,336,3,58,29,0,335,334,1,0,0,0,335, + 336,1,0,0,0,336,339,1,0,0,0,337,340,3,62,31,0,338,340,3,64,32,0,339,337, + 1,0,0,0,339,338,1,0,0,0,339,340,1,0,0,0,340,342,1,0,0,0,341,343,3,66, + 33,0,342,341,1,0,0,0,342,343,1,0,0,0,343,39,1,0,0,0,344,345,5,98,0,0, + 345,346,3,118,59,0,346,41,1,0,0,0,347,348,5,85,0,0,348,351,5,104,0,0, + 349,350,5,98,0,0,350,352,5,82,0,0,351,349,1,0,0,0,351,352,1,0,0,0,352, + 43,1,0,0,0,353,354,5,32,0,0,354,355,3,68,34,0,355,45,1,0,0,0,356,358, + 7,1,0,0,357,356,1,0,0,0,357,358,1,0,0,0,358,359,1,0,0,0,359,360,5,5,0, + 0,360,361,5,45,0,0,361,362,3,104,52,0,362,47,1,0,0,0,363,364,5,97,0,0, + 364,365,3,150,75,0,365,366,5,6,0,0,366,367,5,126,0,0,367,368,3,88,44, + 0,368,378,5,144,0,0,369,370,5,112,0,0,370,371,3,150,75,0,371,372,5,6, + 0,0,372,373,5,126,0,0,373,374,3,88,44,0,374,375,5,144,0,0,375,377,1,0, + 0,0,376,369,1,0,0,0,377,380,1,0,0,0,378,376,1,0,0,0,378,379,1,0,0,0,379, + 49,1,0,0,0,380,378,1,0,0,0,381,382,5,67,0,0,382,383,3,106,53,0,383,51, + 1,0,0,0,384,385,5,95,0,0,385,386,3,106,53,0,386,53,1,0,0,0,387,388,5, + 34,0,0,388,395,5,11,0,0,389,390,7,0,0,0,390,391,5,126,0,0,391,392,3,104, + 52,0,392,393,5,144,0,0,393,396,1,0,0,0,394,396,3,104,52,0,395,389,1,0, + 0,0,395,394,1,0,0,0,396,55,1,0,0,0,397,398,5,35,0,0,398,399,3,106,53, + 0,399,57,1,0,0,0,400,401,5,62,0,0,401,402,5,11,0,0,402,403,3,78,39,0, + 403,59,1,0,0,0,404,405,5,62,0,0,405,406,5,11,0,0,406,407,3,104,52,0,407, + 61,1,0,0,0,408,409,5,52,0,0,409,412,3,106,53,0,410,411,5,112,0,0,411, + 413,3,106,53,0,412,410,1,0,0,0,412,413,1,0,0,0,413,418,1,0,0,0,414,415, + 5,98,0,0,415,419,5,82,0,0,416,417,5,11,0,0,417,419,3,104,52,0,418,414, + 1,0,0,0,418,416,1,0,0,0,418,419,1,0,0,0,419,438,1,0,0,0,420,421,5,52, + 0,0,421,424,3,106,53,0,422,423,5,98,0,0,423,425,5,82,0,0,424,422,1,0, + 0,0,424,425,1,0,0,0,425,426,1,0,0,0,426,427,5,59,0,0,427,428,3,106,53, + 0,428,438,1,0,0,0,429,430,5,52,0,0,430,431,3,106,53,0,431,432,5,59,0, + 0,432,435,3,106,53,0,433,434,5,11,0,0,434,436,3,104,52,0,435,433,1,0, + 0,0,435,436,1,0,0,0,436,438,1,0,0,0,437,408,1,0,0,0,437,420,1,0,0,0,437, + 429,1,0,0,0,438,63,1,0,0,0,439,440,5,59,0,0,440,441,3,106,53,0,441,65, + 1,0,0,0,442,443,5,79,0,0,443,444,3,84,42,0,444,67,1,0,0,0,445,446,6,34, + -1,0,446,448,3,126,63,0,447,449,5,27,0,0,448,447,1,0,0,0,448,449,1,0, + 0,0,449,451,1,0,0,0,450,452,3,76,38,0,451,450,1,0,0,0,451,452,1,0,0,0, + 452,458,1,0,0,0,453,454,5,126,0,0,454,455,3,68,34,0,455,456,5,144,0,0, + 456,458,1,0,0,0,457,445,1,0,0,0,457,453,1,0,0,0,458,473,1,0,0,0,459,460, + 10,3,0,0,460,461,3,72,36,0,461,462,3,68,34,4,462,472,1,0,0,0,463,465, + 10,4,0,0,464,466,3,70,35,0,465,464,1,0,0,0,465,466,1,0,0,0,466,467,1, + 0,0,0,467,468,5,45,0,0,468,469,3,68,34,0,469,470,3,74,37,0,470,472,1, + 0,0,0,471,459,1,0,0,0,471,463,1,0,0,0,472,475,1,0,0,0,473,471,1,0,0,0, + 473,474,1,0,0,0,474,69,1,0,0,0,475,473,1,0,0,0,476,478,7,2,0,0,477,476, + 1,0,0,0,477,478,1,0,0,0,478,479,1,0,0,0,479,486,5,42,0,0,480,482,5,42, + 0,0,481,483,7,2,0,0,482,481,1,0,0,0,482,483,1,0,0,0,483,486,1,0,0,0,484, + 486,7,2,0,0,485,477,1,0,0,0,485,480,1,0,0,0,485,484,1,0,0,0,486,520,1, + 0,0,0,487,489,7,3,0,0,488,487,1,0,0,0,488,489,1,0,0,0,489,490,1,0,0,0, + 490,492,7,4,0,0,491,493,5,63,0,0,492,491,1,0,0,0,492,493,1,0,0,0,493, + 502,1,0,0,0,494,496,7,4,0,0,495,497,5,63,0,0,496,495,1,0,0,0,496,497, + 1,0,0,0,497,499,1,0,0,0,498,500,7,3,0,0,499,498,1,0,0,0,499,500,1,0,0, + 0,500,502,1,0,0,0,501,488,1,0,0,0,501,494,1,0,0,0,502,520,1,0,0,0,503, + 505,7,5,0,0,504,503,1,0,0,0,504,505,1,0,0,0,505,506,1,0,0,0,506,508,5, + 33,0,0,507,509,5,63,0,0,508,507,1,0,0,0,508,509,1,0,0,0,509,518,1,0,0, + 0,510,512,5,33,0,0,511,513,5,63,0,0,512,511,1,0,0,0,512,513,1,0,0,0,513, + 515,1,0,0,0,514,516,7,5,0,0,515,514,1,0,0,0,515,516,1,0,0,0,516,518,1, + 0,0,0,517,504,1,0,0,0,517,510,1,0,0,0,518,520,1,0,0,0,519,485,1,0,0,0, + 519,501,1,0,0,0,519,517,1,0,0,0,520,71,1,0,0,0,521,522,5,16,0,0,522,525, + 5,45,0,0,523,525,5,112,0,0,524,521,1,0,0,0,524,523,1,0,0,0,525,73,1,0, + 0,0,526,527,5,60,0,0,527,536,3,104,52,0,528,529,5,92,0,0,529,530,5,126, + 0,0,530,531,3,104,52,0,531,532,5,144,0,0,532,536,1,0,0,0,533,534,5,92, + 0,0,534,536,3,104,52,0,535,526,1,0,0,0,535,528,1,0,0,0,535,533,1,0,0, + 0,536,75,1,0,0,0,537,538,5,75,0,0,538,541,3,82,41,0,539,540,5,59,0,0, + 540,542,3,82,41,0,541,539,1,0,0,0,541,542,1,0,0,0,542,77,1,0,0,0,543, + 548,3,80,40,0,544,545,5,112,0,0,545,547,3,80,40,0,546,544,1,0,0,0,547, + 550,1,0,0,0,548,546,1,0,0,0,548,549,1,0,0,0,549,79,1,0,0,0,550,548,1, + 0,0,0,551,553,3,106,53,0,552,554,7,6,0,0,553,552,1,0,0,0,553,554,1,0, + 0,0,554,557,1,0,0,0,555,556,5,58,0,0,556,558,7,7,0,0,557,555,1,0,0,0, + 557,558,1,0,0,0,558,561,1,0,0,0,559,560,5,15,0,0,560,562,5,106,0,0,561, + 559,1,0,0,0,561,562,1,0,0,0,562,81,1,0,0,0,563,570,3,154,77,0,564,567, + 3,138,69,0,565,566,5,146,0,0,566,568,3,138,69,0,567,565,1,0,0,0,567,568, + 1,0,0,0,568,570,1,0,0,0,569,563,1,0,0,0,569,564,1,0,0,0,570,83,1,0,0, + 0,571,576,3,86,43,0,572,573,5,112,0,0,573,575,3,86,43,0,574,572,1,0,0, + 0,575,578,1,0,0,0,576,574,1,0,0,0,576,577,1,0,0,0,577,85,1,0,0,0,578, + 576,1,0,0,0,579,580,3,150,75,0,580,581,5,118,0,0,581,582,3,140,70,0,582, + 87,1,0,0,0,583,585,3,90,45,0,584,583,1,0,0,0,584,585,1,0,0,0,585,587, + 1,0,0,0,586,588,3,92,46,0,587,586,1,0,0,0,587,588,1,0,0,0,588,590,1,0, + 0,0,589,591,3,94,47,0,590,589,1,0,0,0,590,591,1,0,0,0,591,89,1,0,0,0, + 592,593,5,65,0,0,593,594,5,11,0,0,594,595,3,104,52,0,595,91,1,0,0,0,596, + 597,5,62,0,0,597,598,5,11,0,0,598,599,3,78,39,0,599,93,1,0,0,0,600,601, + 7,8,0,0,601,602,3,96,48,0,602,95,1,0,0,0,603,610,3,98,49,0,604,605,5, + 9,0,0,605,606,3,98,49,0,606,607,5,2,0,0,607,608,3,98,49,0,608,610,1,0, + 0,0,609,603,1,0,0,0,609,604,1,0,0,0,610,97,1,0,0,0,611,612,5,18,0,0,612, + 624,5,73,0,0,613,614,5,90,0,0,614,624,5,66,0,0,615,616,5,90,0,0,616,624, + 5,30,0,0,617,618,3,138,69,0,618,619,5,66,0,0,619,624,1,0,0,0,620,621, + 3,138,69,0,621,622,5,30,0,0,622,624,1,0,0,0,623,611,1,0,0,0,623,613,1, + 0,0,0,623,615,1,0,0,0,623,617,1,0,0,0,623,620,1,0,0,0,624,99,1,0,0,0, + 625,626,3,106,53,0,626,627,5,0,0,1,627,101,1,0,0,0,628,676,3,150,75,0, + 629,630,3,150,75,0,630,631,5,126,0,0,631,632,3,150,75,0,632,639,3,102, + 51,0,633,634,5,112,0,0,634,635,3,150,75,0,635,636,3,102,51,0,636,638, + 1,0,0,0,637,633,1,0,0,0,638,641,1,0,0,0,639,637,1,0,0,0,639,640,1,0,0, + 0,640,642,1,0,0,0,641,639,1,0,0,0,642,643,5,144,0,0,643,676,1,0,0,0,644, + 645,3,150,75,0,645,646,5,126,0,0,646,651,3,152,76,0,647,648,5,112,0,0, + 648,650,3,152,76,0,649,647,1,0,0,0,650,653,1,0,0,0,651,649,1,0,0,0,651, + 652,1,0,0,0,652,654,1,0,0,0,653,651,1,0,0,0,654,655,5,144,0,0,655,676, + 1,0,0,0,656,657,3,150,75,0,657,658,5,126,0,0,658,663,3,102,51,0,659,660, + 5,112,0,0,660,662,3,102,51,0,661,659,1,0,0,0,662,665,1,0,0,0,663,661, + 1,0,0,0,663,664,1,0,0,0,664,666,1,0,0,0,665,663,1,0,0,0,666,667,5,144, + 0,0,667,676,1,0,0,0,668,669,3,150,75,0,669,671,5,126,0,0,670,672,3,104, + 52,0,671,670,1,0,0,0,671,672,1,0,0,0,672,673,1,0,0,0,673,674,5,144,0, + 0,674,676,1,0,0,0,675,628,1,0,0,0,675,629,1,0,0,0,675,644,1,0,0,0,675, + 656,1,0,0,0,675,668,1,0,0,0,676,103,1,0,0,0,677,682,3,106,53,0,678,679, + 5,112,0,0,679,681,3,106,53,0,680,678,1,0,0,0,681,684,1,0,0,0,682,680, + 1,0,0,0,682,683,1,0,0,0,683,105,1,0,0,0,684,682,1,0,0,0,685,686,6,53, + -1,0,686,688,5,12,0,0,687,689,3,106,53,0,688,687,1,0,0,0,688,689,1,0, + 0,0,689,695,1,0,0,0,690,691,5,94,0,0,691,692,3,106,53,0,692,693,5,81, + 0,0,693,694,3,106,53,0,694,696,1,0,0,0,695,690,1,0,0,0,696,697,1,0,0, + 0,697,695,1,0,0,0,697,698,1,0,0,0,698,701,1,0,0,0,699,700,5,24,0,0,700, + 702,3,106,53,0,701,699,1,0,0,0,701,702,1,0,0,0,702,703,1,0,0,0,703,704, + 5,25,0,0,704,835,1,0,0,0,705,706,5,13,0,0,706,707,5,126,0,0,707,708,3, + 106,53,0,708,709,5,6,0,0,709,710,3,102,51,0,710,711,5,144,0,0,711,835, + 1,0,0,0,712,713,5,19,0,0,713,835,5,106,0,0,714,715,5,43,0,0,715,716,3, + 106,53,0,716,717,3,142,71,0,717,835,1,0,0,0,718,719,5,80,0,0,719,720, + 5,126,0,0,720,721,3,106,53,0,721,722,5,32,0,0,722,725,3,106,53,0,723, + 724,5,31,0,0,724,726,3,106,53,0,725,723,1,0,0,0,725,726,1,0,0,0,726,727, + 1,0,0,0,727,728,5,144,0,0,728,835,1,0,0,0,729,730,5,83,0,0,730,835,5, + 106,0,0,731,732,5,88,0,0,732,733,5,126,0,0,733,734,7,9,0,0,734,735,3, + 156,78,0,735,736,5,32,0,0,736,737,3,106,53,0,737,738,5,144,0,0,738,835, + 1,0,0,0,739,740,3,150,75,0,740,742,5,126,0,0,741,743,3,104,52,0,742,741, + 1,0,0,0,742,743,1,0,0,0,743,744,1,0,0,0,744,745,5,144,0,0,745,754,1,0, + 0,0,746,748,5,126,0,0,747,749,5,23,0,0,748,747,1,0,0,0,748,749,1,0,0, + 0,749,751,1,0,0,0,750,752,3,108,54,0,751,750,1,0,0,0,751,752,1,0,0,0, + 752,753,1,0,0,0,753,755,5,144,0,0,754,746,1,0,0,0,754,755,1,0,0,0,755, + 756,1,0,0,0,756,757,5,64,0,0,757,758,5,126,0,0,758,759,3,88,44,0,759, + 760,5,144,0,0,760,835,1,0,0,0,761,762,3,150,75,0,762,764,5,126,0,0,763, + 765,3,104,52,0,764,763,1,0,0,0,764,765,1,0,0,0,765,766,1,0,0,0,766,767, + 5,144,0,0,767,776,1,0,0,0,768,770,5,126,0,0,769,771,5,23,0,0,770,769, + 1,0,0,0,770,771,1,0,0,0,771,773,1,0,0,0,772,774,3,108,54,0,773,772,1, + 0,0,0,773,774,1,0,0,0,774,775,1,0,0,0,775,777,5,144,0,0,776,768,1,0,0, + 0,776,777,1,0,0,0,777,778,1,0,0,0,778,779,5,64,0,0,779,780,3,150,75,0, + 780,835,1,0,0,0,781,787,3,150,75,0,782,784,5,126,0,0,783,785,3,104,52, + 0,784,783,1,0,0,0,784,785,1,0,0,0,785,786,1,0,0,0,786,788,5,144,0,0,787, + 782,1,0,0,0,787,788,1,0,0,0,788,789,1,0,0,0,789,791,5,126,0,0,790,792, + 5,23,0,0,791,790,1,0,0,0,791,792,1,0,0,0,792,794,1,0,0,0,793,795,3,108, + 54,0,794,793,1,0,0,0,794,795,1,0,0,0,795,796,1,0,0,0,796,797,5,144,0, + 0,797,835,1,0,0,0,798,835,3,114,57,0,799,835,3,158,79,0,800,835,3,140, + 70,0,801,802,5,114,0,0,802,835,3,106,53,19,803,804,5,56,0,0,804,835,3, + 106,53,13,805,806,3,130,65,0,806,807,5,116,0,0,807,809,1,0,0,0,808,805, + 1,0,0,0,808,809,1,0,0,0,809,810,1,0,0,0,810,835,5,108,0,0,811,812,5,126, + 0,0,812,813,3,34,17,0,813,814,5,144,0,0,814,835,1,0,0,0,815,816,5,126, + 0,0,816,817,3,106,53,0,817,818,5,144,0,0,818,835,1,0,0,0,819,820,5,126, + 0,0,820,821,3,104,52,0,821,822,5,144,0,0,822,835,1,0,0,0,823,825,5,125, + 0,0,824,826,3,104,52,0,825,824,1,0,0,0,825,826,1,0,0,0,826,827,1,0,0, + 0,827,835,5,143,0,0,828,830,5,124,0,0,829,831,3,30,15,0,830,829,1,0,0, + 0,830,831,1,0,0,0,831,832,1,0,0,0,832,835,5,142,0,0,833,835,3,122,61, + 0,834,685,1,0,0,0,834,705,1,0,0,0,834,712,1,0,0,0,834,714,1,0,0,0,834, + 718,1,0,0,0,834,729,1,0,0,0,834,731,1,0,0,0,834,739,1,0,0,0,834,761,1, + 0,0,0,834,781,1,0,0,0,834,798,1,0,0,0,834,799,1,0,0,0,834,800,1,0,0,0, + 834,801,1,0,0,0,834,803,1,0,0,0,834,808,1,0,0,0,834,811,1,0,0,0,834,815, + 1,0,0,0,834,819,1,0,0,0,834,823,1,0,0,0,834,828,1,0,0,0,834,833,1,0,0, + 0,835,928,1,0,0,0,836,840,10,18,0,0,837,841,5,108,0,0,838,841,5,146,0, + 0,839,841,5,133,0,0,840,837,1,0,0,0,840,838,1,0,0,0,840,839,1,0,0,0,841, + 842,1,0,0,0,842,927,3,106,53,19,843,847,10,17,0,0,844,848,5,134,0,0,845, + 848,5,114,0,0,846,848,5,113,0,0,847,844,1,0,0,0,847,845,1,0,0,0,847,846, + 1,0,0,0,848,849,1,0,0,0,849,927,3,106,53,18,850,875,10,16,0,0,851,876, + 5,117,0,0,852,876,5,118,0,0,853,876,5,129,0,0,854,876,5,127,0,0,855,876, + 5,128,0,0,856,876,5,119,0,0,857,876,5,120,0,0,858,860,5,56,0,0,859,858, + 1,0,0,0,859,860,1,0,0,0,860,861,1,0,0,0,861,863,5,40,0,0,862,864,5,14, + 0,0,863,862,1,0,0,0,863,864,1,0,0,0,864,876,1,0,0,0,865,867,5,56,0,0, + 866,865,1,0,0,0,866,867,1,0,0,0,867,868,1,0,0,0,868,876,7,10,0,0,869, + 876,5,140,0,0,870,876,5,141,0,0,871,876,5,131,0,0,872,876,5,122,0,0,873, + 876,5,123,0,0,874,876,5,130,0,0,875,851,1,0,0,0,875,852,1,0,0,0,875,853, + 1,0,0,0,875,854,1,0,0,0,875,855,1,0,0,0,875,856,1,0,0,0,875,857,1,0,0, + 0,875,859,1,0,0,0,875,866,1,0,0,0,875,869,1,0,0,0,875,870,1,0,0,0,875, + 871,1,0,0,0,875,872,1,0,0,0,875,873,1,0,0,0,875,874,1,0,0,0,876,877,1, + 0,0,0,877,927,3,106,53,17,878,879,10,14,0,0,879,880,5,132,0,0,880,927, + 3,106,53,15,881,882,10,12,0,0,882,883,5,2,0,0,883,927,3,106,53,13,884, + 885,10,11,0,0,885,886,5,61,0,0,886,927,3,106,53,12,887,889,10,10,0,0, + 888,890,5,56,0,0,889,888,1,0,0,0,889,890,1,0,0,0,890,891,1,0,0,0,891, + 892,5,9,0,0,892,893,3,106,53,0,893,894,5,2,0,0,894,895,3,106,53,11,895, + 927,1,0,0,0,896,897,10,9,0,0,897,898,5,135,0,0,898,899,3,106,53,0,899, + 900,5,111,0,0,900,901,3,106,53,9,901,927,1,0,0,0,902,903,10,22,0,0,903, + 904,5,125,0,0,904,905,3,106,53,0,905,906,5,143,0,0,906,927,1,0,0,0,907, + 908,10,21,0,0,908,909,5,116,0,0,909,927,5,104,0,0,910,911,10,20,0,0,911, + 912,5,116,0,0,912,927,3,150,75,0,913,914,10,15,0,0,914,916,5,44,0,0,915, + 917,5,56,0,0,916,915,1,0,0,0,916,917,1,0,0,0,917,918,1,0,0,0,918,927, + 5,57,0,0,919,924,10,8,0,0,920,921,5,6,0,0,921,925,3,150,75,0,922,923, + 5,6,0,0,923,925,5,106,0,0,924,920,1,0,0,0,924,922,1,0,0,0,925,927,1,0, + 0,0,926,836,1,0,0,0,926,843,1,0,0,0,926,850,1,0,0,0,926,878,1,0,0,0,926, + 881,1,0,0,0,926,884,1,0,0,0,926,887,1,0,0,0,926,896,1,0,0,0,926,902,1, + 0,0,0,926,907,1,0,0,0,926,910,1,0,0,0,926,913,1,0,0,0,926,919,1,0,0,0, + 927,930,1,0,0,0,928,926,1,0,0,0,928,929,1,0,0,0,929,107,1,0,0,0,930,928, + 1,0,0,0,931,936,3,110,55,0,932,933,5,112,0,0,933,935,3,110,55,0,934,932, + 1,0,0,0,935,938,1,0,0,0,936,934,1,0,0,0,936,937,1,0,0,0,937,109,1,0,0, + 0,938,936,1,0,0,0,939,942,3,112,56,0,940,942,3,106,53,0,941,939,1,0,0, + 0,941,940,1,0,0,0,942,111,1,0,0,0,943,944,5,126,0,0,944,949,3,150,75, + 0,945,946,5,112,0,0,946,948,3,150,75,0,947,945,1,0,0,0,948,951,1,0,0, + 0,949,947,1,0,0,0,949,950,1,0,0,0,950,952,1,0,0,0,951,949,1,0,0,0,952, + 953,5,144,0,0,953,963,1,0,0,0,954,959,3,150,75,0,955,956,5,112,0,0,956, + 958,3,150,75,0,957,955,1,0,0,0,958,961,1,0,0,0,959,957,1,0,0,0,959,960, + 1,0,0,0,960,963,1,0,0,0,961,959,1,0,0,0,962,943,1,0,0,0,962,954,1,0,0, + 0,963,964,1,0,0,0,964,965,5,107,0,0,965,966,3,106,53,0,966,113,1,0,0, + 0,967,968,5,128,0,0,968,972,3,150,75,0,969,971,3,116,58,0,970,969,1,0, + 0,0,971,974,1,0,0,0,972,970,1,0,0,0,972,973,1,0,0,0,973,975,1,0,0,0,974, + 972,1,0,0,0,975,976,5,146,0,0,976,977,5,120,0,0,977,996,1,0,0,0,978,979, + 5,128,0,0,979,983,3,150,75,0,980,982,3,116,58,0,981,980,1,0,0,0,982,985, + 1,0,0,0,983,981,1,0,0,0,983,984,1,0,0,0,984,986,1,0,0,0,985,983,1,0,0, + 0,986,988,5,120,0,0,987,989,3,114,57,0,988,987,1,0,0,0,988,989,1,0,0, + 0,989,990,1,0,0,0,990,991,5,128,0,0,991,992,5,146,0,0,992,993,3,150,75, + 0,993,994,5,120,0,0,994,996,1,0,0,0,995,967,1,0,0,0,995,978,1,0,0,0,996, + 115,1,0,0,0,997,998,3,150,75,0,998,999,5,118,0,0,999,1000,3,156,78,0, + 1000,1009,1,0,0,0,1001,1002,3,150,75,0,1002,1003,5,118,0,0,1003,1004, + 5,124,0,0,1004,1005,3,106,53,0,1005,1006,5,142,0,0,1006,1009,1,0,0,0, + 1007,1009,3,150,75,0,1008,997,1,0,0,0,1008,1001,1,0,0,0,1008,1007,1,0, + 0,0,1009,117,1,0,0,0,1010,1015,3,120,60,0,1011,1012,5,112,0,0,1012,1014, + 3,120,60,0,1013,1011,1,0,0,0,1014,1017,1,0,0,0,1015,1013,1,0,0,0,1015, + 1016,1,0,0,0,1016,119,1,0,0,0,1017,1015,1,0,0,0,1018,1019,3,150,75,0, + 1019,1020,5,6,0,0,1020,1021,5,126,0,0,1021,1022,3,34,17,0,1022,1023,5, + 144,0,0,1023,1029,1,0,0,0,1024,1025,3,106,53,0,1025,1026,5,6,0,0,1026, + 1027,3,150,75,0,1027,1029,1,0,0,0,1028,1018,1,0,0,0,1028,1024,1,0,0,0, + 1029,121,1,0,0,0,1030,1038,3,154,77,0,1031,1032,3,130,65,0,1032,1033, + 5,116,0,0,1033,1035,1,0,0,0,1034,1031,1,0,0,0,1034,1035,1,0,0,0,1035, + 1036,1,0,0,0,1036,1038,3,124,62,0,1037,1030,1,0,0,0,1037,1034,1,0,0,0, + 1038,123,1,0,0,0,1039,1044,3,150,75,0,1040,1041,5,116,0,0,1041,1043,3, + 150,75,0,1042,1040,1,0,0,0,1043,1046,1,0,0,0,1044,1042,1,0,0,0,1044,1045, + 1,0,0,0,1045,125,1,0,0,0,1046,1044,1,0,0,0,1047,1048,6,63,-1,0,1048,1057, + 3,130,65,0,1049,1057,3,128,64,0,1050,1051,5,126,0,0,1051,1052,3,34,17, + 0,1052,1053,5,144,0,0,1053,1057,1,0,0,0,1054,1057,3,114,57,0,1055,1057, + 3,154,77,0,1056,1047,1,0,0,0,1056,1049,1,0,0,0,1056,1050,1,0,0,0,1056, + 1054,1,0,0,0,1056,1055,1,0,0,0,1057,1066,1,0,0,0,1058,1062,10,3,0,0,1059, + 1063,3,148,74,0,1060,1061,5,6,0,0,1061,1063,3,150,75,0,1062,1059,1,0, + 0,0,1062,1060,1,0,0,0,1063,1065,1,0,0,0,1064,1058,1,0,0,0,1065,1068,1, + 0,0,0,1066,1064,1,0,0,0,1066,1067,1,0,0,0,1067,127,1,0,0,0,1068,1066, + 1,0,0,0,1069,1070,3,150,75,0,1070,1072,5,126,0,0,1071,1073,3,132,66,0, + 1072,1071,1,0,0,0,1072,1073,1,0,0,0,1073,1074,1,0,0,0,1074,1075,5,144, + 0,0,1075,129,1,0,0,0,1076,1077,3,134,67,0,1077,1078,5,116,0,0,1078,1080, + 1,0,0,0,1079,1076,1,0,0,0,1079,1080,1,0,0,0,1080,1081,1,0,0,0,1081,1082, + 3,150,75,0,1082,131,1,0,0,0,1083,1088,3,106,53,0,1084,1085,5,112,0,0, + 1085,1087,3,106,53,0,1086,1084,1,0,0,0,1087,1090,1,0,0,0,1088,1086,1, + 0,0,0,1088,1089,1,0,0,0,1089,133,1,0,0,0,1090,1088,1,0,0,0,1091,1092, + 3,150,75,0,1092,135,1,0,0,0,1093,1102,5,102,0,0,1094,1095,5,116,0,0,1095, + 1102,7,11,0,0,1096,1097,5,104,0,0,1097,1099,5,116,0,0,1098,1100,7,11, + 0,0,1099,1098,1,0,0,0,1099,1100,1,0,0,0,1100,1102,1,0,0,0,1101,1093,1, + 0,0,0,1101,1094,1,0,0,0,1101,1096,1,0,0,0,1102,137,1,0,0,0,1103,1105, + 7,12,0,0,1104,1103,1,0,0,0,1104,1105,1,0,0,0,1105,1112,1,0,0,0,1106,1113, + 3,136,68,0,1107,1113,5,103,0,0,1108,1113,5,104,0,0,1109,1113,5,105,0, + 0,1110,1113,5,41,0,0,1111,1113,5,55,0,0,1112,1106,1,0,0,0,1112,1107,1, + 0,0,0,1112,1108,1,0,0,0,1112,1109,1,0,0,0,1112,1110,1,0,0,0,1112,1111, + 1,0,0,0,1113,139,1,0,0,0,1114,1118,3,138,69,0,1115,1118,5,106,0,0,1116, + 1118,5,57,0,0,1117,1114,1,0,0,0,1117,1115,1,0,0,0,1117,1116,1,0,0,0,1118, + 141,1,0,0,0,1119,1120,7,13,0,0,1120,143,1,0,0,0,1121,1122,7,14,0,0,1122, + 145,1,0,0,0,1123,1124,7,15,0,0,1124,147,1,0,0,0,1125,1128,5,101,0,0,1126, + 1128,3,146,73,0,1127,1125,1,0,0,0,1127,1126,1,0,0,0,1128,149,1,0,0,0, + 1129,1133,5,101,0,0,1130,1133,3,142,71,0,1131,1133,3,144,72,0,1132,1129, + 1,0,0,0,1132,1130,1,0,0,0,1132,1131,1,0,0,0,1133,151,1,0,0,0,1134,1135, + 3,156,78,0,1135,1136,5,118,0,0,1136,1137,3,138,69,0,1137,153,1,0,0,0, + 1138,1139,5,124,0,0,1139,1140,3,150,75,0,1140,1141,5,142,0,0,1141,155, + 1,0,0,0,1142,1145,5,106,0,0,1143,1145,3,158,79,0,1144,1142,1,0,0,0,1144, + 1143,1,0,0,0,1145,157,1,0,0,0,1146,1150,5,137,0,0,1147,1149,3,160,80, + 0,1148,1147,1,0,0,0,1149,1152,1,0,0,0,1150,1148,1,0,0,0,1150,1151,1,0, + 0,0,1151,1153,1,0,0,0,1152,1150,1,0,0,0,1153,1154,5,139,0,0,1154,159, + 1,0,0,0,1155,1156,5,152,0,0,1156,1157,3,106,53,0,1157,1158,5,142,0,0, + 1158,1161,1,0,0,0,1159,1161,5,151,0,0,1160,1155,1,0,0,0,1160,1159,1,0, + 0,0,1161,161,1,0,0,0,1162,1166,5,138,0,0,1163,1165,3,164,82,0,1164,1163, + 1,0,0,0,1165,1168,1,0,0,0,1166,1164,1,0,0,0,1166,1167,1,0,0,0,1167,1169, + 1,0,0,0,1168,1166,1,0,0,0,1169,1170,5,0,0,1,1170,163,1,0,0,0,1171,1172, + 5,154,0,0,1172,1173,3,106,53,0,1173,1174,5,142,0,0,1174,1177,1,0,0,0, + 1175,1177,5,153,0,0,1176,1171,1,0,0,0,1176,1175,1,0,0,0,1177,165,1,0, + 0,0,145,169,176,185,192,203,207,210,219,227,233,245,253,267,273,283,292, + 295,299,302,306,309,312,315,318,322,326,329,332,335,339,342,351,357,378, + 395,412,418,424,435,437,448,451,457,465,471,473,477,482,485,488,492,496, + 499,501,504,508,512,515,517,519,524,535,541,548,553,557,561,567,569,576, + 584,587,590,609,623,639,651,663,671,675,682,688,697,701,725,742,748,751, + 754,764,770,773,776,784,787,791,794,808,825,830,834,840,847,859,863,866, + 875,889,916,924,926,928,936,941,949,959,962,972,983,988,995,1008,1015, + 1028,1034,1037,1044,1056,1062,1066,1072,1079,1088,1099,1101,1104,1112, + 1117,1127,1132,1144,1150,1160,1166,1176 }; staticData->serializedATN = antlr4::atn::SerializedATNView(serializedATNSegment, sizeof(serializedATNSegment) / sizeof(serializedATNSegment[0])); @@ -910,10 +910,6 @@ HogQLParser::IdentifierContext* HogQLParser::VarDeclContext::identifier() { return getRuleContext(0); } -tree::TerminalNode* HogQLParser::VarDeclContext::SEMICOLON() { - return getToken(HogQLParser::SEMICOLON, 0); -} - tree::TerminalNode* HogQLParser::VarDeclContext::COLON() { return getToken(HogQLParser::COLON, 0); } @@ -969,81 +965,6 @@ HogQLParser::VarDeclContext* HogQLParser::varDecl() { setState(184); expression(); } - setState(187); - match(HogQLParser::SEMICOLON); - - } - catch (RecognitionException &e) { - _errHandler->reportError(this, e); - _localctx->exception = std::current_exception(); - _errHandler->recover(this, _localctx->exception); - } - - return _localctx; -} - -//----------------- VarAssignmentContext ------------------------------------------------------------------ - -HogQLParser::VarAssignmentContext::VarAssignmentContext(ParserRuleContext *parent, size_t invokingState) - : ParserRuleContext(parent, invokingState) { -} - -std::vector HogQLParser::VarAssignmentContext::expression() { - return getRuleContexts(); -} - -HogQLParser::ExpressionContext* HogQLParser::VarAssignmentContext::expression(size_t i) { - return getRuleContext(i); -} - -tree::TerminalNode* HogQLParser::VarAssignmentContext::COLON() { - return getToken(HogQLParser::COLON, 0); -} - -tree::TerminalNode* HogQLParser::VarAssignmentContext::EQ_SINGLE() { - return getToken(HogQLParser::EQ_SINGLE, 0); -} - -tree::TerminalNode* HogQLParser::VarAssignmentContext::SEMICOLON() { - return getToken(HogQLParser::SEMICOLON, 0); -} - - -size_t HogQLParser::VarAssignmentContext::getRuleIndex() const { - return HogQLParser::RuleVarAssignment; -} - - -std::any HogQLParser::VarAssignmentContext::accept(tree::ParseTreeVisitor *visitor) { - if (auto parserVisitor = dynamic_cast(visitor)) - return parserVisitor->visitVarAssignment(this); - else - return visitor->visitChildren(this); -} - -HogQLParser::VarAssignmentContext* HogQLParser::varAssignment() { - VarAssignmentContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 8, HogQLParser::RuleVarAssignment); - -#if __cplusplus > 201703L - auto onExit = finally([=, this] { -#else - auto onExit = finally([=] { -#endif - exitRule(); - }); - try { - enterOuterAlt(_localctx, 1); - setState(189); - expression(); - setState(190); - match(HogQLParser::COLON); - setState(191); - match(HogQLParser::EQ_SINGLE); - setState(192); - expression(); - setState(193); - match(HogQLParser::SEMICOLON); } catch (RecognitionException &e) { @@ -1092,7 +1013,7 @@ std::any HogQLParser::IdentifierListContext::accept(tree::ParseTreeVisitor *visi HogQLParser::IdentifierListContext* HogQLParser::identifierList() { IdentifierListContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 10, HogQLParser::RuleIdentifierList); + enterRule(_localctx, 8, HogQLParser::RuleIdentifierList); size_t _la = 0; #if __cplusplus > 201703L @@ -1104,17 +1025,17 @@ HogQLParser::IdentifierListContext* HogQLParser::identifierList() { }); try { enterOuterAlt(_localctx, 1); - setState(195); + setState(187); identifier(); - setState(200); + setState(192); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(196); + setState(188); match(HogQLParser::COMMA); - setState(197); + setState(189); identifier(); - setState(202); + setState(194); _errHandler->sync(this); _la = _input->LA(1); } @@ -1139,14 +1060,6 @@ HogQLParser::ReturnStmtContext* HogQLParser::StatementContext::returnStmt() { return getRuleContext(0); } -HogQLParser::EmptyStmtContext* HogQLParser::StatementContext::emptyStmt() { - return getRuleContext(0); -} - -HogQLParser::ExprStmtContext* HogQLParser::StatementContext::exprStmt() { - return getRuleContext(0); -} - HogQLParser::IfStmtContext* HogQLParser::StatementContext::ifStmt() { return getRuleContext(0); } @@ -1163,6 +1076,14 @@ HogQLParser::VarAssignmentContext* HogQLParser::StatementContext::varAssignment( return getRuleContext(0); } +HogQLParser::ExprStmtContext* HogQLParser::StatementContext::exprStmt() { + return getRuleContext(0); +} + +HogQLParser::EmptyStmtContext* HogQLParser::StatementContext::emptyStmt() { + return getRuleContext(0); +} + HogQLParser::BlockContext* HogQLParser::StatementContext::block() { return getRuleContext(0); } @@ -1182,7 +1103,7 @@ std::any HogQLParser::StatementContext::accept(tree::ParseTreeVisitor *visitor) HogQLParser::StatementContext* HogQLParser::statement() { StatementContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 12, HogQLParser::RuleStatement); + enterRule(_localctx, 10, HogQLParser::RuleStatement); #if __cplusplus > 201703L auto onExit = finally([=, this] { @@ -1192,68 +1113,61 @@ HogQLParser::StatementContext* HogQLParser::statement() { exitRule(); }); try { - setState(212); + setState(203); _errHandler->sync(this); switch (getInterpreter()->adaptivePredict(_input, 4, _ctx)) { case 1: { enterOuterAlt(_localctx, 1); - setState(203); + setState(195); returnStmt(); break; } case 2: { enterOuterAlt(_localctx, 2); - setState(204); - emptyStmt(); + setState(196); + ifStmt(); break; } case 3: { enterOuterAlt(_localctx, 3); - setState(205); - exprStmt(); + setState(197); + whileStmt(); break; } case 4: { enterOuterAlt(_localctx, 4); - setState(206); - ifStmt(); + setState(198); + funcStmt(); break; } case 5: { enterOuterAlt(_localctx, 5); - setState(207); - whileStmt(); + setState(199); + varAssignment(); break; } case 6: { enterOuterAlt(_localctx, 6); - setState(208); - funcStmt(); + setState(200); + exprStmt(); break; } case 7: { enterOuterAlt(_localctx, 7); - setState(209); - varAssignment(); + setState(201); + emptyStmt(); break; } case 8: { enterOuterAlt(_localctx, 8); - setState(210); - returnStmt(); - break; - } - - case 9: { - enterOuterAlt(_localctx, 9); - setState(211); + setState(202); block(); break; } @@ -1272,36 +1186,40 @@ HogQLParser::StatementContext* HogQLParser::statement() { return _localctx; } -//----------------- ExprStmtContext ------------------------------------------------------------------ +//----------------- ReturnStmtContext ------------------------------------------------------------------ -HogQLParser::ExprStmtContext::ExprStmtContext(ParserRuleContext *parent, size_t invokingState) +HogQLParser::ReturnStmtContext::ReturnStmtContext(ParserRuleContext *parent, size_t invokingState) : ParserRuleContext(parent, invokingState) { } -HogQLParser::ExpressionContext* HogQLParser::ExprStmtContext::expression() { +tree::TerminalNode* HogQLParser::ReturnStmtContext::RETURN() { + return getToken(HogQLParser::RETURN, 0); +} + +HogQLParser::ExpressionContext* HogQLParser::ReturnStmtContext::expression() { return getRuleContext(0); } -tree::TerminalNode* HogQLParser::ExprStmtContext::SEMICOLON() { +tree::TerminalNode* HogQLParser::ReturnStmtContext::SEMICOLON() { return getToken(HogQLParser::SEMICOLON, 0); } -size_t HogQLParser::ExprStmtContext::getRuleIndex() const { - return HogQLParser::RuleExprStmt; +size_t HogQLParser::ReturnStmtContext::getRuleIndex() const { + return HogQLParser::RuleReturnStmt; } -std::any HogQLParser::ExprStmtContext::accept(tree::ParseTreeVisitor *visitor) { +std::any HogQLParser::ReturnStmtContext::accept(tree::ParseTreeVisitor *visitor) { if (auto parserVisitor = dynamic_cast(visitor)) - return parserVisitor->visitExprStmt(this); + return parserVisitor->visitReturnStmt(this); else return visitor->visitChildren(this); } -HogQLParser::ExprStmtContext* HogQLParser::exprStmt() { - ExprStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 14, HogQLParser::RuleExprStmt); +HogQLParser::ReturnStmtContext* HogQLParser::returnStmt() { + ReturnStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); + enterRule(_localctx, 12, HogQLParser::RuleReturnStmt); #if __cplusplus > 201703L auto onExit = finally([=, this] { @@ -1312,10 +1230,34 @@ HogQLParser::ExprStmtContext* HogQLParser::exprStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(214); - expression(); - setState(215); - match(HogQLParser::SEMICOLON); + setState(205); + match(HogQLParser::RETURN); + setState(207); + _errHandler->sync(this); + + switch (getInterpreter()->adaptivePredict(_input, 5, _ctx)) { + case 1: { + setState(206); + expression(); + break; + } + + default: + break; + } + setState(210); + _errHandler->sync(this); + + switch (getInterpreter()->adaptivePredict(_input, 6, _ctx)) { + case 1: { + setState(209); + match(HogQLParser::SEMICOLON); + break; + } + + default: + break; + } } catch (RecognitionException &e) { @@ -1376,7 +1318,7 @@ std::any HogQLParser::IfStmtContext::accept(tree::ParseTreeVisitor *visitor) { HogQLParser::IfStmtContext* HogQLParser::ifStmt() { IfStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 16, HogQLParser::RuleIfStmt); + enterRule(_localctx, 14, HogQLParser::RuleIfStmt); #if __cplusplus > 201703L auto onExit = finally([=, this] { @@ -1387,24 +1329,24 @@ HogQLParser::IfStmtContext* HogQLParser::ifStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(217); + setState(212); match(HogQLParser::IF); - setState(218); + setState(213); match(HogQLParser::LPAREN); - setState(219); + setState(214); expression(); - setState(220); + setState(215); match(HogQLParser::RPAREN); - setState(221); + setState(216); statement(); - setState(224); + setState(219); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 5, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 7, _ctx)) { case 1: { - setState(222); + setState(217); match(HogQLParser::ELSE); - setState(223); + setState(218); statement(); break; } @@ -1449,6 +1391,10 @@ HogQLParser::StatementContext* HogQLParser::WhileStmtContext::statement() { return getRuleContext(0); } +tree::TerminalNode* HogQLParser::WhileStmtContext::SEMICOLON() { + return getToken(HogQLParser::SEMICOLON, 0); +} + size_t HogQLParser::WhileStmtContext::getRuleIndex() const { return HogQLParser::RuleWhileStmt; @@ -1464,7 +1410,7 @@ std::any HogQLParser::WhileStmtContext::accept(tree::ParseTreeVisitor *visitor) HogQLParser::WhileStmtContext* HogQLParser::whileStmt() { WhileStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 18, HogQLParser::RuleWhileStmt); + enterRule(_localctx, 16, HogQLParser::RuleWhileStmt); #if __cplusplus > 201703L auto onExit = finally([=, this] { @@ -1475,16 +1421,29 @@ HogQLParser::WhileStmtContext* HogQLParser::whileStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(226); + setState(221); match(HogQLParser::WHILE); - setState(227); + setState(222); match(HogQLParser::LPAREN); - setState(228); + setState(223); expression(); - setState(229); + setState(224); match(HogQLParser::RPAREN); - setState(230); + setState(225); statement(); + setState(227); + _errHandler->sync(this); + + switch (getInterpreter()->adaptivePredict(_input, 8, _ctx)) { + case 1: { + setState(226); + match(HogQLParser::SEMICOLON); + break; + } + + default: + break; + } } catch (RecognitionException &e) { @@ -1496,40 +1455,53 @@ HogQLParser::WhileStmtContext* HogQLParser::whileStmt() { return _localctx; } -//----------------- ReturnStmtContext ------------------------------------------------------------------ +//----------------- FuncStmtContext ------------------------------------------------------------------ -HogQLParser::ReturnStmtContext::ReturnStmtContext(ParserRuleContext *parent, size_t invokingState) +HogQLParser::FuncStmtContext::FuncStmtContext(ParserRuleContext *parent, size_t invokingState) : ParserRuleContext(parent, invokingState) { } -tree::TerminalNode* HogQLParser::ReturnStmtContext::RETURN() { - return getToken(HogQLParser::RETURN, 0); +tree::TerminalNode* HogQLParser::FuncStmtContext::FN() { + return getToken(HogQLParser::FN, 0); } -HogQLParser::ExpressionContext* HogQLParser::ReturnStmtContext::expression() { - return getRuleContext(0); +HogQLParser::IdentifierContext* HogQLParser::FuncStmtContext::identifier() { + return getRuleContext(0); } -tree::TerminalNode* HogQLParser::ReturnStmtContext::SEMICOLON() { - return getToken(HogQLParser::SEMICOLON, 0); +tree::TerminalNode* HogQLParser::FuncStmtContext::LPAREN() { + return getToken(HogQLParser::LPAREN, 0); } +tree::TerminalNode* HogQLParser::FuncStmtContext::RPAREN() { + return getToken(HogQLParser::RPAREN, 0); +} -size_t HogQLParser::ReturnStmtContext::getRuleIndex() const { - return HogQLParser::RuleReturnStmt; +HogQLParser::BlockContext* HogQLParser::FuncStmtContext::block() { + return getRuleContext(0); } +HogQLParser::IdentifierListContext* HogQLParser::FuncStmtContext::identifierList() { + return getRuleContext(0); +} -std::any HogQLParser::ReturnStmtContext::accept(tree::ParseTreeVisitor *visitor) { + +size_t HogQLParser::FuncStmtContext::getRuleIndex() const { + return HogQLParser::RuleFuncStmt; +} + + +std::any HogQLParser::FuncStmtContext::accept(tree::ParseTreeVisitor *visitor) { if (auto parserVisitor = dynamic_cast(visitor)) - return parserVisitor->visitReturnStmt(this); + return parserVisitor->visitFuncStmt(this); else return visitor->visitChildren(this); } -HogQLParser::ReturnStmtContext* HogQLParser::returnStmt() { - ReturnStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 20, HogQLParser::RuleReturnStmt); +HogQLParser::FuncStmtContext* HogQLParser::funcStmt() { + FuncStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); + enterRule(_localctx, 18, HogQLParser::RuleFuncStmt); + size_t _la = 0; #if __cplusplus > 201703L auto onExit = finally([=, this] { @@ -1540,12 +1512,26 @@ HogQLParser::ReturnStmtContext* HogQLParser::returnStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(232); - match(HogQLParser::RETURN); + setState(229); + match(HogQLParser::FN); + setState(230); + identifier(); + setState(231); + match(HogQLParser::LPAREN); setState(233); - expression(); - setState(234); - match(HogQLParser::SEMICOLON); + _errHandler->sync(this); + + _la = _input->LA(1); + if ((((_la & ~ 0x3fULL) == 0) && + ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && + ((1ULL << (_la - 64)) & 201863462911) != 0)) { + setState(232); + identifierList(); + } + setState(235); + match(HogQLParser::RPAREN); + setState(236); + block(); } catch (RecognitionException &e) { @@ -1557,53 +1543,103 @@ HogQLParser::ReturnStmtContext* HogQLParser::returnStmt() { return _localctx; } -//----------------- FuncStmtContext ------------------------------------------------------------------ +//----------------- VarAssignmentContext ------------------------------------------------------------------ -HogQLParser::FuncStmtContext::FuncStmtContext(ParserRuleContext *parent, size_t invokingState) +HogQLParser::VarAssignmentContext::VarAssignmentContext(ParserRuleContext *parent, size_t invokingState) : ParserRuleContext(parent, invokingState) { } -tree::TerminalNode* HogQLParser::FuncStmtContext::FN() { - return getToken(HogQLParser::FN, 0); +std::vector HogQLParser::VarAssignmentContext::expression() { + return getRuleContexts(); } -HogQLParser::IdentifierContext* HogQLParser::FuncStmtContext::identifier() { - return getRuleContext(0); +HogQLParser::ExpressionContext* HogQLParser::VarAssignmentContext::expression(size_t i) { + return getRuleContext(i); } -tree::TerminalNode* HogQLParser::FuncStmtContext::LPAREN() { - return getToken(HogQLParser::LPAREN, 0); +tree::TerminalNode* HogQLParser::VarAssignmentContext::COLON() { + return getToken(HogQLParser::COLON, 0); } -tree::TerminalNode* HogQLParser::FuncStmtContext::RPAREN() { - return getToken(HogQLParser::RPAREN, 0); +tree::TerminalNode* HogQLParser::VarAssignmentContext::EQ_SINGLE() { + return getToken(HogQLParser::EQ_SINGLE, 0); } -HogQLParser::BlockContext* HogQLParser::FuncStmtContext::block() { - return getRuleContext(0); + +size_t HogQLParser::VarAssignmentContext::getRuleIndex() const { + return HogQLParser::RuleVarAssignment; } -HogQLParser::IdentifierListContext* HogQLParser::FuncStmtContext::identifierList() { - return getRuleContext(0); + +std::any HogQLParser::VarAssignmentContext::accept(tree::ParseTreeVisitor *visitor) { + if (auto parserVisitor = dynamic_cast(visitor)) + return parserVisitor->visitVarAssignment(this); + else + return visitor->visitChildren(this); } +HogQLParser::VarAssignmentContext* HogQLParser::varAssignment() { + VarAssignmentContext *_localctx = _tracker.createInstance(_ctx, getState()); + enterRule(_localctx, 20, HogQLParser::RuleVarAssignment); -size_t HogQLParser::FuncStmtContext::getRuleIndex() const { - return HogQLParser::RuleFuncStmt; +#if __cplusplus > 201703L + auto onExit = finally([=, this] { +#else + auto onExit = finally([=] { +#endif + exitRule(); + }); + try { + enterOuterAlt(_localctx, 1); + setState(238); + expression(); + setState(239); + match(HogQLParser::COLON); + setState(240); + match(HogQLParser::EQ_SINGLE); + setState(241); + expression(); + + } + catch (RecognitionException &e) { + _errHandler->reportError(this, e); + _localctx->exception = std::current_exception(); + _errHandler->recover(this, _localctx->exception); + } + + return _localctx; } +//----------------- ExprStmtContext ------------------------------------------------------------------ -std::any HogQLParser::FuncStmtContext::accept(tree::ParseTreeVisitor *visitor) { +HogQLParser::ExprStmtContext::ExprStmtContext(ParserRuleContext *parent, size_t invokingState) + : ParserRuleContext(parent, invokingState) { +} + +HogQLParser::ExpressionContext* HogQLParser::ExprStmtContext::expression() { + return getRuleContext(0); +} + +tree::TerminalNode* HogQLParser::ExprStmtContext::SEMICOLON() { + return getToken(HogQLParser::SEMICOLON, 0); +} + + +size_t HogQLParser::ExprStmtContext::getRuleIndex() const { + return HogQLParser::RuleExprStmt; +} + + +std::any HogQLParser::ExprStmtContext::accept(tree::ParseTreeVisitor *visitor) { if (auto parserVisitor = dynamic_cast(visitor)) - return parserVisitor->visitFuncStmt(this); + return parserVisitor->visitExprStmt(this); else return visitor->visitChildren(this); } -HogQLParser::FuncStmtContext* HogQLParser::funcStmt() { - FuncStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); - enterRule(_localctx, 22, HogQLParser::RuleFuncStmt); - size_t _la = 0; +HogQLParser::ExprStmtContext* HogQLParser::exprStmt() { + ExprStmtContext *_localctx = _tracker.createInstance(_ctx, getState()); + enterRule(_localctx, 22, HogQLParser::RuleExprStmt); #if __cplusplus > 201703L auto onExit = finally([=, this] { @@ -1614,26 +1650,21 @@ HogQLParser::FuncStmtContext* HogQLParser::funcStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(236); - match(HogQLParser::FN); - setState(237); - identifier(); - setState(238); - match(HogQLParser::LPAREN); - setState(240); + setState(243); + expression(); + setState(245); _errHandler->sync(this); - _la = _input->LA(1); - if ((((_la & ~ 0x3fULL) == 0) && - ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && - ((1ULL << (_la - 64)) & 201863462911) != 0)) { - setState(239); - identifierList(); + switch (getInterpreter()->adaptivePredict(_input, 10, _ctx)) { + case 1: { + setState(244); + match(HogQLParser::SEMICOLON); + break; + } + + default: + break; } - setState(242); - match(HogQLParser::RPAREN); - setState(243); - block(); } catch (RecognitionException &e) { @@ -1681,7 +1712,7 @@ HogQLParser::EmptyStmtContext* HogQLParser::emptyStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(245); + setState(247); match(HogQLParser::SEMICOLON); } @@ -1743,22 +1774,22 @@ HogQLParser::BlockContext* HogQLParser::block() { }); try { enterOuterAlt(_localctx, 1); - setState(247); + setState(249); match(HogQLParser::LBRACE); - setState(251); + setState(253); _errHandler->sync(this); _la = _input->LA(1); while ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -2) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106351341731839) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 131649) != 0)) { - setState(248); + setState(250); declaration(); - setState(253); + setState(255); _errHandler->sync(this); _la = _input->LA(1); } - setState(254); + setState(256); match(HogQLParser::RBRACE); } @@ -1815,11 +1846,11 @@ HogQLParser::KvPairContext* HogQLParser::kvPair() { }); try { enterOuterAlt(_localctx, 1); - setState(256); + setState(258); expression(); - setState(257); + setState(259); match(HogQLParser::COLON); - setState(258); + setState(260); expression(); } @@ -1881,17 +1912,17 @@ HogQLParser::KvPairListContext* HogQLParser::kvPairList() { }); try { enterOuterAlt(_localctx, 1); - setState(260); + setState(262); kvPair(); - setState(265); + setState(267); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(261); + setState(263); match(HogQLParser::COMMA); - setState(262); + setState(264); kvPair(); - setState(267); + setState(269); _errHandler->sync(this); _la = _input->LA(1); } @@ -1954,23 +1985,23 @@ HogQLParser::SelectContext* HogQLParser::select() { }); try { enterOuterAlt(_localctx, 1); - setState(271); + setState(273); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 9, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 13, _ctx)) { case 1: { - setState(268); + setState(270); selectUnionStmt(); break; } case 2: { - setState(269); + setState(271); selectStmt(); break; } case 3: { - setState(270); + setState(272); hogqlxTagElement(); break; } @@ -1978,7 +2009,7 @@ HogQLParser::SelectContext* HogQLParser::select() { default: break; } - setState(273); + setState(275); match(HogQLParser::EOF); } @@ -2048,19 +2079,19 @@ HogQLParser::SelectUnionStmtContext* HogQLParser::selectUnionStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(275); + setState(277); selectStmtWithParens(); - setState(281); + setState(283); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::UNION) { - setState(276); + setState(278); match(HogQLParser::UNION); - setState(277); + setState(279); match(HogQLParser::ALL); - setState(278); + setState(280); selectStmtWithParens(); - setState(283); + setState(285); _errHandler->sync(this); _la = _input->LA(1); } @@ -2126,31 +2157,31 @@ HogQLParser::SelectStmtWithParensContext* HogQLParser::selectStmtWithParens() { exitRule(); }); try { - setState(290); + setState(292); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::SELECT: case HogQLParser::WITH: { enterOuterAlt(_localctx, 1); - setState(284); + setState(286); selectStmt(); break; } case HogQLParser::LPAREN: { enterOuterAlt(_localctx, 2); - setState(285); + setState(287); match(HogQLParser::LPAREN); - setState(286); + setState(288); selectUnionStmt(); - setState(287); + setState(289); match(HogQLParser::RPAREN); break; } case HogQLParser::LBRACE: { enterOuterAlt(_localctx, 3); - setState(289); + setState(291); placeholder(); break; } @@ -2286,22 +2317,22 @@ HogQLParser::SelectStmtContext* HogQLParser::selectStmt() { }); try { enterOuterAlt(_localctx, 1); - setState(293); + setState(295); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::WITH) { - setState(292); + setState(294); antlrcpp::downCast(_localctx)->with = withClause(); } - setState(295); - match(HogQLParser::SELECT); setState(297); + match(HogQLParser::SELECT); + setState(299); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 13, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 17, _ctx)) { case 1: { - setState(296); + setState(298); match(HogQLParser::DISTINCT); break; } @@ -2309,12 +2340,12 @@ HogQLParser::SelectStmtContext* HogQLParser::selectStmt() { default: break; } - setState(300); + setState(302); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 14, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 18, _ctx)) { case 1: { - setState(299); + setState(301); topClause(); break; } @@ -2322,57 +2353,57 @@ HogQLParser::SelectStmtContext* HogQLParser::selectStmt() { default: break; } - setState(302); - antlrcpp::downCast(_localctx)->columns = columnExprList(); setState(304); + antlrcpp::downCast(_localctx)->columns = columnExprList(); + setState(306); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::FROM) { - setState(303); + setState(305); antlrcpp::downCast(_localctx)->from = fromClause(); } - setState(307); + setState(309); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 567347999932448) != 0)) { - setState(306); + setState(308); arrayJoinClause(); } - setState(310); + setState(312); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::PREWHERE) { - setState(309); + setState(311); prewhereClause(); } - setState(313); + setState(315); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::WHERE) { - setState(312); + setState(314); antlrcpp::downCast(_localctx)->where = whereClause(); } - setState(316); + setState(318); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::GROUP) { - setState(315); + setState(317); groupByClause(); } - setState(320); + setState(322); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 20, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 24, _ctx)) { case 1: { - setState(318); + setState(320); match(HogQLParser::WITH); - setState(319); + setState(321); _la = _input->LA(1); if (!(_la == HogQLParser::CUBE @@ -2389,51 +2420,51 @@ HogQLParser::SelectStmtContext* HogQLParser::selectStmt() { default: break; } - setState(324); + setState(326); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::WITH) { - setState(322); + setState(324); match(HogQLParser::WITH); - setState(323); + setState(325); match(HogQLParser::TOTALS); } - setState(327); + setState(329); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::HAVING) { - setState(326); + setState(328); havingClause(); } - setState(330); + setState(332); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::WINDOW) { - setState(329); + setState(331); windowClause(); } - setState(333); + setState(335); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::ORDER) { - setState(332); + setState(334); orderByClause(); } - setState(337); + setState(339); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::LIMIT: { - setState(335); + setState(337); limitAndOffsetClause(); break; } case HogQLParser::OFFSET: { - setState(336); + setState(338); offsetOnlyClause(); break; } @@ -2448,12 +2479,12 @@ HogQLParser::SelectStmtContext* HogQLParser::selectStmt() { default: break; } - setState(340); + setState(342); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::SETTINGS) { - setState(339); + setState(341); settingsClause(); } @@ -2507,9 +2538,9 @@ HogQLParser::WithClauseContext* HogQLParser::withClause() { }); try { enterOuterAlt(_localctx, 1); - setState(342); + setState(344); match(HogQLParser::WITH); - setState(343); + setState(345); withExprList(); } @@ -2570,18 +2601,18 @@ HogQLParser::TopClauseContext* HogQLParser::topClause() { }); try { enterOuterAlt(_localctx, 1); - setState(345); + setState(347); match(HogQLParser::TOP); - setState(346); + setState(348); match(HogQLParser::DECIMAL_LITERAL); - setState(349); + setState(351); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 27, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 31, _ctx)) { case 1: { - setState(347); + setState(349); match(HogQLParser::WITH); - setState(348); + setState(350); match(HogQLParser::TIES); break; } @@ -2640,9 +2671,9 @@ HogQLParser::FromClauseContext* HogQLParser::fromClause() { }); try { enterOuterAlt(_localctx, 1); - setState(351); + setState(353); match(HogQLParser::FROM); - setState(352); + setState(354); joinExpr(0); } @@ -2708,14 +2739,14 @@ HogQLParser::ArrayJoinClauseContext* HogQLParser::arrayJoinClause() { }); try { enterOuterAlt(_localctx, 1); - setState(355); + setState(357); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::INNER || _la == HogQLParser::LEFT) { - setState(354); + setState(356); _la = _input->LA(1); if (!(_la == HogQLParser::INNER @@ -2727,11 +2758,11 @@ HogQLParser::ArrayJoinClauseContext* HogQLParser::arrayJoinClause() { consume(); } } - setState(357); + setState(359); match(HogQLParser::ARRAY); - setState(358); + setState(360); match(HogQLParser::JOIN); - setState(359); + setState(361); columnExprList(); } @@ -2829,35 +2860,35 @@ HogQLParser::WindowClauseContext* HogQLParser::windowClause() { }); try { enterOuterAlt(_localctx, 1); - setState(361); + setState(363); match(HogQLParser::WINDOW); - setState(362); + setState(364); identifier(); - setState(363); + setState(365); match(HogQLParser::AS); - setState(364); + setState(366); match(HogQLParser::LPAREN); - setState(365); + setState(367); windowExpr(); - setState(366); + setState(368); match(HogQLParser::RPAREN); - setState(376); + setState(378); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(367); + setState(369); match(HogQLParser::COMMA); - setState(368); + setState(370); identifier(); - setState(369); + setState(371); match(HogQLParser::AS); - setState(370); + setState(372); match(HogQLParser::LPAREN); - setState(371); + setState(373); windowExpr(); - setState(372); + setState(374); match(HogQLParser::RPAREN); - setState(378); + setState(380); _errHandler->sync(this); _la = _input->LA(1); } @@ -2912,9 +2943,9 @@ HogQLParser::PrewhereClauseContext* HogQLParser::prewhereClause() { }); try { enterOuterAlt(_localctx, 1); - setState(379); + setState(381); match(HogQLParser::PREWHERE); - setState(380); + setState(382); columnExpr(0); } @@ -2967,9 +2998,9 @@ HogQLParser::WhereClauseContext* HogQLParser::whereClause() { }); try { enterOuterAlt(_localctx, 1); - setState(382); + setState(384); match(HogQLParser::WHERE); - setState(383); + setState(385); columnExpr(0); } @@ -3043,15 +3074,15 @@ HogQLParser::GroupByClauseContext* HogQLParser::groupByClause() { }); try { enterOuterAlt(_localctx, 1); - setState(385); + setState(387); match(HogQLParser::GROUP); - setState(386); + setState(388); match(HogQLParser::BY); - setState(393); + setState(395); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 30, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 34, _ctx)) { case 1: { - setState(387); + setState(389); _la = _input->LA(1); if (!(_la == HogQLParser::CUBE @@ -3062,17 +3093,17 @@ HogQLParser::GroupByClauseContext* HogQLParser::groupByClause() { _errHandler->reportMatch(this); consume(); } - setState(388); + setState(390); match(HogQLParser::LPAREN); - setState(389); + setState(391); columnExprList(); - setState(390); + setState(392); match(HogQLParser::RPAREN); break; } case 2: { - setState(392); + setState(394); columnExprList(); break; } @@ -3131,9 +3162,9 @@ HogQLParser::HavingClauseContext* HogQLParser::havingClause() { }); try { enterOuterAlt(_localctx, 1); - setState(395); + setState(397); match(HogQLParser::HAVING); - setState(396); + setState(398); columnExpr(0); } @@ -3190,11 +3221,11 @@ HogQLParser::OrderByClauseContext* HogQLParser::orderByClause() { }); try { enterOuterAlt(_localctx, 1); - setState(398); + setState(400); match(HogQLParser::ORDER); - setState(399); + setState(401); match(HogQLParser::BY); - setState(400); + setState(402); orderExprList(); } @@ -3251,11 +3282,11 @@ HogQLParser::ProjectionOrderByClauseContext* HogQLParser::projectionOrderByClaus }); try { enterOuterAlt(_localctx, 1); - setState(402); + setState(404); match(HogQLParser::ORDER); - setState(403); + setState(405); match(HogQLParser::BY); - setState(404); + setState(406); columnExprList(); } @@ -3336,40 +3367,40 @@ HogQLParser::LimitAndOffsetClauseContext* HogQLParser::limitAndOffsetClause() { exitRule(); }); try { - setState(435); + setState(437); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 35, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 39, _ctx)) { case 1: { enterOuterAlt(_localctx, 1); - setState(406); + setState(408); match(HogQLParser::LIMIT); - setState(407); + setState(409); columnExpr(0); - setState(410); + setState(412); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::COMMA) { - setState(408); + setState(410); match(HogQLParser::COMMA); - setState(409); + setState(411); columnExpr(0); } - setState(416); + setState(418); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::WITH: { - setState(412); + setState(414); match(HogQLParser::WITH); - setState(413); + setState(415); match(HogQLParser::TIES); break; } case HogQLParser::BY: { - setState(414); + setState(416); match(HogQLParser::BY); - setState(415); + setState(417); columnExprList(); break; } @@ -3389,45 +3420,45 @@ HogQLParser::LimitAndOffsetClauseContext* HogQLParser::limitAndOffsetClause() { case 2: { enterOuterAlt(_localctx, 2); - setState(418); + setState(420); match(HogQLParser::LIMIT); - setState(419); + setState(421); columnExpr(0); - setState(422); + setState(424); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::WITH) { - setState(420); + setState(422); match(HogQLParser::WITH); - setState(421); + setState(423); match(HogQLParser::TIES); } - setState(424); + setState(426); match(HogQLParser::OFFSET); - setState(425); + setState(427); columnExpr(0); break; } case 3: { enterOuterAlt(_localctx, 3); - setState(427); + setState(429); match(HogQLParser::LIMIT); - setState(428); + setState(430); columnExpr(0); - setState(429); + setState(431); match(HogQLParser::OFFSET); - setState(430); + setState(432); columnExpr(0); - setState(433); + setState(435); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::BY) { - setState(431); + setState(433); match(HogQLParser::BY); - setState(432); + setState(434); columnExprList(); } break; @@ -3487,9 +3518,9 @@ HogQLParser::OffsetOnlyClauseContext* HogQLParser::offsetOnlyClause() { }); try { enterOuterAlt(_localctx, 1); - setState(437); + setState(439); match(HogQLParser::OFFSET); - setState(438); + setState(440); columnExpr(0); } @@ -3542,9 +3573,9 @@ HogQLParser::SettingsClauseContext* HogQLParser::settingsClause() { }); try { enterOuterAlt(_localctx, 1); - setState(440); + setState(442); match(HogQLParser::SETTINGS); - setState(441); + setState(443); settingExprList(); } @@ -3698,22 +3729,22 @@ HogQLParser::JoinExprContext* HogQLParser::joinExpr(int precedence) { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(455); + setState(457); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 38, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 42, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(444); - tableExpr(0); setState(446); + tableExpr(0); + setState(448); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 36, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 40, _ctx)) { case 1: { - setState(445); + setState(447); match(HogQLParser::FINAL); break; } @@ -3721,12 +3752,12 @@ HogQLParser::JoinExprContext* HogQLParser::joinExpr(int precedence) { default: break; } - setState(449); + setState(451); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 37, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 41, _ctx)) { case 1: { - setState(448); + setState(450); sampleClause(); break; } @@ -3741,11 +3772,11 @@ HogQLParser::JoinExprContext* HogQLParser::joinExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(451); + setState(453); match(HogQLParser::LPAREN); - setState(452); + setState(454); joinExpr(0); - setState(453); + setState(455); match(HogQLParser::RPAREN); break; } @@ -3754,27 +3785,27 @@ HogQLParser::JoinExprContext* HogQLParser::joinExpr(int precedence) { break; } _ctx->stop = _input->LT(-1); - setState(471); + setState(473); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 41, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 45, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { if (!_parseListeners.empty()) triggerExitRuleEvent(); previousContext = _localctx; - setState(469); + setState(471); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 40, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 44, _ctx)) { case 1: { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleJoinExpr); - setState(457); + setState(459); if (!(precpred(_ctx, 3))) throw FailedPredicateException(this, "precpred(_ctx, 3)"); - setState(458); + setState(460); joinOpCross(); - setState(459); + setState(461); joinExpr(4); break; } @@ -3783,10 +3814,10 @@ HogQLParser::JoinExprContext* HogQLParser::joinExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleJoinExpr); - setState(461); + setState(463); if (!(precpred(_ctx, 4))) throw FailedPredicateException(this, "precpred(_ctx, 4)"); - setState(463); + setState(465); _errHandler->sync(this); _la = _input->LA(1); @@ -3794,14 +3825,14 @@ HogQLParser::JoinExprContext* HogQLParser::joinExpr(int precedence) { ((1ULL << _la) & 567356589867290) != 0) || _la == HogQLParser::RIGHT || _la == HogQLParser::SEMI) { - setState(462); + setState(464); joinOp(); } - setState(465); + setState(467); match(HogQLParser::JOIN); - setState(466); + setState(468); joinExpr(0); - setState(467); + setState(469); joinConstraintClause(); break; } @@ -3810,9 +3841,9 @@ HogQLParser::JoinExprContext* HogQLParser::joinExpr(int precedence) { break; } } - setState(473); + setState(475); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 41, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 45, _ctx); } } catch (RecognitionException &e) { @@ -3948,23 +3979,23 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { exitRule(); }); try { - setState(517); + setState(519); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 55, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 59, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 1); - setState(483); + setState(485); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 44, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 48, _ctx)) { case 1: { - setState(475); + setState(477); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 274) != 0)) { - setState(474); + setState(476); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 274) != 0))) { @@ -3975,21 +4006,21 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { consume(); } } - setState(477); + setState(479); match(HogQLParser::INNER); break; } case 2: { - setState(478); - match(HogQLParser::INNER); setState(480); + match(HogQLParser::INNER); + setState(482); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 274) != 0)) { - setState(479); + setState(481); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 274) != 0))) { @@ -4004,7 +4035,7 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { } case 3: { - setState(482); + setState(484); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 274) != 0))) { @@ -4026,17 +4057,17 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { case 2: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 2); - setState(499); + setState(501); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 49, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 53, _ctx)) { case 1: { - setState(486); + setState(488); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 282) != 0) || _la == HogQLParser::SEMI) { - setState(485); + setState(487); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 282) != 0) || _la == HogQLParser::SEMI)) { @@ -4047,7 +4078,7 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { consume(); } } - setState(488); + setState(490); _la = _input->LA(1); if (!(_la == HogQLParser::LEFT @@ -4058,19 +4089,19 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { _errHandler->reportMatch(this); consume(); } - setState(490); + setState(492); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::OUTER) { - setState(489); + setState(491); match(HogQLParser::OUTER); } break; } case 2: { - setState(492); + setState(494); _la = _input->LA(1); if (!(_la == HogQLParser::LEFT @@ -4081,21 +4112,21 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { _errHandler->reportMatch(this); consume(); } - setState(494); + setState(496); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::OUTER) { - setState(493); + setState(495); match(HogQLParser::OUTER); } - setState(497); + setState(499); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 282) != 0) || _la == HogQLParser::SEMI) { - setState(496); + setState(498); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 282) != 0) || _la == HogQLParser::SEMI)) { @@ -4118,18 +4149,18 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { case 3: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 3); - setState(515); + setState(517); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 54, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 58, _ctx)) { case 1: { - setState(502); + setState(504); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::ALL || _la == HogQLParser::ANY) { - setState(501); + setState(503); _la = _input->LA(1); if (!(_la == HogQLParser::ALL @@ -4141,38 +4172,38 @@ HogQLParser::JoinOpContext* HogQLParser::joinOp() { consume(); } } - setState(504); - match(HogQLParser::FULL); setState(506); + match(HogQLParser::FULL); + setState(508); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::OUTER) { - setState(505); + setState(507); match(HogQLParser::OUTER); } break; } case 2: { - setState(508); - match(HogQLParser::FULL); setState(510); + match(HogQLParser::FULL); + setState(512); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::OUTER) { - setState(509); + setState(511); match(HogQLParser::OUTER); } - setState(513); + setState(515); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::ALL || _la == HogQLParser::ANY) { - setState(512); + setState(514); _la = _input->LA(1); if (!(_la == HogQLParser::ALL @@ -4250,21 +4281,21 @@ HogQLParser::JoinOpCrossContext* HogQLParser::joinOpCross() { exitRule(); }); try { - setState(522); + setState(524); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::CROSS: { enterOuterAlt(_localctx, 1); - setState(519); + setState(521); match(HogQLParser::CROSS); - setState(520); + setState(522); match(HogQLParser::JOIN); break; } case HogQLParser::COMMA: { enterOuterAlt(_localctx, 2); - setState(521); + setState(523); match(HogQLParser::COMMA); break; } @@ -4334,36 +4365,36 @@ HogQLParser::JoinConstraintClauseContext* HogQLParser::joinConstraintClause() { exitRule(); }); try { - setState(533); + setState(535); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 57, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 61, _ctx)) { case 1: { enterOuterAlt(_localctx, 1); - setState(524); + setState(526); match(HogQLParser::ON); - setState(525); + setState(527); columnExprList(); break; } case 2: { enterOuterAlt(_localctx, 2); - setState(526); + setState(528); match(HogQLParser::USING); - setState(527); + setState(529); match(HogQLParser::LPAREN); - setState(528); + setState(530); columnExprList(); - setState(529); + setState(531); match(HogQLParser::RPAREN); break; } case 3: { enterOuterAlt(_localctx, 3); - setState(531); + setState(533); match(HogQLParser::USING); - setState(532); + setState(534); columnExprList(); break; } @@ -4430,18 +4461,18 @@ HogQLParser::SampleClauseContext* HogQLParser::sampleClause() { }); try { enterOuterAlt(_localctx, 1); - setState(535); + setState(537); match(HogQLParser::SAMPLE); - setState(536); + setState(538); ratioExpr(); - setState(539); + setState(541); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 58, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 62, _ctx)) { case 1: { - setState(537); + setState(539); match(HogQLParser::OFFSET); - setState(538); + setState(540); ratioExpr(); break; } @@ -4509,17 +4540,17 @@ HogQLParser::OrderExprListContext* HogQLParser::orderExprList() { }); try { enterOuterAlt(_localctx, 1); - setState(541); + setState(543); orderExpr(); - setState(546); + setState(548); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(542); + setState(544); match(HogQLParser::COMMA); - setState(543); + setState(545); orderExpr(); - setState(548); + setState(550); _errHandler->sync(this); _la = _input->LA(1); } @@ -4603,15 +4634,15 @@ HogQLParser::OrderExprContext* HogQLParser::orderExpr() { }); try { enterOuterAlt(_localctx, 1); - setState(549); - columnExpr(0); setState(551); + columnExpr(0); + setState(553); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 6291584) != 0)) { - setState(550); + setState(552); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 6291584) != 0))) { @@ -4622,14 +4653,14 @@ HogQLParser::OrderExprContext* HogQLParser::orderExpr() { consume(); } } - setState(555); + setState(557); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NULLS) { - setState(553); + setState(555); match(HogQLParser::NULLS); - setState(554); + setState(556); _la = _input->LA(1); if (!(_la == HogQLParser::FIRST @@ -4641,14 +4672,14 @@ HogQLParser::OrderExprContext* HogQLParser::orderExpr() { consume(); } } - setState(559); + setState(561); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::COLLATE) { - setState(557); + setState(559); match(HogQLParser::COLLATE); - setState(558); + setState(560); match(HogQLParser::STRING_LITERAL); } @@ -4709,12 +4740,12 @@ HogQLParser::RatioExprContext* HogQLParser::ratioExpr() { exitRule(); }); try { - setState(567); + setState(569); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::LBRACE: { enterOuterAlt(_localctx, 1); - setState(561); + setState(563); placeholder(); break; } @@ -4729,16 +4760,16 @@ HogQLParser::RatioExprContext* HogQLParser::ratioExpr() { case HogQLParser::DOT: case HogQLParser::PLUS: { enterOuterAlt(_localctx, 2); - setState(562); + setState(564); numberLiteral(); - setState(565); + setState(567); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 63, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 67, _ctx)) { case 1: { - setState(563); + setState(565); match(HogQLParser::SLASH); - setState(564); + setState(566); numberLiteral(); break; } @@ -4812,17 +4843,17 @@ HogQLParser::SettingExprListContext* HogQLParser::settingExprList() { }); try { enterOuterAlt(_localctx, 1); - setState(569); + setState(571); settingExpr(); - setState(574); + setState(576); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(570); + setState(572); match(HogQLParser::COMMA); - setState(571); + setState(573); settingExpr(); - setState(576); + setState(578); _errHandler->sync(this); _la = _input->LA(1); } @@ -4881,11 +4912,11 @@ HogQLParser::SettingExprContext* HogQLParser::settingExpr() { }); try { enterOuterAlt(_localctx, 1); - setState(577); + setState(579); identifier(); - setState(578); + setState(580); match(HogQLParser::EQ_SINGLE); - setState(579); + setState(581); literal(); } @@ -4943,30 +4974,30 @@ HogQLParser::WindowExprContext* HogQLParser::windowExpr() { }); try { enterOuterAlt(_localctx, 1); - setState(582); + setState(584); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::PARTITION) { - setState(581); + setState(583); winPartitionByClause(); } - setState(585); + setState(587); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::ORDER) { - setState(584); + setState(586); winOrderByClause(); } - setState(588); + setState(590); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::RANGE || _la == HogQLParser::ROWS) { - setState(587); + setState(589); winFrameClause(); } @@ -5024,11 +5055,11 @@ HogQLParser::WinPartitionByClauseContext* HogQLParser::winPartitionByClause() { }); try { enterOuterAlt(_localctx, 1); - setState(590); + setState(592); match(HogQLParser::PARTITION); - setState(591); + setState(593); match(HogQLParser::BY); - setState(592); + setState(594); columnExprList(); } @@ -5085,11 +5116,11 @@ HogQLParser::WinOrderByClauseContext* HogQLParser::winOrderByClause() { }); try { enterOuterAlt(_localctx, 1); - setState(594); + setState(596); match(HogQLParser::ORDER); - setState(595); + setState(597); match(HogQLParser::BY); - setState(596); + setState(598); orderExprList(); } @@ -5147,7 +5178,7 @@ HogQLParser::WinFrameClauseContext* HogQLParser::winFrameClause() { }); try { enterOuterAlt(_localctx, 1); - setState(598); + setState(600); _la = _input->LA(1); if (!(_la == HogQLParser::RANGE @@ -5158,7 +5189,7 @@ HogQLParser::WinFrameClauseContext* HogQLParser::winFrameClause() { _errHandler->reportMatch(this); consume(); } - setState(599); + setState(601); winFrameExtend(); } @@ -5240,7 +5271,7 @@ HogQLParser::WinFrameExtendContext* HogQLParser::winFrameExtend() { exitRule(); }); try { - setState(607); + setState(609); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::CURRENT: @@ -5256,7 +5287,7 @@ HogQLParser::WinFrameExtendContext* HogQLParser::winFrameExtend() { case HogQLParser::PLUS: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 1); - setState(601); + setState(603); winFrameBound(); break; } @@ -5264,13 +5295,13 @@ HogQLParser::WinFrameExtendContext* HogQLParser::winFrameExtend() { case HogQLParser::BETWEEN: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 2); - setState(602); + setState(604); match(HogQLParser::BETWEEN); - setState(603); + setState(605); winFrameBound(); - setState(604); + setState(606); match(HogQLParser::AND); - setState(605); + setState(607); winFrameBound(); break; } @@ -5345,45 +5376,45 @@ HogQLParser::WinFrameBoundContext* HogQLParser::winFrameBound() { }); try { enterOuterAlt(_localctx, 1); - setState(621); + setState(623); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 70, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 74, _ctx)) { case 1: { - setState(609); + setState(611); match(HogQLParser::CURRENT); - setState(610); + setState(612); match(HogQLParser::ROW); break; } case 2: { - setState(611); + setState(613); match(HogQLParser::UNBOUNDED); - setState(612); + setState(614); match(HogQLParser::PRECEDING); break; } case 3: { - setState(613); + setState(615); match(HogQLParser::UNBOUNDED); - setState(614); + setState(616); match(HogQLParser::FOLLOWING); break; } case 4: { - setState(615); + setState(617); numberLiteral(); - setState(616); + setState(618); match(HogQLParser::PRECEDING); break; } case 5: { - setState(618); + setState(620); numberLiteral(); - setState(619); + setState(621); match(HogQLParser::FOLLOWING); break; } @@ -5442,9 +5473,9 @@ HogQLParser::ExprContext* HogQLParser::expr() { }); try { enterOuterAlt(_localctx, 1); - setState(623); + setState(625); columnExpr(0); - setState(624); + setState(626); match(HogQLParser::EOF); } @@ -5648,13 +5679,13 @@ HogQLParser::ColumnTypeExprContext* HogQLParser::columnTypeExpr() { exitRule(); }); try { - setState(673); + setState(675); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 75, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 79, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 1); - setState(626); + setState(628); identifier(); break; } @@ -5662,29 +5693,29 @@ HogQLParser::ColumnTypeExprContext* HogQLParser::columnTypeExpr() { case 2: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 2); - setState(627); - identifier(); - setState(628); - match(HogQLParser::LPAREN); setState(629); identifier(); setState(630); + match(HogQLParser::LPAREN); + setState(631); + identifier(); + setState(632); columnTypeExpr(); - setState(637); + setState(639); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(631); + setState(633); match(HogQLParser::COMMA); - setState(632); + setState(634); identifier(); - setState(633); + setState(635); columnTypeExpr(); - setState(639); + setState(641); _errHandler->sync(this); _la = _input->LA(1); } - setState(640); + setState(642); match(HogQLParser::RPAREN); break; } @@ -5692,25 +5723,25 @@ HogQLParser::ColumnTypeExprContext* HogQLParser::columnTypeExpr() { case 3: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 3); - setState(642); + setState(644); identifier(); - setState(643); + setState(645); match(HogQLParser::LPAREN); - setState(644); + setState(646); enumValue(); - setState(649); + setState(651); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(645); + setState(647); match(HogQLParser::COMMA); - setState(646); + setState(648); enumValue(); - setState(651); + setState(653); _errHandler->sync(this); _la = _input->LA(1); } - setState(652); + setState(654); match(HogQLParser::RPAREN); break; } @@ -5718,25 +5749,25 @@ HogQLParser::ColumnTypeExprContext* HogQLParser::columnTypeExpr() { case 4: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 4); - setState(654); + setState(656); identifier(); - setState(655); + setState(657); match(HogQLParser::LPAREN); - setState(656); + setState(658); columnTypeExpr(); - setState(661); + setState(663); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(657); + setState(659); match(HogQLParser::COMMA); - setState(658); + setState(660); columnTypeExpr(); - setState(663); + setState(665); _errHandler->sync(this); _la = _input->LA(1); } - setState(664); + setState(666); match(HogQLParser::RPAREN); break; } @@ -5744,11 +5775,11 @@ HogQLParser::ColumnTypeExprContext* HogQLParser::columnTypeExpr() { case 5: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 5); - setState(666); + setState(668); identifier(); - setState(667); - match(HogQLParser::LPAREN); setState(669); + match(HogQLParser::LPAREN); + setState(671); _errHandler->sync(this); _la = _input->LA(1); @@ -5756,10 +5787,10 @@ HogQLParser::ColumnTypeExprContext* HogQLParser::columnTypeExpr() { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(668); + setState(670); columnExprList(); } - setState(671); + setState(673); match(HogQLParser::RPAREN); break; } @@ -5827,21 +5858,21 @@ HogQLParser::ColumnExprListContext* HogQLParser::columnExprList() { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(675); + setState(677); columnExpr(0); - setState(680); + setState(682); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 76, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 80, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { - setState(676); + setState(678); match(HogQLParser::COMMA); - setState(677); + setState(679); columnExpr(0); } - setState(682); + setState(684); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 76, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 80, _ctx); } } @@ -5902,10 +5933,6 @@ HogQLParser::ColumnExprContext* HogQLParser::ColumnExprAliasContext::columnExpr( return getRuleContext(0); } -HogQLParser::AliasContext* HogQLParser::ColumnExprAliasContext::alias() { - return getRuleContext(0); -} - tree::TerminalNode* HogQLParser::ColumnExprAliasContext::AS() { return getToken(HogQLParser::AS, 0); } @@ -6916,22 +6943,22 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(832); + setState(834); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 96, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 100, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(684); - match(HogQLParser::CASE); setState(686); + match(HogQLParser::CASE); + setState(688); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 77, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 81, _ctx)) { case 1: { - setState(685); + setState(687); antlrcpp::downCast(_localctx)->caseExpr = columnExpr(0); break; } @@ -6939,33 +6966,33 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(693); + setState(695); _errHandler->sync(this); _la = _input->LA(1); do { - setState(688); + setState(690); match(HogQLParser::WHEN); - setState(689); + setState(691); antlrcpp::downCast(_localctx)->whenExpr = columnExpr(0); - setState(690); + setState(692); match(HogQLParser::THEN); - setState(691); + setState(693); antlrcpp::downCast(_localctx)->thenExpr = columnExpr(0); - setState(695); + setState(697); _errHandler->sync(this); _la = _input->LA(1); } while (_la == HogQLParser::WHEN); - setState(699); + setState(701); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::ELSE) { - setState(697); + setState(699); match(HogQLParser::ELSE); - setState(698); + setState(700); antlrcpp::downCast(_localctx)->elseExpr = columnExpr(0); } - setState(701); + setState(703); match(HogQLParser::END); break; } @@ -6974,17 +7001,17 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(703); + setState(705); match(HogQLParser::CAST); - setState(704); + setState(706); match(HogQLParser::LPAREN); - setState(705); + setState(707); columnExpr(0); - setState(706); + setState(708); match(HogQLParser::AS); - setState(707); + setState(709); columnTypeExpr(); - setState(708); + setState(710); match(HogQLParser::RPAREN); break; } @@ -6993,9 +7020,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(710); + setState(712); match(HogQLParser::DATE); - setState(711); + setState(713); match(HogQLParser::STRING_LITERAL); break; } @@ -7004,11 +7031,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(712); + setState(714); match(HogQLParser::INTERVAL); - setState(713); + setState(715); columnExpr(0); - setState(714); + setState(716); interval(); break; } @@ -7017,27 +7044,27 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(716); + setState(718); match(HogQLParser::SUBSTRING); - setState(717); + setState(719); match(HogQLParser::LPAREN); - setState(718); + setState(720); columnExpr(0); - setState(719); + setState(721); match(HogQLParser::FROM); - setState(720); + setState(722); columnExpr(0); - setState(723); + setState(725); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::FOR) { - setState(721); + setState(723); match(HogQLParser::FOR); - setState(722); + setState(724); columnExpr(0); } - setState(725); + setState(727); match(HogQLParser::RPAREN); break; } @@ -7046,9 +7073,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(727); + setState(729); match(HogQLParser::TIMESTAMP); - setState(728); + setState(730); match(HogQLParser::STRING_LITERAL); break; } @@ -7057,11 +7084,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(729); + setState(731); match(HogQLParser::TRIM); - setState(730); + setState(732); match(HogQLParser::LPAREN); - setState(731); + setState(733); _la = _input->LA(1); if (!(_la == HogQLParser::BOTH @@ -7072,13 +7099,13 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _errHandler->reportMatch(this); consume(); } - setState(732); + setState(734); string(); - setState(733); + setState(735); match(HogQLParser::FROM); - setState(734); + setState(736); columnExpr(0); - setState(735); + setState(737); match(HogQLParser::RPAREN); break; } @@ -7087,12 +7114,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(737); + setState(739); identifier(); - setState(738); - match(HogQLParser::LPAREN); setState(740); + match(HogQLParser::LPAREN); + setState(742); _errHandler->sync(this); _la = _input->LA(1); @@ -7100,24 +7127,24 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(739); + setState(741); columnExprList(); } - setState(742); + setState(744); match(HogQLParser::RPAREN); - setState(752); + setState(754); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::LPAREN) { - setState(744); - match(HogQLParser::LPAREN); setState(746); + match(HogQLParser::LPAREN); + setState(748); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 82, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 86, _ctx)) { case 1: { - setState(745); + setState(747); match(HogQLParser::DISTINCT); break; } @@ -7125,7 +7152,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(749); + setState(751); _errHandler->sync(this); _la = _input->LA(1); @@ -7133,19 +7160,19 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(748); + setState(750); columnArgList(); } - setState(751); + setState(753); match(HogQLParser::RPAREN); } - setState(754); + setState(756); match(HogQLParser::OVER); - setState(755); + setState(757); match(HogQLParser::LPAREN); - setState(756); + setState(758); windowExpr(); - setState(757); + setState(759); match(HogQLParser::RPAREN); break; } @@ -7154,12 +7181,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(759); + setState(761); identifier(); - setState(760); - match(HogQLParser::LPAREN); setState(762); + match(HogQLParser::LPAREN); + setState(764); _errHandler->sync(this); _la = _input->LA(1); @@ -7167,24 +7194,24 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(761); + setState(763); columnExprList(); } - setState(764); + setState(766); match(HogQLParser::RPAREN); - setState(774); + setState(776); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::LPAREN) { - setState(766); - match(HogQLParser::LPAREN); setState(768); + match(HogQLParser::LPAREN); + setState(770); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 86, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 90, _ctx)) { case 1: { - setState(767); + setState(769); match(HogQLParser::DISTINCT); break; } @@ -7192,7 +7219,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(771); + setState(773); _errHandler->sync(this); _la = _input->LA(1); @@ -7200,15 +7227,15 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(770); + setState(772); columnArgList(); } - setState(773); + setState(775); match(HogQLParser::RPAREN); } - setState(776); + setState(778); match(HogQLParser::OVER); - setState(777); + setState(779); identifier(); break; } @@ -7217,16 +7244,16 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(779); + setState(781); identifier(); - setState(785); + setState(787); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 90, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 94, _ctx)) { case 1: { - setState(780); - match(HogQLParser::LPAREN); setState(782); + match(HogQLParser::LPAREN); + setState(784); _errHandler->sync(this); _la = _input->LA(1); @@ -7234,10 +7261,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(781); + setState(783); columnExprList(); } - setState(784); + setState(786); match(HogQLParser::RPAREN); break; } @@ -7245,14 +7272,14 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(787); - match(HogQLParser::LPAREN); setState(789); + match(HogQLParser::LPAREN); + setState(791); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 91, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 95, _ctx)) { case 1: { - setState(788); + setState(790); match(HogQLParser::DISTINCT); break; } @@ -7260,7 +7287,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(792); + setState(794); _errHandler->sync(this); _la = _input->LA(1); @@ -7268,10 +7295,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(791); + setState(793); columnArgList(); } - setState(794); + setState(796); match(HogQLParser::RPAREN); break; } @@ -7280,7 +7307,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(796); + setState(798); hogqlxTagElement(); break; } @@ -7289,7 +7316,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(797); + setState(799); templateString(); break; } @@ -7298,7 +7325,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(798); + setState(800); literal(); break; } @@ -7307,9 +7334,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(799); + setState(801); match(HogQLParser::DASH); - setState(800); + setState(802); columnExpr(19); break; } @@ -7318,9 +7345,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(801); + setState(803); match(HogQLParser::NOT); - setState(802); + setState(804); columnExpr(13); break; } @@ -7329,19 +7356,19 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(806); + setState(808); _errHandler->sync(this); _la = _input->LA(1); if ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 201863462911) != 0)) { - setState(803); + setState(805); tableIdentifier(); - setState(804); + setState(806); match(HogQLParser::DOT); } - setState(808); + setState(810); match(HogQLParser::ASTERISK); break; } @@ -7350,11 +7377,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(809); + setState(811); match(HogQLParser::LPAREN); - setState(810); + setState(812); selectUnionStmt(); - setState(811); + setState(813); match(HogQLParser::RPAREN); break; } @@ -7363,11 +7390,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(813); + setState(815); match(HogQLParser::LPAREN); - setState(814); + setState(816); columnExpr(0); - setState(815); + setState(817); match(HogQLParser::RPAREN); break; } @@ -7376,11 +7403,11 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(817); + setState(819); match(HogQLParser::LPAREN); - setState(818); + setState(820); columnExprList(); - setState(819); + setState(821); match(HogQLParser::RPAREN); break; } @@ -7389,9 +7416,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(821); - match(HogQLParser::LBRACKET); setState(823); + match(HogQLParser::LBRACKET); + setState(825); _errHandler->sync(this); _la = _input->LA(1); @@ -7399,10 +7426,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(822); + setState(824); columnExprList(); } - setState(825); + setState(827); match(HogQLParser::RBRACKET); break; } @@ -7411,9 +7438,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(826); - match(HogQLParser::LBRACE); setState(828); + match(HogQLParser::LBRACE); + setState(830); _errHandler->sync(this); _la = _input->LA(1); @@ -7421,10 +7448,10 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(827); + setState(829); kvPairList(); } - setState(830); + setState(832); match(HogQLParser::RBRACE); break; } @@ -7433,7 +7460,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(831); + setState(833); columnIdentifier(); break; } @@ -7442,42 +7469,42 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { break; } _ctx->stop = _input->LT(-1); - setState(927); + setState(928); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 107, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 111, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { if (!_parseListeners.empty()) triggerExitRuleEvent(); previousContext = _localctx; - setState(925); + setState(926); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 106, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 110, _ctx)) { case 1: { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; newContext->left = previousContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(834); + setState(836); if (!(precpred(_ctx, 18))) throw FailedPredicateException(this, "precpred(_ctx, 18)"); - setState(838); + setState(840); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::ASTERISK: { - setState(835); + setState(837); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::ASTERISK); break; } case HogQLParser::SLASH: { - setState(836); + setState(838); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::SLASH); break; } case HogQLParser::PERCENT: { - setState(837); + setState(839); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::PERCENT); break; } @@ -7485,7 +7512,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: throw NoViableAltException(this); } - setState(840); + setState(842); antlrcpp::downCast(_localctx)->right = columnExpr(19); break; } @@ -7495,26 +7522,26 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = newContext; newContext->left = previousContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(841); + setState(843); if (!(precpred(_ctx, 17))) throw FailedPredicateException(this, "precpred(_ctx, 17)"); - setState(845); + setState(847); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::PLUS: { - setState(842); + setState(844); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::PLUS); break; } case HogQLParser::DASH: { - setState(843); + setState(845); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::DASH); break; } case HogQLParser::CONCAT: { - setState(844); + setState(846); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::CONCAT); break; } @@ -7522,7 +7549,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: throw NoViableAltException(this); } - setState(847); + setState(849); antlrcpp::downCast(_localctx)->right = columnExpr(18); break; } @@ -7532,71 +7559,71 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { _localctx = newContext; newContext->left = previousContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(848); + setState(850); if (!(precpred(_ctx, 16))) throw FailedPredicateException(this, "precpred(_ctx, 16)"); - setState(873); + setState(875); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 102, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 106, _ctx)) { case 1: { - setState(849); + setState(851); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::EQ_DOUBLE); break; } case 2: { - setState(850); + setState(852); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::EQ_SINGLE); break; } case 3: { - setState(851); + setState(853); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT_EQ); break; } case 4: { - setState(852); + setState(854); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::LT_EQ); break; } case 5: { - setState(853); + setState(855); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::LT); break; } case 6: { - setState(854); + setState(856); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::GT_EQ); break; } case 7: { - setState(855); + setState(857); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::GT); break; } case 8: { - setState(857); + setState(859); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(856); + setState(858); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT); } - setState(859); - match(HogQLParser::IN); setState(861); + match(HogQLParser::IN); + setState(863); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 100, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 104, _ctx)) { case 1: { - setState(860); + setState(862); match(HogQLParser::COHORT); break; } @@ -7608,15 +7635,15 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { } case 9: { - setState(864); + setState(866); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(863); + setState(865); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT); } - setState(866); + setState(868); _la = _input->LA(1); if (!(_la == HogQLParser::ILIKE @@ -7631,37 +7658,37 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { } case 10: { - setState(867); + setState(869); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::REGEX_SINGLE); break; } case 11: { - setState(868); + setState(870); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::REGEX_DOUBLE); break; } case 12: { - setState(869); + setState(871); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT_REGEX); break; } case 13: { - setState(870); + setState(872); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::IREGEX_SINGLE); break; } case 14: { - setState(871); + setState(873); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::IREGEX_DOUBLE); break; } case 15: { - setState(872); + setState(874); antlrcpp::downCast(_localctx)->operator_ = match(HogQLParser::NOT_IREGEX); break; } @@ -7669,7 +7696,7 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { default: break; } - setState(875); + setState(877); antlrcpp::downCast(_localctx)->right = columnExpr(17); break; } @@ -7678,12 +7705,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(876); + setState(878); if (!(precpred(_ctx, 14))) throw FailedPredicateException(this, "precpred(_ctx, 14)"); - setState(877); + setState(879); match(HogQLParser::NULLISH); - setState(878); + setState(880); columnExpr(15); break; } @@ -7692,12 +7719,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(879); + setState(881); if (!(precpred(_ctx, 12))) throw FailedPredicateException(this, "precpred(_ctx, 12)"); - setState(880); + setState(882); match(HogQLParser::AND); - setState(881); + setState(883); columnExpr(13); break; } @@ -7706,12 +7733,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(882); + setState(884); if (!(precpred(_ctx, 11))) throw FailedPredicateException(this, "precpred(_ctx, 11)"); - setState(883); + setState(885); match(HogQLParser::OR); - setState(884); + setState(886); columnExpr(12); break; } @@ -7720,24 +7747,24 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(885); + setState(887); if (!(precpred(_ctx, 10))) throw FailedPredicateException(this, "precpred(_ctx, 10)"); - setState(887); + setState(889); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(886); + setState(888); match(HogQLParser::NOT); } - setState(889); + setState(891); match(HogQLParser::BETWEEN); - setState(890); + setState(892); columnExpr(0); - setState(891); + setState(893); match(HogQLParser::AND); - setState(892); + setState(894); columnExpr(11); break; } @@ -7746,16 +7773,16 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(894); + setState(896); if (!(precpred(_ctx, 9))) throw FailedPredicateException(this, "precpred(_ctx, 9)"); - setState(895); + setState(897); match(HogQLParser::QUERY); - setState(896); + setState(898); columnExpr(0); - setState(897); + setState(899); match(HogQLParser::COLON); - setState(898); + setState(900); columnExpr(9); break; } @@ -7764,14 +7791,14 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(900); + setState(902); if (!(precpred(_ctx, 22))) throw FailedPredicateException(this, "precpred(_ctx, 22)"); - setState(901); + setState(903); match(HogQLParser::LBRACKET); - setState(902); + setState(904); columnExpr(0); - setState(903); + setState(905); match(HogQLParser::RBRACKET); break; } @@ -7780,12 +7807,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(905); + setState(907); if (!(precpred(_ctx, 21))) throw FailedPredicateException(this, "precpred(_ctx, 21)"); - setState(906); + setState(908); match(HogQLParser::DOT); - setState(907); + setState(909); match(HogQLParser::DECIMAL_LITERAL); break; } @@ -7794,12 +7821,12 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(908); + setState(910); if (!(precpred(_ctx, 20))) throw FailedPredicateException(this, "precpred(_ctx, 20)"); - setState(909); + setState(911); match(HogQLParser::DOT); - setState(910); + setState(912); identifier(); break; } @@ -7808,20 +7835,20 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(911); + setState(913); if (!(precpred(_ctx, 15))) throw FailedPredicateException(this, "precpred(_ctx, 15)"); - setState(912); - match(HogQLParser::IS); setState(914); + match(HogQLParser::IS); + setState(916); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::NOT) { - setState(913); + setState(915); match(HogQLParser::NOT); } - setState(916); + setState(918); match(HogQLParser::NULL_SQL); break; } @@ -7830,30 +7857,24 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleColumnExpr); - setState(917); + setState(919); if (!(precpred(_ctx, 8))) throw FailedPredicateException(this, "precpred(_ctx, 8)"); - setState(923); + setState(924); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 105, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 109, _ctx)) { case 1: { - setState(918); - alias(); - break; - } - - case 2: { - setState(919); - match(HogQLParser::AS); setState(920); + match(HogQLParser::AS); + setState(921); identifier(); break; } - case 3: { - setState(921); - match(HogQLParser::AS); + case 2: { setState(922); + match(HogQLParser::AS); + setState(923); match(HogQLParser::STRING_LITERAL); break; } @@ -7868,9 +7889,9 @@ HogQLParser::ColumnExprContext* HogQLParser::columnExpr(int precedence) { break; } } - setState(929); + setState(930); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 107, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 111, _ctx); } } catch (RecognitionException &e) { @@ -7930,17 +7951,17 @@ HogQLParser::ColumnArgListContext* HogQLParser::columnArgList() { }); try { enterOuterAlt(_localctx, 1); - setState(930); + setState(931); columnArgExpr(); - setState(935); + setState(936); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(931); - match(HogQLParser::COMMA); setState(932); + match(HogQLParser::COMMA); + setState(933); columnArgExpr(); - setState(937); + setState(938); _errHandler->sync(this); _la = _input->LA(1); } @@ -7994,19 +8015,19 @@ HogQLParser::ColumnArgExprContext* HogQLParser::columnArgExpr() { exitRule(); }); try { - setState(940); + setState(941); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 109, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 113, _ctx)) { case 1: { enterOuterAlt(_localctx, 1); - setState(938); + setState(939); columnLambdaExpr(); break; } case 2: { enterOuterAlt(_localctx, 2); - setState(939); + setState(940); columnExpr(0); break; } @@ -8090,27 +8111,27 @@ HogQLParser::ColumnLambdaExprContext* HogQLParser::columnLambdaExpr() { }); try { enterOuterAlt(_localctx, 1); - setState(961); + setState(962); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::LPAREN: { - setState(942); - match(HogQLParser::LPAREN); setState(943); + match(HogQLParser::LPAREN); + setState(944); identifier(); - setState(948); + setState(949); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(944); - match(HogQLParser::COMMA); setState(945); + match(HogQLParser::COMMA); + setState(946); identifier(); - setState(950); + setState(951); _errHandler->sync(this); _la = _input->LA(1); } - setState(951); + setState(952); match(HogQLParser::RPAREN); break; } @@ -8209,17 +8230,17 @@ HogQLParser::ColumnLambdaExprContext* HogQLParser::columnLambdaExpr() { case HogQLParser::WITH: case HogQLParser::YEAR: case HogQLParser::IDENTIFIER: { - setState(953); + setState(954); identifier(); - setState(958); + setState(959); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(954); - match(HogQLParser::COMMA); setState(955); + match(HogQLParser::COMMA); + setState(956); identifier(); - setState(960); + setState(961); _errHandler->sync(this); _la = _input->LA(1); } @@ -8229,9 +8250,9 @@ HogQLParser::ColumnLambdaExprContext* HogQLParser::columnLambdaExpr() { default: throw NoViableAltException(this); } - setState(963); - match(HogQLParser::ARROW); setState(964); + match(HogQLParser::ARROW); + setState(965); columnExpr(0); } @@ -8358,31 +8379,31 @@ HogQLParser::HogqlxTagElementContext* HogQLParser::hogqlxTagElement() { exitRule(); }); try { - setState(994); + setState(995); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 116, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 120, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 1); - setState(966); - match(HogQLParser::LT); setState(967); + match(HogQLParser::LT); + setState(968); identifier(); - setState(971); + setState(972); _errHandler->sync(this); _la = _input->LA(1); while ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 201863462911) != 0)) { - setState(968); + setState(969); hogqlxTagAttribute(); - setState(973); + setState(974); _errHandler->sync(this); _la = _input->LA(1); } - setState(974); - match(HogQLParser::SLASH); setState(975); + match(HogQLParser::SLASH); + setState(976); match(HogQLParser::GT); break; } @@ -8390,30 +8411,30 @@ HogQLParser::HogqlxTagElementContext* HogQLParser::hogqlxTagElement() { case 2: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 2); - setState(977); - match(HogQLParser::LT); setState(978); + match(HogQLParser::LT); + setState(979); identifier(); - setState(982); + setState(983); _errHandler->sync(this); _la = _input->LA(1); while ((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -181272084561788930) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 201863462911) != 0)) { - setState(979); + setState(980); hogqlxTagAttribute(); - setState(984); + setState(985); _errHandler->sync(this); _la = _input->LA(1); } - setState(985); + setState(986); match(HogQLParser::GT); - setState(987); + setState(988); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 115, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 119, _ctx)) { case 1: { - setState(986); + setState(987); hogqlxTagElement(); break; } @@ -8421,13 +8442,13 @@ HogQLParser::HogqlxTagElementContext* HogQLParser::hogqlxTagElement() { default: break; } - setState(989); - match(HogQLParser::LT); setState(990); - match(HogQLParser::SLASH); + match(HogQLParser::LT); setState(991); - identifier(); + match(HogQLParser::SLASH); setState(992); + identifier(); + setState(993); match(HogQLParser::GT); break; } @@ -8501,38 +8522,38 @@ HogQLParser::HogqlxTagAttributeContext* HogQLParser::hogqlxTagAttribute() { exitRule(); }); try { - setState(1007); + setState(1008); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 117, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 121, _ctx)) { case 1: { enterOuterAlt(_localctx, 1); - setState(996); - identifier(); setState(997); - match(HogQLParser::EQ_SINGLE); + identifier(); setState(998); + match(HogQLParser::EQ_SINGLE); + setState(999); string(); break; } case 2: { enterOuterAlt(_localctx, 2); - setState(1000); - identifier(); setState(1001); - match(HogQLParser::EQ_SINGLE); + identifier(); setState(1002); - match(HogQLParser::LBRACE); + match(HogQLParser::EQ_SINGLE); setState(1003); - columnExpr(0); + match(HogQLParser::LBRACE); setState(1004); + columnExpr(0); + setState(1005); match(HogQLParser::RBRACE); break; } case 3: { enterOuterAlt(_localctx, 3); - setState(1006); + setState(1007); identifier(); break; } @@ -8600,17 +8621,17 @@ HogQLParser::WithExprListContext* HogQLParser::withExprList() { }); try { enterOuterAlt(_localctx, 1); - setState(1009); + setState(1010); withExpr(); - setState(1014); + setState(1015); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(1010); - match(HogQLParser::COMMA); setState(1011); + match(HogQLParser::COMMA); + setState(1012); withExpr(); - setState(1016); + setState(1017); _errHandler->sync(this); _la = _input->LA(1); } @@ -8706,21 +8727,21 @@ HogQLParser::WithExprContext* HogQLParser::withExpr() { exitRule(); }); try { - setState(1027); + setState(1028); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 119, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 123, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 1); - setState(1017); - identifier(); setState(1018); - match(HogQLParser::AS); + identifier(); setState(1019); - match(HogQLParser::LPAREN); + match(HogQLParser::AS); setState(1020); - selectUnionStmt(); + match(HogQLParser::LPAREN); setState(1021); + selectUnionStmt(); + setState(1022); match(HogQLParser::RPAREN); break; } @@ -8728,11 +8749,11 @@ HogQLParser::WithExprContext* HogQLParser::withExpr() { case 2: { _localctx = _tracker.createInstance(_localctx); enterOuterAlt(_localctx, 2); - setState(1023); - columnExpr(0); setState(1024); - match(HogQLParser::AS); + columnExpr(0); setState(1025); + match(HogQLParser::AS); + setState(1026); identifier(); break; } @@ -8798,12 +8819,12 @@ HogQLParser::ColumnIdentifierContext* HogQLParser::columnIdentifier() { exitRule(); }); try { - setState(1036); + setState(1037); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::LBRACE: { enterOuterAlt(_localctx, 1); - setState(1029); + setState(1030); placeholder(); break; } @@ -8903,14 +8924,14 @@ HogQLParser::ColumnIdentifierContext* HogQLParser::columnIdentifier() { case HogQLParser::YEAR: case HogQLParser::IDENTIFIER: { enterOuterAlt(_localctx, 2); - setState(1033); + setState(1034); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 120, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 124, _ctx)) { case 1: { - setState(1030); - tableIdentifier(); setState(1031); + tableIdentifier(); + setState(1032); match(HogQLParser::DOT); break; } @@ -8918,7 +8939,7 @@ HogQLParser::ColumnIdentifierContext* HogQLParser::columnIdentifier() { default: break; } - setState(1035); + setState(1036); nestedIdentifier(); break; } @@ -8986,21 +9007,21 @@ HogQLParser::NestedIdentifierContext* HogQLParser::nestedIdentifier() { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(1038); + setState(1039); identifier(); - setState(1043); + setState(1044); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 122, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 126, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { - setState(1039); - match(HogQLParser::DOT); setState(1040); + match(HogQLParser::DOT); + setState(1041); identifier(); } - setState(1045); + setState(1046); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 122, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 126, _ctx); } } @@ -9164,15 +9185,15 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { try { size_t alt; enterOuterAlt(_localctx, 1); - setState(1055); + setState(1056); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 123, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 127, _ctx)) { case 1: { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1047); + setState(1048); tableIdentifier(); break; } @@ -9181,7 +9202,7 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1048); + setState(1049); tableFunctionExpr(); break; } @@ -9190,11 +9211,11 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1049); - match(HogQLParser::LPAREN); setState(1050); - selectUnionStmt(); + match(HogQLParser::LPAREN); setState(1051); + selectUnionStmt(); + setState(1052); match(HogQLParser::RPAREN); break; } @@ -9203,7 +9224,7 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1053); + setState(1054); hogqlxTagElement(); break; } @@ -9212,7 +9233,7 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { _localctx = _tracker.createInstance(_localctx); _ctx = _localctx; previousContext = _localctx; - setState(1054); + setState(1055); placeholder(); break; } @@ -9221,9 +9242,9 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { break; } _ctx->stop = _input->LT(-1); - setState(1065); + setState(1066); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 125, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 129, _ctx); while (alt != 2 && alt != atn::ATN::INVALID_ALT_NUMBER) { if (alt == 1) { if (!_parseListeners.empty()) @@ -9232,10 +9253,10 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { auto newContext = _tracker.createInstance(_tracker.createInstance(parentContext, parentState)); _localctx = newContext; pushNewRecursionContext(newContext, startState, RuleTableExpr); - setState(1057); + setState(1058); if (!(precpred(_ctx, 3))) throw FailedPredicateException(this, "precpred(_ctx, 3)"); - setState(1061); + setState(1062); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::DATE: @@ -9243,15 +9264,15 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { case HogQLParser::ID: case HogQLParser::KEY: case HogQLParser::IDENTIFIER: { - setState(1058); + setState(1059); alias(); break; } case HogQLParser::AS: { - setState(1059); - match(HogQLParser::AS); setState(1060); + match(HogQLParser::AS); + setState(1061); identifier(); break; } @@ -9260,9 +9281,9 @@ HogQLParser::TableExprContext* HogQLParser::tableExpr(int precedence) { throw NoViableAltException(this); } } - setState(1067); + setState(1068); _errHandler->sync(this); - alt = getInterpreter()->adaptivePredict(_input, 125, _ctx); + alt = getInterpreter()->adaptivePredict(_input, 129, _ctx); } } catch (RecognitionException &e) { @@ -9322,11 +9343,11 @@ HogQLParser::TableFunctionExprContext* HogQLParser::tableFunctionExpr() { }); try { enterOuterAlt(_localctx, 1); - setState(1068); - identifier(); setState(1069); + identifier(); + setState(1070); match(HogQLParser::LPAREN); - setState(1071); + setState(1072); _errHandler->sync(this); _la = _input->LA(1); @@ -9334,10 +9355,10 @@ HogQLParser::TableFunctionExprContext* HogQLParser::tableFunctionExpr() { ((1ULL << _la) & -1125900443713538) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 64)) & 8076106347046764543) != 0) || ((((_la - 128) & ~ 0x3fULL) == 0) && ((1ULL << (_la - 128)) & 577) != 0)) { - setState(1070); + setState(1071); tableArgList(); } - setState(1073); + setState(1074); match(HogQLParser::RPAREN); } @@ -9394,14 +9415,14 @@ HogQLParser::TableIdentifierContext* HogQLParser::tableIdentifier() { }); try { enterOuterAlt(_localctx, 1); - setState(1078); + setState(1079); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 127, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 131, _ctx)) { case 1: { - setState(1075); - databaseIdentifier(); setState(1076); + databaseIdentifier(); + setState(1077); match(HogQLParser::DOT); break; } @@ -9409,7 +9430,7 @@ HogQLParser::TableIdentifierContext* HogQLParser::tableIdentifier() { default: break; } - setState(1080); + setState(1081); identifier(); } @@ -9471,17 +9492,17 @@ HogQLParser::TableArgListContext* HogQLParser::tableArgList() { }); try { enterOuterAlt(_localctx, 1); - setState(1082); + setState(1083); columnExpr(0); - setState(1087); + setState(1088); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::COMMA) { - setState(1083); - match(HogQLParser::COMMA); setState(1084); + match(HogQLParser::COMMA); + setState(1085); columnExpr(0); - setState(1089); + setState(1090); _errHandler->sync(this); _la = _input->LA(1); } @@ -9532,7 +9553,7 @@ HogQLParser::DatabaseIdentifierContext* HogQLParser::databaseIdentifier() { }); try { enterOuterAlt(_localctx, 1); - setState(1090); + setState(1091); identifier(); } @@ -9597,21 +9618,21 @@ HogQLParser::FloatingLiteralContext* HogQLParser::floatingLiteral() { exitRule(); }); try { - setState(1100); + setState(1101); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::FLOATING_LITERAL: { enterOuterAlt(_localctx, 1); - setState(1092); + setState(1093); match(HogQLParser::FLOATING_LITERAL); break; } case HogQLParser::DOT: { enterOuterAlt(_localctx, 2); - setState(1093); - match(HogQLParser::DOT); setState(1094); + match(HogQLParser::DOT); + setState(1095); _la = _input->LA(1); if (!(_la == HogQLParser::OCTAL_LITERAL @@ -9627,16 +9648,16 @@ HogQLParser::FloatingLiteralContext* HogQLParser::floatingLiteral() { case HogQLParser::DECIMAL_LITERAL: { enterOuterAlt(_localctx, 3); - setState(1095); - match(HogQLParser::DECIMAL_LITERAL); setState(1096); + match(HogQLParser::DECIMAL_LITERAL); + setState(1097); match(HogQLParser::DOT); - setState(1098); + setState(1099); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 129, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 133, _ctx)) { case 1: { - setState(1097); + setState(1098); _la = _input->LA(1); if (!(_la == HogQLParser::OCTAL_LITERAL @@ -9735,14 +9756,14 @@ HogQLParser::NumberLiteralContext* HogQLParser::numberLiteral() { }); try { enterOuterAlt(_localctx, 1); - setState(1103); + setState(1104); _errHandler->sync(this); _la = _input->LA(1); if (_la == HogQLParser::DASH || _la == HogQLParser::PLUS) { - setState(1102); + setState(1103); _la = _input->LA(1); if (!(_la == HogQLParser::DASH @@ -9754,41 +9775,41 @@ HogQLParser::NumberLiteralContext* HogQLParser::numberLiteral() { consume(); } } - setState(1111); + setState(1112); _errHandler->sync(this); - switch (getInterpreter()->adaptivePredict(_input, 132, _ctx)) { + switch (getInterpreter()->adaptivePredict(_input, 136, _ctx)) { case 1: { - setState(1105); + setState(1106); floatingLiteral(); break; } case 2: { - setState(1106); + setState(1107); match(HogQLParser::OCTAL_LITERAL); break; } case 3: { - setState(1107); + setState(1108); match(HogQLParser::DECIMAL_LITERAL); break; } case 4: { - setState(1108); + setState(1109); match(HogQLParser::HEXADECIMAL_LITERAL); break; } case 5: { - setState(1109); + setState(1110); match(HogQLParser::INF); break; } case 6: { - setState(1110); + setState(1111); match(HogQLParser::NAN_SQL); break; } @@ -9850,7 +9871,7 @@ HogQLParser::LiteralContext* HogQLParser::literal() { exitRule(); }); try { - setState(1116); + setState(1117); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::INF: @@ -9863,21 +9884,21 @@ HogQLParser::LiteralContext* HogQLParser::literal() { case HogQLParser::DOT: case HogQLParser::PLUS: { enterOuterAlt(_localctx, 1); - setState(1113); + setState(1114); numberLiteral(); break; } case HogQLParser::STRING_LITERAL: { enterOuterAlt(_localctx, 2); - setState(1114); + setState(1115); match(HogQLParser::STRING_LITERAL); break; } case HogQLParser::NULL_SQL: { enterOuterAlt(_localctx, 3); - setState(1115); + setState(1116); match(HogQLParser::NULL_SQL); break; } @@ -9961,7 +9982,7 @@ HogQLParser::IntervalContext* HogQLParser::interval() { }); try { enterOuterAlt(_localctx, 1); - setState(1118); + setState(1119); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 27021666484748288) != 0) || ((((_la - 68) & ~ 0x3fULL) == 0) && @@ -10356,7 +10377,7 @@ HogQLParser::KeywordContext* HogQLParser::keyword() { }); try { enterOuterAlt(_localctx, 1); - setState(1120); + setState(1121); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & -208293751046537218) != 0) || ((((_la - 64) & ~ 0x3fULL) == 0) && @@ -10427,7 +10448,7 @@ HogQLParser::KeywordForAliasContext* HogQLParser::keywordForAlias() { }); try { enterOuterAlt(_localctx, 1); - setState(1122); + setState(1123); _la = _input->LA(1); if (!((((_la & ~ 0x3fULL) == 0) && ((1ULL << _la) & 70506452090880) != 0))) { @@ -10487,12 +10508,12 @@ HogQLParser::AliasContext* HogQLParser::alias() { exitRule(); }); try { - setState(1126); + setState(1127); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::IDENTIFIER: { enterOuterAlt(_localctx, 1); - setState(1124); + setState(1125); match(HogQLParser::IDENTIFIER); break; } @@ -10502,7 +10523,7 @@ HogQLParser::AliasContext* HogQLParser::alias() { case HogQLParser::ID: case HogQLParser::KEY: { enterOuterAlt(_localctx, 2); - setState(1125); + setState(1126); keywordForAlias(); break; } @@ -10564,12 +10585,12 @@ HogQLParser::IdentifierContext* HogQLParser::identifier() { exitRule(); }); try { - setState(1131); + setState(1132); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::IDENTIFIER: { enterOuterAlt(_localctx, 1); - setState(1128); + setState(1129); match(HogQLParser::IDENTIFIER); break; } @@ -10583,7 +10604,7 @@ HogQLParser::IdentifierContext* HogQLParser::identifier() { case HogQLParser::WEEK: case HogQLParser::YEAR: { enterOuterAlt(_localctx, 2); - setState(1129); + setState(1130); interval(); break; } @@ -10674,7 +10695,7 @@ HogQLParser::IdentifierContext* HogQLParser::identifier() { case HogQLParser::WINDOW: case HogQLParser::WITH: { enterOuterAlt(_localctx, 3); - setState(1130); + setState(1131); keyword(); break; } @@ -10737,11 +10758,11 @@ HogQLParser::EnumValueContext* HogQLParser::enumValue() { }); try { enterOuterAlt(_localctx, 1); - setState(1133); - string(); setState(1134); - match(HogQLParser::EQ_SINGLE); + string(); setState(1135); + match(HogQLParser::EQ_SINGLE); + setState(1136); numberLiteral(); } @@ -10798,11 +10819,11 @@ HogQLParser::PlaceholderContext* HogQLParser::placeholder() { }); try { enterOuterAlt(_localctx, 1); - setState(1137); - match(HogQLParser::LBRACE); setState(1138); - identifier(); + match(HogQLParser::LBRACE); setState(1139); + identifier(); + setState(1140); match(HogQLParser::RBRACE); } @@ -10854,19 +10875,19 @@ HogQLParser::StringContext* HogQLParser::string() { exitRule(); }); try { - setState(1143); + setState(1144); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::STRING_LITERAL: { enterOuterAlt(_localctx, 1); - setState(1141); + setState(1142); match(HogQLParser::STRING_LITERAL); break; } case HogQLParser::QUOTE_SINGLE_TEMPLATE: { enterOuterAlt(_localctx, 2); - setState(1142); + setState(1143); templateString(); break; } @@ -10934,21 +10955,21 @@ HogQLParser::TemplateStringContext* HogQLParser::templateString() { }); try { enterOuterAlt(_localctx, 1); - setState(1145); + setState(1146); match(HogQLParser::QUOTE_SINGLE_TEMPLATE); - setState(1149); + setState(1150); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::STRING_TEXT || _la == HogQLParser::STRING_ESCAPE_TRIGGER) { - setState(1146); + setState(1147); stringContents(); - setState(1151); + setState(1152); _errHandler->sync(this); _la = _input->LA(1); } - setState(1152); + setState(1153); match(HogQLParser::QUOTE_SINGLE); } @@ -11008,23 +11029,23 @@ HogQLParser::StringContentsContext* HogQLParser::stringContents() { exitRule(); }); try { - setState(1159); + setState(1160); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::STRING_ESCAPE_TRIGGER: { enterOuterAlt(_localctx, 1); - setState(1154); - match(HogQLParser::STRING_ESCAPE_TRIGGER); setState(1155); - columnExpr(0); + match(HogQLParser::STRING_ESCAPE_TRIGGER); setState(1156); + columnExpr(0); + setState(1157); match(HogQLParser::RBRACE); break; } case HogQLParser::STRING_TEXT: { enterOuterAlt(_localctx, 2); - setState(1158); + setState(1159); match(HogQLParser::STRING_TEXT); break; } @@ -11092,21 +11113,21 @@ HogQLParser::FullTemplateStringContext* HogQLParser::fullTemplateString() { }); try { enterOuterAlt(_localctx, 1); - setState(1161); + setState(1162); match(HogQLParser::QUOTE_SINGLE_TEMPLATE_FULL); - setState(1165); + setState(1166); _errHandler->sync(this); _la = _input->LA(1); while (_la == HogQLParser::FULL_STRING_TEXT || _la == HogQLParser::FULL_STRING_ESCAPE_TRIGGER) { - setState(1162); + setState(1163); stringContentsFull(); - setState(1167); + setState(1168); _errHandler->sync(this); _la = _input->LA(1); } - setState(1168); + setState(1169); match(HogQLParser::EOF); } @@ -11166,23 +11187,23 @@ HogQLParser::StringContentsFullContext* HogQLParser::stringContentsFull() { exitRule(); }); try { - setState(1175); + setState(1176); _errHandler->sync(this); switch (_input->LA(1)) { case HogQLParser::FULL_STRING_ESCAPE_TRIGGER: { enterOuterAlt(_localctx, 1); - setState(1170); - match(HogQLParser::FULL_STRING_ESCAPE_TRIGGER); setState(1171); - columnExpr(0); + match(HogQLParser::FULL_STRING_ESCAPE_TRIGGER); setState(1172); + columnExpr(0); + setState(1173); match(HogQLParser::RBRACE); break; } case HogQLParser::FULL_STRING_TEXT: { enterOuterAlt(_localctx, 2); - setState(1174); + setState(1175); match(HogQLParser::FULL_STRING_TEXT); break; } diff --git a/hogql_parser/HogQLParser.h b/hogql_parser/HogQLParser.h index 94f46d07b4562..9496b4e90c9ba 100644 --- a/hogql_parser/HogQLParser.h +++ b/hogql_parser/HogQLParser.h @@ -45,8 +45,8 @@ class HogQLParser : public antlr4::Parser { enum { RuleProgram = 0, RuleDeclaration = 1, RuleExpression = 2, RuleVarDecl = 3, - RuleVarAssignment = 4, RuleIdentifierList = 5, RuleStatement = 6, RuleExprStmt = 7, - RuleIfStmt = 8, RuleWhileStmt = 9, RuleReturnStmt = 10, RuleFuncStmt = 11, + RuleIdentifierList = 4, RuleStatement = 5, RuleReturnStmt = 6, RuleIfStmt = 7, + RuleWhileStmt = 8, RuleFuncStmt = 9, RuleVarAssignment = 10, RuleExprStmt = 11, RuleEmptyStmt = 12, RuleBlock = 13, RuleKvPair = 14, RuleKvPairList = 15, RuleSelect = 16, RuleSelectUnionStmt = 17, RuleSelectStmtWithParens = 18, RuleSelectStmt = 19, RuleWithClause = 20, RuleTopClause = 21, RuleFromClause = 22, @@ -92,14 +92,14 @@ class HogQLParser : public antlr4::Parser { class DeclarationContext; class ExpressionContext; class VarDeclContext; - class VarAssignmentContext; class IdentifierListContext; class StatementContext; - class ExprStmtContext; + class ReturnStmtContext; class IfStmtContext; class WhileStmtContext; - class ReturnStmtContext; class FuncStmtContext; + class VarAssignmentContext; + class ExprStmtContext; class EmptyStmtContext; class BlockContext; class KvPairContext; @@ -220,7 +220,6 @@ class HogQLParser : public antlr4::Parser { virtual size_t getRuleIndex() const override; antlr4::tree::TerminalNode *LET(); IdentifierContext *identifier(); - antlr4::tree::TerminalNode *SEMICOLON(); antlr4::tree::TerminalNode *COLON(); antlr4::tree::TerminalNode *EQ_SINGLE(); ExpressionContext *expression(); @@ -232,23 +231,6 @@ class HogQLParser : public antlr4::Parser { VarDeclContext* varDecl(); - class VarAssignmentContext : public antlr4::ParserRuleContext { - public: - VarAssignmentContext(antlr4::ParserRuleContext *parent, size_t invokingState); - virtual size_t getRuleIndex() const override; - std::vector expression(); - ExpressionContext* expression(size_t i); - antlr4::tree::TerminalNode *COLON(); - antlr4::tree::TerminalNode *EQ_SINGLE(); - antlr4::tree::TerminalNode *SEMICOLON(); - - - virtual std::any accept(antlr4::tree::ParseTreeVisitor *visitor) override; - - }; - - VarAssignmentContext* varAssignment(); - class IdentifierListContext : public antlr4::ParserRuleContext { public: IdentifierListContext(antlr4::ParserRuleContext *parent, size_t invokingState); @@ -270,12 +252,12 @@ class HogQLParser : public antlr4::Parser { StatementContext(antlr4::ParserRuleContext *parent, size_t invokingState); virtual size_t getRuleIndex() const override; ReturnStmtContext *returnStmt(); - EmptyStmtContext *emptyStmt(); - ExprStmtContext *exprStmt(); IfStmtContext *ifStmt(); WhileStmtContext *whileStmt(); FuncStmtContext *funcStmt(); VarAssignmentContext *varAssignment(); + ExprStmtContext *exprStmt(); + EmptyStmtContext *emptyStmt(); BlockContext *block(); @@ -285,10 +267,11 @@ class HogQLParser : public antlr4::Parser { StatementContext* statement(); - class ExprStmtContext : public antlr4::ParserRuleContext { + class ReturnStmtContext : public antlr4::ParserRuleContext { public: - ExprStmtContext(antlr4::ParserRuleContext *parent, size_t invokingState); + ReturnStmtContext(antlr4::ParserRuleContext *parent, size_t invokingState); virtual size_t getRuleIndex() const override; + antlr4::tree::TerminalNode *RETURN(); ExpressionContext *expression(); antlr4::tree::TerminalNode *SEMICOLON(); @@ -297,7 +280,7 @@ class HogQLParser : public antlr4::Parser { }; - ExprStmtContext* exprStmt(); + ReturnStmtContext* returnStmt(); class IfStmtContext : public antlr4::ParserRuleContext { public: @@ -327,20 +310,6 @@ class HogQLParser : public antlr4::Parser { ExpressionContext *expression(); antlr4::tree::TerminalNode *RPAREN(); StatementContext *statement(); - - - virtual std::any accept(antlr4::tree::ParseTreeVisitor *visitor) override; - - }; - - WhileStmtContext* whileStmt(); - - class ReturnStmtContext : public antlr4::ParserRuleContext { - public: - ReturnStmtContext(antlr4::ParserRuleContext *parent, size_t invokingState); - virtual size_t getRuleIndex() const override; - antlr4::tree::TerminalNode *RETURN(); - ExpressionContext *expression(); antlr4::tree::TerminalNode *SEMICOLON(); @@ -348,7 +317,7 @@ class HogQLParser : public antlr4::Parser { }; - ReturnStmtContext* returnStmt(); + WhileStmtContext* whileStmt(); class FuncStmtContext : public antlr4::ParserRuleContext { public: @@ -368,6 +337,36 @@ class HogQLParser : public antlr4::Parser { FuncStmtContext* funcStmt(); + class VarAssignmentContext : public antlr4::ParserRuleContext { + public: + VarAssignmentContext(antlr4::ParserRuleContext *parent, size_t invokingState); + virtual size_t getRuleIndex() const override; + std::vector expression(); + ExpressionContext* expression(size_t i); + antlr4::tree::TerminalNode *COLON(); + antlr4::tree::TerminalNode *EQ_SINGLE(); + + + virtual std::any accept(antlr4::tree::ParseTreeVisitor *visitor) override; + + }; + + VarAssignmentContext* varAssignment(); + + class ExprStmtContext : public antlr4::ParserRuleContext { + public: + ExprStmtContext(antlr4::ParserRuleContext *parent, size_t invokingState); + virtual size_t getRuleIndex() const override; + ExpressionContext *expression(); + antlr4::tree::TerminalNode *SEMICOLON(); + + + virtual std::any accept(antlr4::tree::ParseTreeVisitor *visitor) override; + + }; + + ExprStmtContext* exprStmt(); + class EmptyStmtContext : public antlr4::ParserRuleContext { public: EmptyStmtContext(antlr4::ParserRuleContext *parent, size_t invokingState); @@ -1246,7 +1245,6 @@ class HogQLParser : public antlr4::Parser { ColumnExprAliasContext(ColumnExprContext *ctx); ColumnExprContext *columnExpr(); - AliasContext *alias(); antlr4::tree::TerminalNode *AS(); IdentifierContext *identifier(); antlr4::tree::TerminalNode *STRING_LITERAL(); diff --git a/hogql_parser/HogQLParser.interp b/hogql_parser/HogQLParser.interp index 086eca220c32f..620eb0b471748 100644 --- a/hogql_parser/HogQLParser.interp +++ b/hogql_parser/HogQLParser.interp @@ -317,14 +317,14 @@ program declaration expression varDecl -varAssignment identifierList statement -exprStmt +returnStmt ifStmt whileStmt -returnStmt funcStmt +varAssignment +exprStmt emptyStmt block kvPair @@ -399,4 +399,4 @@ stringContentsFull atn: -[4, 1, 154, 1178, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 199, 8, 5, 10, 5, 12, 5, 202, 9, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 213, 8, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 225, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 241, 8, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 250, 8, 13, 10, 13, 12, 13, 253, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 264, 8, 15, 10, 15, 12, 15, 267, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 272, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 280, 8, 17, 10, 17, 12, 17, 283, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 291, 8, 18, 1, 19, 3, 19, 294, 8, 19, 1, 19, 1, 19, 3, 19, 298, 8, 19, 1, 19, 3, 19, 301, 8, 19, 1, 19, 1, 19, 3, 19, 305, 8, 19, 1, 19, 3, 19, 308, 8, 19, 1, 19, 3, 19, 311, 8, 19, 1, 19, 3, 19, 314, 8, 19, 1, 19, 3, 19, 317, 8, 19, 1, 19, 1, 19, 3, 19, 321, 8, 19, 1, 19, 1, 19, 3, 19, 325, 8, 19, 1, 19, 3, 19, 328, 8, 19, 1, 19, 3, 19, 331, 8, 19, 1, 19, 3, 19, 334, 8, 19, 1, 19, 1, 19, 3, 19, 338, 8, 19, 1, 19, 3, 19, 341, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 350, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 356, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 375, 8, 24, 10, 24, 12, 24, 378, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 394, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 411, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 417, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 423, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 434, 8, 31, 3, 31, 436, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 447, 8, 34, 1, 34, 3, 34, 450, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 456, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 464, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 470, 8, 34, 10, 34, 12, 34, 473, 9, 34, 1, 35, 3, 35, 476, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 481, 8, 35, 1, 35, 3, 35, 484, 8, 35, 1, 35, 3, 35, 487, 8, 35, 1, 35, 1, 35, 3, 35, 491, 8, 35, 1, 35, 1, 35, 3, 35, 495, 8, 35, 1, 35, 3, 35, 498, 8, 35, 3, 35, 500, 8, 35, 1, 35, 3, 35, 503, 8, 35, 1, 35, 1, 35, 3, 35, 507, 8, 35, 1, 35, 1, 35, 3, 35, 511, 8, 35, 1, 35, 3, 35, 514, 8, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 523, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 534, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 540, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 545, 8, 39, 10, 39, 12, 39, 548, 9, 39, 1, 40, 1, 40, 3, 40, 552, 8, 40, 1, 40, 1, 40, 3, 40, 556, 8, 40, 1, 40, 1, 40, 3, 40, 560, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 566, 8, 41, 3, 41, 568, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 573, 8, 42, 10, 42, 12, 42, 576, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 583, 8, 44, 1, 44, 3, 44, 586, 8, 44, 1, 44, 3, 44, 589, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 608, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 622, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 636, 8, 51, 10, 51, 12, 51, 639, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 648, 8, 51, 10, 51, 12, 51, 651, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 660, 8, 51, 10, 51, 12, 51, 663, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 670, 8, 51, 1, 51, 1, 51, 3, 51, 674, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 679, 8, 52, 10, 52, 12, 52, 682, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 687, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 694, 8, 53, 11, 53, 12, 53, 695, 1, 53, 1, 53, 3, 53, 700, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 724, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 741, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 747, 8, 53, 1, 53, 3, 53, 750, 8, 53, 1, 53, 3, 53, 753, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 763, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 769, 8, 53, 1, 53, 3, 53, 772, 8, 53, 1, 53, 3, 53, 775, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 783, 8, 53, 1, 53, 3, 53, 786, 8, 53, 1, 53, 1, 53, 3, 53, 790, 8, 53, 1, 53, 3, 53, 793, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 807, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 824, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 829, 8, 53, 1, 53, 1, 53, 3, 53, 833, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 839, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 846, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 858, 8, 53, 1, 53, 1, 53, 3, 53, 862, 8, 53, 1, 53, 3, 53, 865, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 874, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 888, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 915, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 924, 8, 53, 5, 53, 926, 8, 53, 10, 53, 12, 53, 929, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 934, 8, 54, 10, 54, 12, 54, 937, 9, 54, 1, 55, 1, 55, 3, 55, 941, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 947, 8, 56, 10, 56, 12, 56, 950, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 957, 8, 56, 10, 56, 12, 56, 960, 9, 56, 3, 56, 962, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 970, 8, 57, 10, 57, 12, 57, 973, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 981, 8, 57, 10, 57, 12, 57, 984, 9, 57, 1, 57, 1, 57, 3, 57, 988, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 995, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 1008, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 1013, 8, 59, 10, 59, 12, 59, 1016, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1028, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1034, 8, 61, 1, 61, 3, 61, 1037, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1042, 8, 62, 10, 62, 12, 62, 1045, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1056, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1062, 8, 63, 5, 63, 1064, 8, 63, 10, 63, 12, 63, 1067, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1072, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1079, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1086, 8, 66, 10, 66, 12, 66, 1089, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1099, 8, 68, 3, 68, 1101, 8, 68, 1, 69, 3, 69, 1104, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1112, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1117, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1127, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1132, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1144, 8, 78, 1, 79, 1, 79, 5, 79, 1148, 8, 79, 10, 79, 12, 79, 1151, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1160, 8, 80, 1, 81, 1, 81, 5, 81, 1164, 8, 81, 10, 81, 12, 81, 1167, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1176, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1314, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 189, 1, 0, 0, 0, 10, 195, 1, 0, 0, 0, 12, 212, 1, 0, 0, 0, 14, 214, 1, 0, 0, 0, 16, 217, 1, 0, 0, 0, 18, 226, 1, 0, 0, 0, 20, 232, 1, 0, 0, 0, 22, 236, 1, 0, 0, 0, 24, 245, 1, 0, 0, 0, 26, 247, 1, 0, 0, 0, 28, 256, 1, 0, 0, 0, 30, 260, 1, 0, 0, 0, 32, 271, 1, 0, 0, 0, 34, 275, 1, 0, 0, 0, 36, 290, 1, 0, 0, 0, 38, 293, 1, 0, 0, 0, 40, 342, 1, 0, 0, 0, 42, 345, 1, 0, 0, 0, 44, 351, 1, 0, 0, 0, 46, 355, 1, 0, 0, 0, 48, 361, 1, 0, 0, 0, 50, 379, 1, 0, 0, 0, 52, 382, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 395, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 402, 1, 0, 0, 0, 62, 435, 1, 0, 0, 0, 64, 437, 1, 0, 0, 0, 66, 440, 1, 0, 0, 0, 68, 455, 1, 0, 0, 0, 70, 517, 1, 0, 0, 0, 72, 522, 1, 0, 0, 0, 74, 533, 1, 0, 0, 0, 76, 535, 1, 0, 0, 0, 78, 541, 1, 0, 0, 0, 80, 549, 1, 0, 0, 0, 82, 567, 1, 0, 0, 0, 84, 569, 1, 0, 0, 0, 86, 577, 1, 0, 0, 0, 88, 582, 1, 0, 0, 0, 90, 590, 1, 0, 0, 0, 92, 594, 1, 0, 0, 0, 94, 598, 1, 0, 0, 0, 96, 607, 1, 0, 0, 0, 98, 621, 1, 0, 0, 0, 100, 623, 1, 0, 0, 0, 102, 673, 1, 0, 0, 0, 104, 675, 1, 0, 0, 0, 106, 832, 1, 0, 0, 0, 108, 930, 1, 0, 0, 0, 110, 940, 1, 0, 0, 0, 112, 961, 1, 0, 0, 0, 114, 994, 1, 0, 0, 0, 116, 1007, 1, 0, 0, 0, 118, 1009, 1, 0, 0, 0, 120, 1027, 1, 0, 0, 0, 122, 1036, 1, 0, 0, 0, 124, 1038, 1, 0, 0, 0, 126, 1055, 1, 0, 0, 0, 128, 1068, 1, 0, 0, 0, 130, 1078, 1, 0, 0, 0, 132, 1082, 1, 0, 0, 0, 134, 1090, 1, 0, 0, 0, 136, 1100, 1, 0, 0, 0, 138, 1103, 1, 0, 0, 0, 140, 1116, 1, 0, 0, 0, 142, 1118, 1, 0, 0, 0, 144, 1120, 1, 0, 0, 0, 146, 1122, 1, 0, 0, 0, 148, 1126, 1, 0, 0, 0, 150, 1131, 1, 0, 0, 0, 152, 1133, 1, 0, 0, 0, 154, 1137, 1, 0, 0, 0, 156, 1143, 1, 0, 0, 0, 158, 1145, 1, 0, 0, 0, 160, 1159, 1, 0, 0, 0, 162, 1161, 1, 0, 0, 0, 164, 1175, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 12, 6, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 188, 5, 145, 0, 0, 188, 7, 1, 0, 0, 0, 189, 190, 3, 4, 2, 0, 190, 191, 5, 111, 0, 0, 191, 192, 5, 118, 0, 0, 192, 193, 3, 4, 2, 0, 193, 194, 5, 145, 0, 0, 194, 9, 1, 0, 0, 0, 195, 200, 3, 150, 75, 0, 196, 197, 5, 112, 0, 0, 197, 199, 3, 150, 75, 0, 198, 196, 1, 0, 0, 0, 199, 202, 1, 0, 0, 0, 200, 198, 1, 0, 0, 0, 200, 201, 1, 0, 0, 0, 201, 11, 1, 0, 0, 0, 202, 200, 1, 0, 0, 0, 203, 213, 3, 20, 10, 0, 204, 213, 3, 24, 12, 0, 205, 213, 3, 14, 7, 0, 206, 213, 3, 16, 8, 0, 207, 213, 3, 18, 9, 0, 208, 213, 3, 22, 11, 0, 209, 213, 3, 8, 4, 0, 210, 213, 3, 20, 10, 0, 211, 213, 3, 26, 13, 0, 212, 203, 1, 0, 0, 0, 212, 204, 1, 0, 0, 0, 212, 205, 1, 0, 0, 0, 212, 206, 1, 0, 0, 0, 212, 207, 1, 0, 0, 0, 212, 208, 1, 0, 0, 0, 212, 209, 1, 0, 0, 0, 212, 210, 1, 0, 0, 0, 212, 211, 1, 0, 0, 0, 213, 13, 1, 0, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 145, 0, 0, 216, 15, 1, 0, 0, 0, 217, 218, 5, 38, 0, 0, 218, 219, 5, 126, 0, 0, 219, 220, 3, 4, 2, 0, 220, 221, 5, 144, 0, 0, 221, 224, 3, 12, 6, 0, 222, 223, 5, 24, 0, 0, 223, 225, 3, 12, 6, 0, 224, 222, 1, 0, 0, 0, 224, 225, 1, 0, 0, 0, 225, 17, 1, 0, 0, 0, 226, 227, 5, 96, 0, 0, 227, 228, 5, 126, 0, 0, 228, 229, 3, 4, 2, 0, 229, 230, 5, 144, 0, 0, 230, 231, 3, 12, 6, 0, 231, 19, 1, 0, 0, 0, 232, 233, 5, 70, 0, 0, 233, 234, 3, 4, 2, 0, 234, 235, 5, 145, 0, 0, 235, 21, 1, 0, 0, 0, 236, 237, 5, 29, 0, 0, 237, 238, 3, 150, 75, 0, 238, 240, 5, 126, 0, 0, 239, 241, 3, 10, 5, 0, 240, 239, 1, 0, 0, 0, 240, 241, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 5, 144, 0, 0, 243, 244, 3, 26, 13, 0, 244, 23, 1, 0, 0, 0, 245, 246, 5, 145, 0, 0, 246, 25, 1, 0, 0, 0, 247, 251, 5, 124, 0, 0, 248, 250, 3, 2, 1, 0, 249, 248, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 254, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 255, 5, 142, 0, 0, 255, 27, 1, 0, 0, 0, 256, 257, 3, 4, 2, 0, 257, 258, 5, 111, 0, 0, 258, 259, 3, 4, 2, 0, 259, 29, 1, 0, 0, 0, 260, 265, 3, 28, 14, 0, 261, 262, 5, 112, 0, 0, 262, 264, 3, 28, 14, 0, 263, 261, 1, 0, 0, 0, 264, 267, 1, 0, 0, 0, 265, 263, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 31, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 272, 3, 34, 17, 0, 269, 272, 3, 38, 19, 0, 270, 272, 3, 114, 57, 0, 271, 268, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 271, 270, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 274, 5, 0, 0, 1, 274, 33, 1, 0, 0, 0, 275, 281, 3, 36, 18, 0, 276, 277, 5, 91, 0, 0, 277, 278, 5, 1, 0, 0, 278, 280, 3, 36, 18, 0, 279, 276, 1, 0, 0, 0, 280, 283, 1, 0, 0, 0, 281, 279, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 35, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 284, 291, 3, 38, 19, 0, 285, 286, 5, 126, 0, 0, 286, 287, 3, 34, 17, 0, 287, 288, 5, 144, 0, 0, 288, 291, 1, 0, 0, 0, 289, 291, 3, 154, 77, 0, 290, 284, 1, 0, 0, 0, 290, 285, 1, 0, 0, 0, 290, 289, 1, 0, 0, 0, 291, 37, 1, 0, 0, 0, 292, 294, 3, 40, 20, 0, 293, 292, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 297, 5, 77, 0, 0, 296, 298, 5, 23, 0, 0, 297, 296, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 300, 1, 0, 0, 0, 299, 301, 3, 42, 21, 0, 300, 299, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 304, 3, 104, 52, 0, 303, 305, 3, 44, 22, 0, 304, 303, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 307, 1, 0, 0, 0, 306, 308, 3, 46, 23, 0, 307, 306, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308, 310, 1, 0, 0, 0, 309, 311, 3, 50, 25, 0, 310, 309, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 313, 1, 0, 0, 0, 312, 314, 3, 52, 26, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 316, 1, 0, 0, 0, 315, 317, 3, 54, 27, 0, 316, 315, 1, 0, 0, 0, 316, 317, 1, 0, 0, 0, 317, 320, 1, 0, 0, 0, 318, 319, 5, 98, 0, 0, 319, 321, 7, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 324, 1, 0, 0, 0, 322, 323, 5, 98, 0, 0, 323, 325, 5, 86, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 327, 1, 0, 0, 0, 326, 328, 3, 56, 28, 0, 327, 326, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328, 330, 1, 0, 0, 0, 329, 331, 3, 48, 24, 0, 330, 329, 1, 0, 0, 0, 330, 331, 1, 0, 0, 0, 331, 333, 1, 0, 0, 0, 332, 334, 3, 58, 29, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 337, 1, 0, 0, 0, 335, 338, 3, 62, 31, 0, 336, 338, 3, 64, 32, 0, 337, 335, 1, 0, 0, 0, 337, 336, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 340, 1, 0, 0, 0, 339, 341, 3, 66, 33, 0, 340, 339, 1, 0, 0, 0, 340, 341, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 343, 5, 98, 0, 0, 343, 344, 3, 118, 59, 0, 344, 41, 1, 0, 0, 0, 345, 346, 5, 85, 0, 0, 346, 349, 5, 104, 0, 0, 347, 348, 5, 98, 0, 0, 348, 350, 5, 82, 0, 0, 349, 347, 1, 0, 0, 0, 349, 350, 1, 0, 0, 0, 350, 43, 1, 0, 0, 0, 351, 352, 5, 32, 0, 0, 352, 353, 3, 68, 34, 0, 353, 45, 1, 0, 0, 0, 354, 356, 7, 1, 0, 0, 355, 354, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 357, 1, 0, 0, 0, 357, 358, 5, 5, 0, 0, 358, 359, 5, 45, 0, 0, 359, 360, 3, 104, 52, 0, 360, 47, 1, 0, 0, 0, 361, 362, 5, 97, 0, 0, 362, 363, 3, 150, 75, 0, 363, 364, 5, 6, 0, 0, 364, 365, 5, 126, 0, 0, 365, 366, 3, 88, 44, 0, 366, 376, 5, 144, 0, 0, 367, 368, 5, 112, 0, 0, 368, 369, 3, 150, 75, 0, 369, 370, 5, 6, 0, 0, 370, 371, 5, 126, 0, 0, 371, 372, 3, 88, 44, 0, 372, 373, 5, 144, 0, 0, 373, 375, 1, 0, 0, 0, 374, 367, 1, 0, 0, 0, 375, 378, 1, 0, 0, 0, 376, 374, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 49, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 379, 380, 5, 67, 0, 0, 380, 381, 3, 106, 53, 0, 381, 51, 1, 0, 0, 0, 382, 383, 5, 95, 0, 0, 383, 384, 3, 106, 53, 0, 384, 53, 1, 0, 0, 0, 385, 386, 5, 34, 0, 0, 386, 393, 5, 11, 0, 0, 387, 388, 7, 0, 0, 0, 388, 389, 5, 126, 0, 0, 389, 390, 3, 104, 52, 0, 390, 391, 5, 144, 0, 0, 391, 394, 1, 0, 0, 0, 392, 394, 3, 104, 52, 0, 393, 387, 1, 0, 0, 0, 393, 392, 1, 0, 0, 0, 394, 55, 1, 0, 0, 0, 395, 396, 5, 35, 0, 0, 396, 397, 3, 106, 53, 0, 397, 57, 1, 0, 0, 0, 398, 399, 5, 62, 0, 0, 399, 400, 5, 11, 0, 0, 400, 401, 3, 78, 39, 0, 401, 59, 1, 0, 0, 0, 402, 403, 5, 62, 0, 0, 403, 404, 5, 11, 0, 0, 404, 405, 3, 104, 52, 0, 405, 61, 1, 0, 0, 0, 406, 407, 5, 52, 0, 0, 407, 410, 3, 106, 53, 0, 408, 409, 5, 112, 0, 0, 409, 411, 3, 106, 53, 0, 410, 408, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 416, 1, 0, 0, 0, 412, 413, 5, 98, 0, 0, 413, 417, 5, 82, 0, 0, 414, 415, 5, 11, 0, 0, 415, 417, 3, 104, 52, 0, 416, 412, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 416, 417, 1, 0, 0, 0, 417, 436, 1, 0, 0, 0, 418, 419, 5, 52, 0, 0, 419, 422, 3, 106, 53, 0, 420, 421, 5, 98, 0, 0, 421, 423, 5, 82, 0, 0, 422, 420, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 425, 5, 59, 0, 0, 425, 426, 3, 106, 53, 0, 426, 436, 1, 0, 0, 0, 427, 428, 5, 52, 0, 0, 428, 429, 3, 106, 53, 0, 429, 430, 5, 59, 0, 0, 430, 433, 3, 106, 53, 0, 431, 432, 5, 11, 0, 0, 432, 434, 3, 104, 52, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 436, 1, 0, 0, 0, 435, 406, 1, 0, 0, 0, 435, 418, 1, 0, 0, 0, 435, 427, 1, 0, 0, 0, 436, 63, 1, 0, 0, 0, 437, 438, 5, 59, 0, 0, 438, 439, 3, 106, 53, 0, 439, 65, 1, 0, 0, 0, 440, 441, 5, 79, 0, 0, 441, 442, 3, 84, 42, 0, 442, 67, 1, 0, 0, 0, 443, 444, 6, 34, -1, 0, 444, 446, 3, 126, 63, 0, 445, 447, 5, 27, 0, 0, 446, 445, 1, 0, 0, 0, 446, 447, 1, 0, 0, 0, 447, 449, 1, 0, 0, 0, 448, 450, 3, 76, 38, 0, 449, 448, 1, 0, 0, 0, 449, 450, 1, 0, 0, 0, 450, 456, 1, 0, 0, 0, 451, 452, 5, 126, 0, 0, 452, 453, 3, 68, 34, 0, 453, 454, 5, 144, 0, 0, 454, 456, 1, 0, 0, 0, 455, 443, 1, 0, 0, 0, 455, 451, 1, 0, 0, 0, 456, 471, 1, 0, 0, 0, 457, 458, 10, 3, 0, 0, 458, 459, 3, 72, 36, 0, 459, 460, 3, 68, 34, 4, 460, 470, 1, 0, 0, 0, 461, 463, 10, 4, 0, 0, 462, 464, 3, 70, 35, 0, 463, 462, 1, 0, 0, 0, 463, 464, 1, 0, 0, 0, 464, 465, 1, 0, 0, 0, 465, 466, 5, 45, 0, 0, 466, 467, 3, 68, 34, 0, 467, 468, 3, 74, 37, 0, 468, 470, 1, 0, 0, 0, 469, 457, 1, 0, 0, 0, 469, 461, 1, 0, 0, 0, 470, 473, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 471, 472, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 474, 476, 7, 2, 0, 0, 475, 474, 1, 0, 0, 0, 475, 476, 1, 0, 0, 0, 476, 477, 1, 0, 0, 0, 477, 484, 5, 42, 0, 0, 478, 480, 5, 42, 0, 0, 479, 481, 7, 2, 0, 0, 480, 479, 1, 0, 0, 0, 480, 481, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 484, 7, 2, 0, 0, 483, 475, 1, 0, 0, 0, 483, 478, 1, 0, 0, 0, 483, 482, 1, 0, 0, 0, 484, 518, 1, 0, 0, 0, 485, 487, 7, 3, 0, 0, 486, 485, 1, 0, 0, 0, 486, 487, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 490, 7, 4, 0, 0, 489, 491, 5, 63, 0, 0, 490, 489, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 500, 1, 0, 0, 0, 492, 494, 7, 4, 0, 0, 493, 495, 5, 63, 0, 0, 494, 493, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 497, 1, 0, 0, 0, 496, 498, 7, 3, 0, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 500, 1, 0, 0, 0, 499, 486, 1, 0, 0, 0, 499, 492, 1, 0, 0, 0, 500, 518, 1, 0, 0, 0, 501, 503, 7, 5, 0, 0, 502, 501, 1, 0, 0, 0, 502, 503, 1, 0, 0, 0, 503, 504, 1, 0, 0, 0, 504, 506, 5, 33, 0, 0, 505, 507, 5, 63, 0, 0, 506, 505, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 516, 1, 0, 0, 0, 508, 510, 5, 33, 0, 0, 509, 511, 5, 63, 0, 0, 510, 509, 1, 0, 0, 0, 510, 511, 1, 0, 0, 0, 511, 513, 1, 0, 0, 0, 512, 514, 7, 5, 0, 0, 513, 512, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 516, 1, 0, 0, 0, 515, 502, 1, 0, 0, 0, 515, 508, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 483, 1, 0, 0, 0, 517, 499, 1, 0, 0, 0, 517, 515, 1, 0, 0, 0, 518, 71, 1, 0, 0, 0, 519, 520, 5, 16, 0, 0, 520, 523, 5, 45, 0, 0, 521, 523, 5, 112, 0, 0, 522, 519, 1, 0, 0, 0, 522, 521, 1, 0, 0, 0, 523, 73, 1, 0, 0, 0, 524, 525, 5, 60, 0, 0, 525, 534, 3, 104, 52, 0, 526, 527, 5, 92, 0, 0, 527, 528, 5, 126, 0, 0, 528, 529, 3, 104, 52, 0, 529, 530, 5, 144, 0, 0, 530, 534, 1, 0, 0, 0, 531, 532, 5, 92, 0, 0, 532, 534, 3, 104, 52, 0, 533, 524, 1, 0, 0, 0, 533, 526, 1, 0, 0, 0, 533, 531, 1, 0, 0, 0, 534, 75, 1, 0, 0, 0, 535, 536, 5, 75, 0, 0, 536, 539, 3, 82, 41, 0, 537, 538, 5, 59, 0, 0, 538, 540, 3, 82, 41, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 77, 1, 0, 0, 0, 541, 546, 3, 80, 40, 0, 542, 543, 5, 112, 0, 0, 543, 545, 3, 80, 40, 0, 544, 542, 1, 0, 0, 0, 545, 548, 1, 0, 0, 0, 546, 544, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 79, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 549, 551, 3, 106, 53, 0, 550, 552, 7, 6, 0, 0, 551, 550, 1, 0, 0, 0, 551, 552, 1, 0, 0, 0, 552, 555, 1, 0, 0, 0, 553, 554, 5, 58, 0, 0, 554, 556, 7, 7, 0, 0, 555, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 559, 1, 0, 0, 0, 557, 558, 5, 15, 0, 0, 558, 560, 5, 106, 0, 0, 559, 557, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 81, 1, 0, 0, 0, 561, 568, 3, 154, 77, 0, 562, 565, 3, 138, 69, 0, 563, 564, 5, 146, 0, 0, 564, 566, 3, 138, 69, 0, 565, 563, 1, 0, 0, 0, 565, 566, 1, 0, 0, 0, 566, 568, 1, 0, 0, 0, 567, 561, 1, 0, 0, 0, 567, 562, 1, 0, 0, 0, 568, 83, 1, 0, 0, 0, 569, 574, 3, 86, 43, 0, 570, 571, 5, 112, 0, 0, 571, 573, 3, 86, 43, 0, 572, 570, 1, 0, 0, 0, 573, 576, 1, 0, 0, 0, 574, 572, 1, 0, 0, 0, 574, 575, 1, 0, 0, 0, 575, 85, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 577, 578, 3, 150, 75, 0, 578, 579, 5, 118, 0, 0, 579, 580, 3, 140, 70, 0, 580, 87, 1, 0, 0, 0, 581, 583, 3, 90, 45, 0, 582, 581, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 585, 1, 0, 0, 0, 584, 586, 3, 92, 46, 0, 585, 584, 1, 0, 0, 0, 585, 586, 1, 0, 0, 0, 586, 588, 1, 0, 0, 0, 587, 589, 3, 94, 47, 0, 588, 587, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 89, 1, 0, 0, 0, 590, 591, 5, 65, 0, 0, 591, 592, 5, 11, 0, 0, 592, 593, 3, 104, 52, 0, 593, 91, 1, 0, 0, 0, 594, 595, 5, 62, 0, 0, 595, 596, 5, 11, 0, 0, 596, 597, 3, 78, 39, 0, 597, 93, 1, 0, 0, 0, 598, 599, 7, 8, 0, 0, 599, 600, 3, 96, 48, 0, 600, 95, 1, 0, 0, 0, 601, 608, 3, 98, 49, 0, 602, 603, 5, 9, 0, 0, 603, 604, 3, 98, 49, 0, 604, 605, 5, 2, 0, 0, 605, 606, 3, 98, 49, 0, 606, 608, 1, 0, 0, 0, 607, 601, 1, 0, 0, 0, 607, 602, 1, 0, 0, 0, 608, 97, 1, 0, 0, 0, 609, 610, 5, 18, 0, 0, 610, 622, 5, 73, 0, 0, 611, 612, 5, 90, 0, 0, 612, 622, 5, 66, 0, 0, 613, 614, 5, 90, 0, 0, 614, 622, 5, 30, 0, 0, 615, 616, 3, 138, 69, 0, 616, 617, 5, 66, 0, 0, 617, 622, 1, 0, 0, 0, 618, 619, 3, 138, 69, 0, 619, 620, 5, 30, 0, 0, 620, 622, 1, 0, 0, 0, 621, 609, 1, 0, 0, 0, 621, 611, 1, 0, 0, 0, 621, 613, 1, 0, 0, 0, 621, 615, 1, 0, 0, 0, 621, 618, 1, 0, 0, 0, 622, 99, 1, 0, 0, 0, 623, 624, 3, 106, 53, 0, 624, 625, 5, 0, 0, 1, 625, 101, 1, 0, 0, 0, 626, 674, 3, 150, 75, 0, 627, 628, 3, 150, 75, 0, 628, 629, 5, 126, 0, 0, 629, 630, 3, 150, 75, 0, 630, 637, 3, 102, 51, 0, 631, 632, 5, 112, 0, 0, 632, 633, 3, 150, 75, 0, 633, 634, 3, 102, 51, 0, 634, 636, 1, 0, 0, 0, 635, 631, 1, 0, 0, 0, 636, 639, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 637, 638, 1, 0, 0, 0, 638, 640, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 640, 641, 5, 144, 0, 0, 641, 674, 1, 0, 0, 0, 642, 643, 3, 150, 75, 0, 643, 644, 5, 126, 0, 0, 644, 649, 3, 152, 76, 0, 645, 646, 5, 112, 0, 0, 646, 648, 3, 152, 76, 0, 647, 645, 1, 0, 0, 0, 648, 651, 1, 0, 0, 0, 649, 647, 1, 0, 0, 0, 649, 650, 1, 0, 0, 0, 650, 652, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 652, 653, 5, 144, 0, 0, 653, 674, 1, 0, 0, 0, 654, 655, 3, 150, 75, 0, 655, 656, 5, 126, 0, 0, 656, 661, 3, 102, 51, 0, 657, 658, 5, 112, 0, 0, 658, 660, 3, 102, 51, 0, 659, 657, 1, 0, 0, 0, 660, 663, 1, 0, 0, 0, 661, 659, 1, 0, 0, 0, 661, 662, 1, 0, 0, 0, 662, 664, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 664, 665, 5, 144, 0, 0, 665, 674, 1, 0, 0, 0, 666, 667, 3, 150, 75, 0, 667, 669, 5, 126, 0, 0, 668, 670, 3, 104, 52, 0, 669, 668, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 672, 5, 144, 0, 0, 672, 674, 1, 0, 0, 0, 673, 626, 1, 0, 0, 0, 673, 627, 1, 0, 0, 0, 673, 642, 1, 0, 0, 0, 673, 654, 1, 0, 0, 0, 673, 666, 1, 0, 0, 0, 674, 103, 1, 0, 0, 0, 675, 680, 3, 106, 53, 0, 676, 677, 5, 112, 0, 0, 677, 679, 3, 106, 53, 0, 678, 676, 1, 0, 0, 0, 679, 682, 1, 0, 0, 0, 680, 678, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 681, 105, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 683, 684, 6, 53, -1, 0, 684, 686, 5, 12, 0, 0, 685, 687, 3, 106, 53, 0, 686, 685, 1, 0, 0, 0, 686, 687, 1, 0, 0, 0, 687, 693, 1, 0, 0, 0, 688, 689, 5, 94, 0, 0, 689, 690, 3, 106, 53, 0, 690, 691, 5, 81, 0, 0, 691, 692, 3, 106, 53, 0, 692, 694, 1, 0, 0, 0, 693, 688, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 693, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 699, 1, 0, 0, 0, 697, 698, 5, 24, 0, 0, 698, 700, 3, 106, 53, 0, 699, 697, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 702, 5, 25, 0, 0, 702, 833, 1, 0, 0, 0, 703, 704, 5, 13, 0, 0, 704, 705, 5, 126, 0, 0, 705, 706, 3, 106, 53, 0, 706, 707, 5, 6, 0, 0, 707, 708, 3, 102, 51, 0, 708, 709, 5, 144, 0, 0, 709, 833, 1, 0, 0, 0, 710, 711, 5, 19, 0, 0, 711, 833, 5, 106, 0, 0, 712, 713, 5, 43, 0, 0, 713, 714, 3, 106, 53, 0, 714, 715, 3, 142, 71, 0, 715, 833, 1, 0, 0, 0, 716, 717, 5, 80, 0, 0, 717, 718, 5, 126, 0, 0, 718, 719, 3, 106, 53, 0, 719, 720, 5, 32, 0, 0, 720, 723, 3, 106, 53, 0, 721, 722, 5, 31, 0, 0, 722, 724, 3, 106, 53, 0, 723, 721, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 144, 0, 0, 726, 833, 1, 0, 0, 0, 727, 728, 5, 83, 0, 0, 728, 833, 5, 106, 0, 0, 729, 730, 5, 88, 0, 0, 730, 731, 5, 126, 0, 0, 731, 732, 7, 9, 0, 0, 732, 733, 3, 156, 78, 0, 733, 734, 5, 32, 0, 0, 734, 735, 3, 106, 53, 0, 735, 736, 5, 144, 0, 0, 736, 833, 1, 0, 0, 0, 737, 738, 3, 150, 75, 0, 738, 740, 5, 126, 0, 0, 739, 741, 3, 104, 52, 0, 740, 739, 1, 0, 0, 0, 740, 741, 1, 0, 0, 0, 741, 742, 1, 0, 0, 0, 742, 743, 5, 144, 0, 0, 743, 752, 1, 0, 0, 0, 744, 746, 5, 126, 0, 0, 745, 747, 5, 23, 0, 0, 746, 745, 1, 0, 0, 0, 746, 747, 1, 0, 0, 0, 747, 749, 1, 0, 0, 0, 748, 750, 3, 108, 54, 0, 749, 748, 1, 0, 0, 0, 749, 750, 1, 0, 0, 0, 750, 751, 1, 0, 0, 0, 751, 753, 5, 144, 0, 0, 752, 744, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 754, 1, 0, 0, 0, 754, 755, 5, 64, 0, 0, 755, 756, 5, 126, 0, 0, 756, 757, 3, 88, 44, 0, 757, 758, 5, 144, 0, 0, 758, 833, 1, 0, 0, 0, 759, 760, 3, 150, 75, 0, 760, 762, 5, 126, 0, 0, 761, 763, 3, 104, 52, 0, 762, 761, 1, 0, 0, 0, 762, 763, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 765, 5, 144, 0, 0, 765, 774, 1, 0, 0, 0, 766, 768, 5, 126, 0, 0, 767, 769, 5, 23, 0, 0, 768, 767, 1, 0, 0, 0, 768, 769, 1, 0, 0, 0, 769, 771, 1, 0, 0, 0, 770, 772, 3, 108, 54, 0, 771, 770, 1, 0, 0, 0, 771, 772, 1, 0, 0, 0, 772, 773, 1, 0, 0, 0, 773, 775, 5, 144, 0, 0, 774, 766, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 776, 1, 0, 0, 0, 776, 777, 5, 64, 0, 0, 777, 778, 3, 150, 75, 0, 778, 833, 1, 0, 0, 0, 779, 785, 3, 150, 75, 0, 780, 782, 5, 126, 0, 0, 781, 783, 3, 104, 52, 0, 782, 781, 1, 0, 0, 0, 782, 783, 1, 0, 0, 0, 783, 784, 1, 0, 0, 0, 784, 786, 5, 144, 0, 0, 785, 780, 1, 0, 0, 0, 785, 786, 1, 0, 0, 0, 786, 787, 1, 0, 0, 0, 787, 789, 5, 126, 0, 0, 788, 790, 5, 23, 0, 0, 789, 788, 1, 0, 0, 0, 789, 790, 1, 0, 0, 0, 790, 792, 1, 0, 0, 0, 791, 793, 3, 108, 54, 0, 792, 791, 1, 0, 0, 0, 792, 793, 1, 0, 0, 0, 793, 794, 1, 0, 0, 0, 794, 795, 5, 144, 0, 0, 795, 833, 1, 0, 0, 0, 796, 833, 3, 114, 57, 0, 797, 833, 3, 158, 79, 0, 798, 833, 3, 140, 70, 0, 799, 800, 5, 114, 0, 0, 800, 833, 3, 106, 53, 19, 801, 802, 5, 56, 0, 0, 802, 833, 3, 106, 53, 13, 803, 804, 3, 130, 65, 0, 804, 805, 5, 116, 0, 0, 805, 807, 1, 0, 0, 0, 806, 803, 1, 0, 0, 0, 806, 807, 1, 0, 0, 0, 807, 808, 1, 0, 0, 0, 808, 833, 5, 108, 0, 0, 809, 810, 5, 126, 0, 0, 810, 811, 3, 34, 17, 0, 811, 812, 5, 144, 0, 0, 812, 833, 1, 0, 0, 0, 813, 814, 5, 126, 0, 0, 814, 815, 3, 106, 53, 0, 815, 816, 5, 144, 0, 0, 816, 833, 1, 0, 0, 0, 817, 818, 5, 126, 0, 0, 818, 819, 3, 104, 52, 0, 819, 820, 5, 144, 0, 0, 820, 833, 1, 0, 0, 0, 821, 823, 5, 125, 0, 0, 822, 824, 3, 104, 52, 0, 823, 822, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 825, 1, 0, 0, 0, 825, 833, 5, 143, 0, 0, 826, 828, 5, 124, 0, 0, 827, 829, 3, 30, 15, 0, 828, 827, 1, 0, 0, 0, 828, 829, 1, 0, 0, 0, 829, 830, 1, 0, 0, 0, 830, 833, 5, 142, 0, 0, 831, 833, 3, 122, 61, 0, 832, 683, 1, 0, 0, 0, 832, 703, 1, 0, 0, 0, 832, 710, 1, 0, 0, 0, 832, 712, 1, 0, 0, 0, 832, 716, 1, 0, 0, 0, 832, 727, 1, 0, 0, 0, 832, 729, 1, 0, 0, 0, 832, 737, 1, 0, 0, 0, 832, 759, 1, 0, 0, 0, 832, 779, 1, 0, 0, 0, 832, 796, 1, 0, 0, 0, 832, 797, 1, 0, 0, 0, 832, 798, 1, 0, 0, 0, 832, 799, 1, 0, 0, 0, 832, 801, 1, 0, 0, 0, 832, 806, 1, 0, 0, 0, 832, 809, 1, 0, 0, 0, 832, 813, 1, 0, 0, 0, 832, 817, 1, 0, 0, 0, 832, 821, 1, 0, 0, 0, 832, 826, 1, 0, 0, 0, 832, 831, 1, 0, 0, 0, 833, 927, 1, 0, 0, 0, 834, 838, 10, 18, 0, 0, 835, 839, 5, 108, 0, 0, 836, 839, 5, 146, 0, 0, 837, 839, 5, 133, 0, 0, 838, 835, 1, 0, 0, 0, 838, 836, 1, 0, 0, 0, 838, 837, 1, 0, 0, 0, 839, 840, 1, 0, 0, 0, 840, 926, 3, 106, 53, 19, 841, 845, 10, 17, 0, 0, 842, 846, 5, 134, 0, 0, 843, 846, 5, 114, 0, 0, 844, 846, 5, 113, 0, 0, 845, 842, 1, 0, 0, 0, 845, 843, 1, 0, 0, 0, 845, 844, 1, 0, 0, 0, 846, 847, 1, 0, 0, 0, 847, 926, 3, 106, 53, 18, 848, 873, 10, 16, 0, 0, 849, 874, 5, 117, 0, 0, 850, 874, 5, 118, 0, 0, 851, 874, 5, 129, 0, 0, 852, 874, 5, 127, 0, 0, 853, 874, 5, 128, 0, 0, 854, 874, 5, 119, 0, 0, 855, 874, 5, 120, 0, 0, 856, 858, 5, 56, 0, 0, 857, 856, 1, 0, 0, 0, 857, 858, 1, 0, 0, 0, 858, 859, 1, 0, 0, 0, 859, 861, 5, 40, 0, 0, 860, 862, 5, 14, 0, 0, 861, 860, 1, 0, 0, 0, 861, 862, 1, 0, 0, 0, 862, 874, 1, 0, 0, 0, 863, 865, 5, 56, 0, 0, 864, 863, 1, 0, 0, 0, 864, 865, 1, 0, 0, 0, 865, 866, 1, 0, 0, 0, 866, 874, 7, 10, 0, 0, 867, 874, 5, 140, 0, 0, 868, 874, 5, 141, 0, 0, 869, 874, 5, 131, 0, 0, 870, 874, 5, 122, 0, 0, 871, 874, 5, 123, 0, 0, 872, 874, 5, 130, 0, 0, 873, 849, 1, 0, 0, 0, 873, 850, 1, 0, 0, 0, 873, 851, 1, 0, 0, 0, 873, 852, 1, 0, 0, 0, 873, 853, 1, 0, 0, 0, 873, 854, 1, 0, 0, 0, 873, 855, 1, 0, 0, 0, 873, 857, 1, 0, 0, 0, 873, 864, 1, 0, 0, 0, 873, 867, 1, 0, 0, 0, 873, 868, 1, 0, 0, 0, 873, 869, 1, 0, 0, 0, 873, 870, 1, 0, 0, 0, 873, 871, 1, 0, 0, 0, 873, 872, 1, 0, 0, 0, 874, 875, 1, 0, 0, 0, 875, 926, 3, 106, 53, 17, 876, 877, 10, 14, 0, 0, 877, 878, 5, 132, 0, 0, 878, 926, 3, 106, 53, 15, 879, 880, 10, 12, 0, 0, 880, 881, 5, 2, 0, 0, 881, 926, 3, 106, 53, 13, 882, 883, 10, 11, 0, 0, 883, 884, 5, 61, 0, 0, 884, 926, 3, 106, 53, 12, 885, 887, 10, 10, 0, 0, 886, 888, 5, 56, 0, 0, 887, 886, 1, 0, 0, 0, 887, 888, 1, 0, 0, 0, 888, 889, 1, 0, 0, 0, 889, 890, 5, 9, 0, 0, 890, 891, 3, 106, 53, 0, 891, 892, 5, 2, 0, 0, 892, 893, 3, 106, 53, 11, 893, 926, 1, 0, 0, 0, 894, 895, 10, 9, 0, 0, 895, 896, 5, 135, 0, 0, 896, 897, 3, 106, 53, 0, 897, 898, 5, 111, 0, 0, 898, 899, 3, 106, 53, 9, 899, 926, 1, 0, 0, 0, 900, 901, 10, 22, 0, 0, 901, 902, 5, 125, 0, 0, 902, 903, 3, 106, 53, 0, 903, 904, 5, 143, 0, 0, 904, 926, 1, 0, 0, 0, 905, 906, 10, 21, 0, 0, 906, 907, 5, 116, 0, 0, 907, 926, 5, 104, 0, 0, 908, 909, 10, 20, 0, 0, 909, 910, 5, 116, 0, 0, 910, 926, 3, 150, 75, 0, 911, 912, 10, 15, 0, 0, 912, 914, 5, 44, 0, 0, 913, 915, 5, 56, 0, 0, 914, 913, 1, 0, 0, 0, 914, 915, 1, 0, 0, 0, 915, 916, 1, 0, 0, 0, 916, 926, 5, 57, 0, 0, 917, 923, 10, 8, 0, 0, 918, 924, 3, 148, 74, 0, 919, 920, 5, 6, 0, 0, 920, 924, 3, 150, 75, 0, 921, 922, 5, 6, 0, 0, 922, 924, 5, 106, 0, 0, 923, 918, 1, 0, 0, 0, 923, 919, 1, 0, 0, 0, 923, 921, 1, 0, 0, 0, 924, 926, 1, 0, 0, 0, 925, 834, 1, 0, 0, 0, 925, 841, 1, 0, 0, 0, 925, 848, 1, 0, 0, 0, 925, 876, 1, 0, 0, 0, 925, 879, 1, 0, 0, 0, 925, 882, 1, 0, 0, 0, 925, 885, 1, 0, 0, 0, 925, 894, 1, 0, 0, 0, 925, 900, 1, 0, 0, 0, 925, 905, 1, 0, 0, 0, 925, 908, 1, 0, 0, 0, 925, 911, 1, 0, 0, 0, 925, 917, 1, 0, 0, 0, 926, 929, 1, 0, 0, 0, 927, 925, 1, 0, 0, 0, 927, 928, 1, 0, 0, 0, 928, 107, 1, 0, 0, 0, 929, 927, 1, 0, 0, 0, 930, 935, 3, 110, 55, 0, 931, 932, 5, 112, 0, 0, 932, 934, 3, 110, 55, 0, 933, 931, 1, 0, 0, 0, 934, 937, 1, 0, 0, 0, 935, 933, 1, 0, 0, 0, 935, 936, 1, 0, 0, 0, 936, 109, 1, 0, 0, 0, 937, 935, 1, 0, 0, 0, 938, 941, 3, 112, 56, 0, 939, 941, 3, 106, 53, 0, 940, 938, 1, 0, 0, 0, 940, 939, 1, 0, 0, 0, 941, 111, 1, 0, 0, 0, 942, 943, 5, 126, 0, 0, 943, 948, 3, 150, 75, 0, 944, 945, 5, 112, 0, 0, 945, 947, 3, 150, 75, 0, 946, 944, 1, 0, 0, 0, 947, 950, 1, 0, 0, 0, 948, 946, 1, 0, 0, 0, 948, 949, 1, 0, 0, 0, 949, 951, 1, 0, 0, 0, 950, 948, 1, 0, 0, 0, 951, 952, 5, 144, 0, 0, 952, 962, 1, 0, 0, 0, 953, 958, 3, 150, 75, 0, 954, 955, 5, 112, 0, 0, 955, 957, 3, 150, 75, 0, 956, 954, 1, 0, 0, 0, 957, 960, 1, 0, 0, 0, 958, 956, 1, 0, 0, 0, 958, 959, 1, 0, 0, 0, 959, 962, 1, 0, 0, 0, 960, 958, 1, 0, 0, 0, 961, 942, 1, 0, 0, 0, 961, 953, 1, 0, 0, 0, 962, 963, 1, 0, 0, 0, 963, 964, 5, 107, 0, 0, 964, 965, 3, 106, 53, 0, 965, 113, 1, 0, 0, 0, 966, 967, 5, 128, 0, 0, 967, 971, 3, 150, 75, 0, 968, 970, 3, 116, 58, 0, 969, 968, 1, 0, 0, 0, 970, 973, 1, 0, 0, 0, 971, 969, 1, 0, 0, 0, 971, 972, 1, 0, 0, 0, 972, 974, 1, 0, 0, 0, 973, 971, 1, 0, 0, 0, 974, 975, 5, 146, 0, 0, 975, 976, 5, 120, 0, 0, 976, 995, 1, 0, 0, 0, 977, 978, 5, 128, 0, 0, 978, 982, 3, 150, 75, 0, 979, 981, 3, 116, 58, 0, 980, 979, 1, 0, 0, 0, 981, 984, 1, 0, 0, 0, 982, 980, 1, 0, 0, 0, 982, 983, 1, 0, 0, 0, 983, 985, 1, 0, 0, 0, 984, 982, 1, 0, 0, 0, 985, 987, 5, 120, 0, 0, 986, 988, 3, 114, 57, 0, 987, 986, 1, 0, 0, 0, 987, 988, 1, 0, 0, 0, 988, 989, 1, 0, 0, 0, 989, 990, 5, 128, 0, 0, 990, 991, 5, 146, 0, 0, 991, 992, 3, 150, 75, 0, 992, 993, 5, 120, 0, 0, 993, 995, 1, 0, 0, 0, 994, 966, 1, 0, 0, 0, 994, 977, 1, 0, 0, 0, 995, 115, 1, 0, 0, 0, 996, 997, 3, 150, 75, 0, 997, 998, 5, 118, 0, 0, 998, 999, 3, 156, 78, 0, 999, 1008, 1, 0, 0, 0, 1000, 1001, 3, 150, 75, 0, 1001, 1002, 5, 118, 0, 0, 1002, 1003, 5, 124, 0, 0, 1003, 1004, 3, 106, 53, 0, 1004, 1005, 5, 142, 0, 0, 1005, 1008, 1, 0, 0, 0, 1006, 1008, 3, 150, 75, 0, 1007, 996, 1, 0, 0, 0, 1007, 1000, 1, 0, 0, 0, 1007, 1006, 1, 0, 0, 0, 1008, 117, 1, 0, 0, 0, 1009, 1014, 3, 120, 60, 0, 1010, 1011, 5, 112, 0, 0, 1011, 1013, 3, 120, 60, 0, 1012, 1010, 1, 0, 0, 0, 1013, 1016, 1, 0, 0, 0, 1014, 1012, 1, 0, 0, 0, 1014, 1015, 1, 0, 0, 0, 1015, 119, 1, 0, 0, 0, 1016, 1014, 1, 0, 0, 0, 1017, 1018, 3, 150, 75, 0, 1018, 1019, 5, 6, 0, 0, 1019, 1020, 5, 126, 0, 0, 1020, 1021, 3, 34, 17, 0, 1021, 1022, 5, 144, 0, 0, 1022, 1028, 1, 0, 0, 0, 1023, 1024, 3, 106, 53, 0, 1024, 1025, 5, 6, 0, 0, 1025, 1026, 3, 150, 75, 0, 1026, 1028, 1, 0, 0, 0, 1027, 1017, 1, 0, 0, 0, 1027, 1023, 1, 0, 0, 0, 1028, 121, 1, 0, 0, 0, 1029, 1037, 3, 154, 77, 0, 1030, 1031, 3, 130, 65, 0, 1031, 1032, 5, 116, 0, 0, 1032, 1034, 1, 0, 0, 0, 1033, 1030, 1, 0, 0, 0, 1033, 1034, 1, 0, 0, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1037, 3, 124, 62, 0, 1036, 1029, 1, 0, 0, 0, 1036, 1033, 1, 0, 0, 0, 1037, 123, 1, 0, 0, 0, 1038, 1043, 3, 150, 75, 0, 1039, 1040, 5, 116, 0, 0, 1040, 1042, 3, 150, 75, 0, 1041, 1039, 1, 0, 0, 0, 1042, 1045, 1, 0, 0, 0, 1043, 1041, 1, 0, 0, 0, 1043, 1044, 1, 0, 0, 0, 1044, 125, 1, 0, 0, 0, 1045, 1043, 1, 0, 0, 0, 1046, 1047, 6, 63, -1, 0, 1047, 1056, 3, 130, 65, 0, 1048, 1056, 3, 128, 64, 0, 1049, 1050, 5, 126, 0, 0, 1050, 1051, 3, 34, 17, 0, 1051, 1052, 5, 144, 0, 0, 1052, 1056, 1, 0, 0, 0, 1053, 1056, 3, 114, 57, 0, 1054, 1056, 3, 154, 77, 0, 1055, 1046, 1, 0, 0, 0, 1055, 1048, 1, 0, 0, 0, 1055, 1049, 1, 0, 0, 0, 1055, 1053, 1, 0, 0, 0, 1055, 1054, 1, 0, 0, 0, 1056, 1065, 1, 0, 0, 0, 1057, 1061, 10, 3, 0, 0, 1058, 1062, 3, 148, 74, 0, 1059, 1060, 5, 6, 0, 0, 1060, 1062, 3, 150, 75, 0, 1061, 1058, 1, 0, 0, 0, 1061, 1059, 1, 0, 0, 0, 1062, 1064, 1, 0, 0, 0, 1063, 1057, 1, 0, 0, 0, 1064, 1067, 1, 0, 0, 0, 1065, 1063, 1, 0, 0, 0, 1065, 1066, 1, 0, 0, 0, 1066, 127, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1068, 1069, 3, 150, 75, 0, 1069, 1071, 5, 126, 0, 0, 1070, 1072, 3, 132, 66, 0, 1071, 1070, 1, 0, 0, 0, 1071, 1072, 1, 0, 0, 0, 1072, 1073, 1, 0, 0, 0, 1073, 1074, 5, 144, 0, 0, 1074, 129, 1, 0, 0, 0, 1075, 1076, 3, 134, 67, 0, 1076, 1077, 5, 116, 0, 0, 1077, 1079, 1, 0, 0, 0, 1078, 1075, 1, 0, 0, 0, 1078, 1079, 1, 0, 0, 0, 1079, 1080, 1, 0, 0, 0, 1080, 1081, 3, 150, 75, 0, 1081, 131, 1, 0, 0, 0, 1082, 1087, 3, 106, 53, 0, 1083, 1084, 5, 112, 0, 0, 1084, 1086, 3, 106, 53, 0, 1085, 1083, 1, 0, 0, 0, 1086, 1089, 1, 0, 0, 0, 1087, 1085, 1, 0, 0, 0, 1087, 1088, 1, 0, 0, 0, 1088, 133, 1, 0, 0, 0, 1089, 1087, 1, 0, 0, 0, 1090, 1091, 3, 150, 75, 0, 1091, 135, 1, 0, 0, 0, 1092, 1101, 5, 102, 0, 0, 1093, 1094, 5, 116, 0, 0, 1094, 1101, 7, 11, 0, 0, 1095, 1096, 5, 104, 0, 0, 1096, 1098, 5, 116, 0, 0, 1097, 1099, 7, 11, 0, 0, 1098, 1097, 1, 0, 0, 0, 1098, 1099, 1, 0, 0, 0, 1099, 1101, 1, 0, 0, 0, 1100, 1092, 1, 0, 0, 0, 1100, 1093, 1, 0, 0, 0, 1100, 1095, 1, 0, 0, 0, 1101, 137, 1, 0, 0, 0, 1102, 1104, 7, 12, 0, 0, 1103, 1102, 1, 0, 0, 0, 1103, 1104, 1, 0, 0, 0, 1104, 1111, 1, 0, 0, 0, 1105, 1112, 3, 136, 68, 0, 1106, 1112, 5, 103, 0, 0, 1107, 1112, 5, 104, 0, 0, 1108, 1112, 5, 105, 0, 0, 1109, 1112, 5, 41, 0, 0, 1110, 1112, 5, 55, 0, 0, 1111, 1105, 1, 0, 0, 0, 1111, 1106, 1, 0, 0, 0, 1111, 1107, 1, 0, 0, 0, 1111, 1108, 1, 0, 0, 0, 1111, 1109, 1, 0, 0, 0, 1111, 1110, 1, 0, 0, 0, 1112, 139, 1, 0, 0, 0, 1113, 1117, 3, 138, 69, 0, 1114, 1117, 5, 106, 0, 0, 1115, 1117, 5, 57, 0, 0, 1116, 1113, 1, 0, 0, 0, 1116, 1114, 1, 0, 0, 0, 1116, 1115, 1, 0, 0, 0, 1117, 141, 1, 0, 0, 0, 1118, 1119, 7, 13, 0, 0, 1119, 143, 1, 0, 0, 0, 1120, 1121, 7, 14, 0, 0, 1121, 145, 1, 0, 0, 0, 1122, 1123, 7, 15, 0, 0, 1123, 147, 1, 0, 0, 0, 1124, 1127, 5, 101, 0, 0, 1125, 1127, 3, 146, 73, 0, 1126, 1124, 1, 0, 0, 0, 1126, 1125, 1, 0, 0, 0, 1127, 149, 1, 0, 0, 0, 1128, 1132, 5, 101, 0, 0, 1129, 1132, 3, 142, 71, 0, 1130, 1132, 3, 144, 72, 0, 1131, 1128, 1, 0, 0, 0, 1131, 1129, 1, 0, 0, 0, 1131, 1130, 1, 0, 0, 0, 1132, 151, 1, 0, 0, 0, 1133, 1134, 3, 156, 78, 0, 1134, 1135, 5, 118, 0, 0, 1135, 1136, 3, 138, 69, 0, 1136, 153, 1, 0, 0, 0, 1137, 1138, 5, 124, 0, 0, 1138, 1139, 3, 150, 75, 0, 1139, 1140, 5, 142, 0, 0, 1140, 155, 1, 0, 0, 0, 1141, 1144, 5, 106, 0, 0, 1142, 1144, 3, 158, 79, 0, 1143, 1141, 1, 0, 0, 0, 1143, 1142, 1, 0, 0, 0, 1144, 157, 1, 0, 0, 0, 1145, 1149, 5, 137, 0, 0, 1146, 1148, 3, 160, 80, 0, 1147, 1146, 1, 0, 0, 0, 1148, 1151, 1, 0, 0, 0, 1149, 1147, 1, 0, 0, 0, 1149, 1150, 1, 0, 0, 0, 1150, 1152, 1, 0, 0, 0, 1151, 1149, 1, 0, 0, 0, 1152, 1153, 5, 139, 0, 0, 1153, 159, 1, 0, 0, 0, 1154, 1155, 5, 152, 0, 0, 1155, 1156, 3, 106, 53, 0, 1156, 1157, 5, 142, 0, 0, 1157, 1160, 1, 0, 0, 0, 1158, 1160, 5, 151, 0, 0, 1159, 1154, 1, 0, 0, 0, 1159, 1158, 1, 0, 0, 0, 1160, 161, 1, 0, 0, 0, 1161, 1165, 5, 138, 0, 0, 1162, 1164, 3, 164, 82, 0, 1163, 1162, 1, 0, 0, 0, 1164, 1167, 1, 0, 0, 0, 1165, 1163, 1, 0, 0, 0, 1165, 1166, 1, 0, 0, 0, 1166, 1168, 1, 0, 0, 0, 1167, 1165, 1, 0, 0, 0, 1168, 1169, 5, 0, 0, 1, 1169, 163, 1, 0, 0, 0, 1170, 1171, 5, 154, 0, 0, 1171, 1172, 3, 106, 53, 0, 1172, 1173, 5, 142, 0, 0, 1173, 1176, 1, 0, 0, 0, 1174, 1176, 5, 153, 0, 0, 1175, 1170, 1, 0, 0, 0, 1175, 1174, 1, 0, 0, 0, 1176, 165, 1, 0, 0, 0, 141, 169, 176, 185, 200, 212, 224, 240, 251, 265, 271, 281, 290, 293, 297, 300, 304, 307, 310, 313, 316, 320, 324, 327, 330, 333, 337, 340, 349, 355, 376, 393, 410, 416, 422, 433, 435, 446, 449, 455, 463, 469, 471, 475, 480, 483, 486, 490, 494, 497, 499, 502, 506, 510, 513, 515, 517, 522, 533, 539, 546, 551, 555, 559, 565, 567, 574, 582, 585, 588, 607, 621, 637, 649, 661, 669, 673, 680, 686, 695, 699, 723, 740, 746, 749, 752, 762, 768, 771, 774, 782, 785, 789, 792, 806, 823, 828, 832, 838, 845, 857, 861, 864, 873, 887, 914, 923, 925, 927, 935, 940, 948, 958, 961, 971, 982, 987, 994, 1007, 1014, 1027, 1033, 1036, 1043, 1055, 1061, 1065, 1071, 1078, 1087, 1098, 1100, 1103, 1111, 1116, 1126, 1131, 1143, 1149, 1159, 1165, 1175] \ No newline at end of file +[4, 1, 154, 1179, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 4, 1, 4, 1, 4, 5, 4, 191, 8, 4, 10, 4, 12, 4, 194, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 204, 8, 5, 1, 6, 1, 6, 3, 6, 208, 8, 6, 1, 6, 3, 6, 211, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 220, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 228, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 234, 8, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 3, 11, 246, 8, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 252, 8, 13, 10, 13, 12, 13, 255, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 266, 8, 15, 10, 15, 12, 15, 269, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 274, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 282, 8, 17, 10, 17, 12, 17, 285, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 293, 8, 18, 1, 19, 3, 19, 296, 8, 19, 1, 19, 1, 19, 3, 19, 300, 8, 19, 1, 19, 3, 19, 303, 8, 19, 1, 19, 1, 19, 3, 19, 307, 8, 19, 1, 19, 3, 19, 310, 8, 19, 1, 19, 3, 19, 313, 8, 19, 1, 19, 3, 19, 316, 8, 19, 1, 19, 3, 19, 319, 8, 19, 1, 19, 1, 19, 3, 19, 323, 8, 19, 1, 19, 1, 19, 3, 19, 327, 8, 19, 1, 19, 3, 19, 330, 8, 19, 1, 19, 3, 19, 333, 8, 19, 1, 19, 3, 19, 336, 8, 19, 1, 19, 1, 19, 3, 19, 340, 8, 19, 1, 19, 3, 19, 343, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 352, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 358, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 377, 8, 24, 10, 24, 12, 24, 380, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 396, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 413, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 419, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 425, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 436, 8, 31, 3, 31, 438, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 449, 8, 34, 1, 34, 3, 34, 452, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 458, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 466, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 472, 8, 34, 10, 34, 12, 34, 475, 9, 34, 1, 35, 3, 35, 478, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 483, 8, 35, 1, 35, 3, 35, 486, 8, 35, 1, 35, 3, 35, 489, 8, 35, 1, 35, 1, 35, 3, 35, 493, 8, 35, 1, 35, 1, 35, 3, 35, 497, 8, 35, 1, 35, 3, 35, 500, 8, 35, 3, 35, 502, 8, 35, 1, 35, 3, 35, 505, 8, 35, 1, 35, 1, 35, 3, 35, 509, 8, 35, 1, 35, 1, 35, 3, 35, 513, 8, 35, 1, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 3, 35, 520, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 525, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 536, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 542, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 547, 8, 39, 10, 39, 12, 39, 550, 9, 39, 1, 40, 1, 40, 3, 40, 554, 8, 40, 1, 40, 1, 40, 3, 40, 558, 8, 40, 1, 40, 1, 40, 3, 40, 562, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 568, 8, 41, 3, 41, 570, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 575, 8, 42, 10, 42, 12, 42, 578, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 585, 8, 44, 1, 44, 3, 44, 588, 8, 44, 1, 44, 3, 44, 591, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 610, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 624, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 638, 8, 51, 10, 51, 12, 51, 641, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 650, 8, 51, 10, 51, 12, 51, 653, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 662, 8, 51, 10, 51, 12, 51, 665, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 672, 8, 51, 1, 51, 1, 51, 3, 51, 676, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 681, 8, 52, 10, 52, 12, 52, 684, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 689, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 696, 8, 53, 11, 53, 12, 53, 697, 1, 53, 1, 53, 3, 53, 702, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 726, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 743, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 749, 8, 53, 1, 53, 3, 53, 752, 8, 53, 1, 53, 3, 53, 755, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 765, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 771, 8, 53, 1, 53, 3, 53, 774, 8, 53, 1, 53, 3, 53, 777, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 785, 8, 53, 1, 53, 3, 53, 788, 8, 53, 1, 53, 1, 53, 3, 53, 792, 8, 53, 1, 53, 3, 53, 795, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 809, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 826, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 831, 8, 53, 1, 53, 1, 53, 3, 53, 835, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 841, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 848, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 860, 8, 53, 1, 53, 1, 53, 3, 53, 864, 8, 53, 1, 53, 3, 53, 867, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 876, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 890, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 917, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 925, 8, 53, 5, 53, 927, 8, 53, 10, 53, 12, 53, 930, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 935, 8, 54, 10, 54, 12, 54, 938, 9, 54, 1, 55, 1, 55, 3, 55, 942, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 948, 8, 56, 10, 56, 12, 56, 951, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 958, 8, 56, 10, 56, 12, 56, 961, 9, 56, 3, 56, 963, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 971, 8, 57, 10, 57, 12, 57, 974, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 982, 8, 57, 10, 57, 12, 57, 985, 9, 57, 1, 57, 1, 57, 3, 57, 989, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 996, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 1009, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 1014, 8, 59, 10, 59, 12, 59, 1017, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1029, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1035, 8, 61, 1, 61, 3, 61, 1038, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1043, 8, 62, 10, 62, 12, 62, 1046, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1057, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1063, 8, 63, 5, 63, 1065, 8, 63, 10, 63, 12, 63, 1068, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1073, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1080, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1087, 8, 66, 10, 66, 12, 66, 1090, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1100, 8, 68, 3, 68, 1102, 8, 68, 1, 69, 3, 69, 1105, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1113, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1118, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1128, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1133, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1145, 8, 78, 1, 79, 1, 79, 5, 79, 1149, 8, 79, 10, 79, 12, 79, 1152, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1161, 8, 80, 1, 81, 1, 81, 5, 81, 1165, 8, 81, 10, 81, 12, 81, 1168, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1177, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1317, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 187, 1, 0, 0, 0, 10, 203, 1, 0, 0, 0, 12, 205, 1, 0, 0, 0, 14, 212, 1, 0, 0, 0, 16, 221, 1, 0, 0, 0, 18, 229, 1, 0, 0, 0, 20, 238, 1, 0, 0, 0, 22, 243, 1, 0, 0, 0, 24, 247, 1, 0, 0, 0, 26, 249, 1, 0, 0, 0, 28, 258, 1, 0, 0, 0, 30, 262, 1, 0, 0, 0, 32, 273, 1, 0, 0, 0, 34, 277, 1, 0, 0, 0, 36, 292, 1, 0, 0, 0, 38, 295, 1, 0, 0, 0, 40, 344, 1, 0, 0, 0, 42, 347, 1, 0, 0, 0, 44, 353, 1, 0, 0, 0, 46, 357, 1, 0, 0, 0, 48, 363, 1, 0, 0, 0, 50, 381, 1, 0, 0, 0, 52, 384, 1, 0, 0, 0, 54, 387, 1, 0, 0, 0, 56, 397, 1, 0, 0, 0, 58, 400, 1, 0, 0, 0, 60, 404, 1, 0, 0, 0, 62, 437, 1, 0, 0, 0, 64, 439, 1, 0, 0, 0, 66, 442, 1, 0, 0, 0, 68, 457, 1, 0, 0, 0, 70, 519, 1, 0, 0, 0, 72, 524, 1, 0, 0, 0, 74, 535, 1, 0, 0, 0, 76, 537, 1, 0, 0, 0, 78, 543, 1, 0, 0, 0, 80, 551, 1, 0, 0, 0, 82, 569, 1, 0, 0, 0, 84, 571, 1, 0, 0, 0, 86, 579, 1, 0, 0, 0, 88, 584, 1, 0, 0, 0, 90, 592, 1, 0, 0, 0, 92, 596, 1, 0, 0, 0, 94, 600, 1, 0, 0, 0, 96, 609, 1, 0, 0, 0, 98, 623, 1, 0, 0, 0, 100, 625, 1, 0, 0, 0, 102, 675, 1, 0, 0, 0, 104, 677, 1, 0, 0, 0, 106, 834, 1, 0, 0, 0, 108, 931, 1, 0, 0, 0, 110, 941, 1, 0, 0, 0, 112, 962, 1, 0, 0, 0, 114, 995, 1, 0, 0, 0, 116, 1008, 1, 0, 0, 0, 118, 1010, 1, 0, 0, 0, 120, 1028, 1, 0, 0, 0, 122, 1037, 1, 0, 0, 0, 124, 1039, 1, 0, 0, 0, 126, 1056, 1, 0, 0, 0, 128, 1069, 1, 0, 0, 0, 130, 1079, 1, 0, 0, 0, 132, 1083, 1, 0, 0, 0, 134, 1091, 1, 0, 0, 0, 136, 1101, 1, 0, 0, 0, 138, 1104, 1, 0, 0, 0, 140, 1117, 1, 0, 0, 0, 142, 1119, 1, 0, 0, 0, 144, 1121, 1, 0, 0, 0, 146, 1123, 1, 0, 0, 0, 148, 1127, 1, 0, 0, 0, 150, 1132, 1, 0, 0, 0, 152, 1134, 1, 0, 0, 0, 154, 1138, 1, 0, 0, 0, 156, 1144, 1, 0, 0, 0, 158, 1146, 1, 0, 0, 0, 160, 1160, 1, 0, 0, 0, 162, 1162, 1, 0, 0, 0, 164, 1176, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 10, 5, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 7, 1, 0, 0, 0, 187, 192, 3, 150, 75, 0, 188, 189, 5, 112, 0, 0, 189, 191, 3, 150, 75, 0, 190, 188, 1, 0, 0, 0, 191, 194, 1, 0, 0, 0, 192, 190, 1, 0, 0, 0, 192, 193, 1, 0, 0, 0, 193, 9, 1, 0, 0, 0, 194, 192, 1, 0, 0, 0, 195, 204, 3, 12, 6, 0, 196, 204, 3, 14, 7, 0, 197, 204, 3, 16, 8, 0, 198, 204, 3, 18, 9, 0, 199, 204, 3, 20, 10, 0, 200, 204, 3, 22, 11, 0, 201, 204, 3, 24, 12, 0, 202, 204, 3, 26, 13, 0, 203, 195, 1, 0, 0, 0, 203, 196, 1, 0, 0, 0, 203, 197, 1, 0, 0, 0, 203, 198, 1, 0, 0, 0, 203, 199, 1, 0, 0, 0, 203, 200, 1, 0, 0, 0, 203, 201, 1, 0, 0, 0, 203, 202, 1, 0, 0, 0, 204, 11, 1, 0, 0, 0, 205, 207, 5, 70, 0, 0, 206, 208, 3, 4, 2, 0, 207, 206, 1, 0, 0, 0, 207, 208, 1, 0, 0, 0, 208, 210, 1, 0, 0, 0, 209, 211, 5, 145, 0, 0, 210, 209, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 13, 1, 0, 0, 0, 212, 213, 5, 38, 0, 0, 213, 214, 5, 126, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 144, 0, 0, 216, 219, 3, 10, 5, 0, 217, 218, 5, 24, 0, 0, 218, 220, 3, 10, 5, 0, 219, 217, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0, 220, 15, 1, 0, 0, 0, 221, 222, 5, 96, 0, 0, 222, 223, 5, 126, 0, 0, 223, 224, 3, 4, 2, 0, 224, 225, 5, 144, 0, 0, 225, 227, 3, 10, 5, 0, 226, 228, 5, 145, 0, 0, 227, 226, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 17, 1, 0, 0, 0, 229, 230, 5, 29, 0, 0, 230, 231, 3, 150, 75, 0, 231, 233, 5, 126, 0, 0, 232, 234, 3, 8, 4, 0, 233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 1, 0, 0, 0, 235, 236, 5, 144, 0, 0, 236, 237, 3, 26, 13, 0, 237, 19, 1, 0, 0, 0, 238, 239, 3, 4, 2, 0, 239, 240, 5, 111, 0, 0, 240, 241, 5, 118, 0, 0, 241, 242, 3, 4, 2, 0, 242, 21, 1, 0, 0, 0, 243, 245, 3, 4, 2, 0, 244, 246, 5, 145, 0, 0, 245, 244, 1, 0, 0, 0, 245, 246, 1, 0, 0, 0, 246, 23, 1, 0, 0, 0, 247, 248, 5, 145, 0, 0, 248, 25, 1, 0, 0, 0, 249, 253, 5, 124, 0, 0, 250, 252, 3, 2, 1, 0, 251, 250, 1, 0, 0, 0, 252, 255, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 253, 254, 1, 0, 0, 0, 254, 256, 1, 0, 0, 0, 255, 253, 1, 0, 0, 0, 256, 257, 5, 142, 0, 0, 257, 27, 1, 0, 0, 0, 258, 259, 3, 4, 2, 0, 259, 260, 5, 111, 0, 0, 260, 261, 3, 4, 2, 0, 261, 29, 1, 0, 0, 0, 262, 267, 3, 28, 14, 0, 263, 264, 5, 112, 0, 0, 264, 266, 3, 28, 14, 0, 265, 263, 1, 0, 0, 0, 266, 269, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 267, 268, 1, 0, 0, 0, 268, 31, 1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 270, 274, 3, 34, 17, 0, 271, 274, 3, 38, 19, 0, 272, 274, 3, 114, 57, 0, 273, 270, 1, 0, 0, 0, 273, 271, 1, 0, 0, 0, 273, 272, 1, 0, 0, 0, 274, 275, 1, 0, 0, 0, 275, 276, 5, 0, 0, 1, 276, 33, 1, 0, 0, 0, 277, 283, 3, 36, 18, 0, 278, 279, 5, 91, 0, 0, 279, 280, 5, 1, 0, 0, 280, 282, 3, 36, 18, 0, 281, 278, 1, 0, 0, 0, 282, 285, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 283, 284, 1, 0, 0, 0, 284, 35, 1, 0, 0, 0, 285, 283, 1, 0, 0, 0, 286, 293, 3, 38, 19, 0, 287, 288, 5, 126, 0, 0, 288, 289, 3, 34, 17, 0, 289, 290, 5, 144, 0, 0, 290, 293, 1, 0, 0, 0, 291, 293, 3, 154, 77, 0, 292, 286, 1, 0, 0, 0, 292, 287, 1, 0, 0, 0, 292, 291, 1, 0, 0, 0, 293, 37, 1, 0, 0, 0, 294, 296, 3, 40, 20, 0, 295, 294, 1, 0, 0, 0, 295, 296, 1, 0, 0, 0, 296, 297, 1, 0, 0, 0, 297, 299, 5, 77, 0, 0, 298, 300, 5, 23, 0, 0, 299, 298, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 302, 1, 0, 0, 0, 301, 303, 3, 42, 21, 0, 302, 301, 1, 0, 0, 0, 302, 303, 1, 0, 0, 0, 303, 304, 1, 0, 0, 0, 304, 306, 3, 104, 52, 0, 305, 307, 3, 44, 22, 0, 306, 305, 1, 0, 0, 0, 306, 307, 1, 0, 0, 0, 307, 309, 1, 0, 0, 0, 308, 310, 3, 46, 23, 0, 309, 308, 1, 0, 0, 0, 309, 310, 1, 0, 0, 0, 310, 312, 1, 0, 0, 0, 311, 313, 3, 50, 25, 0, 312, 311, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0, 313, 315, 1, 0, 0, 0, 314, 316, 3, 52, 26, 0, 315, 314, 1, 0, 0, 0, 315, 316, 1, 0, 0, 0, 316, 318, 1, 0, 0, 0, 317, 319, 3, 54, 27, 0, 318, 317, 1, 0, 0, 0, 318, 319, 1, 0, 0, 0, 319, 322, 1, 0, 0, 0, 320, 321, 5, 98, 0, 0, 321, 323, 7, 0, 0, 0, 322, 320, 1, 0, 0, 0, 322, 323, 1, 0, 0, 0, 323, 326, 1, 0, 0, 0, 324, 325, 5, 98, 0, 0, 325, 327, 5, 86, 0, 0, 326, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 329, 1, 0, 0, 0, 328, 330, 3, 56, 28, 0, 329, 328, 1, 0, 0, 0, 329, 330, 1, 0, 0, 0, 330, 332, 1, 0, 0, 0, 331, 333, 3, 48, 24, 0, 332, 331, 1, 0, 0, 0, 332, 333, 1, 0, 0, 0, 333, 335, 1, 0, 0, 0, 334, 336, 3, 58, 29, 0, 335, 334, 1, 0, 0, 0, 335, 336, 1, 0, 0, 0, 336, 339, 1, 0, 0, 0, 337, 340, 3, 62, 31, 0, 338, 340, 3, 64, 32, 0, 339, 337, 1, 0, 0, 0, 339, 338, 1, 0, 0, 0, 339, 340, 1, 0, 0, 0, 340, 342, 1, 0, 0, 0, 341, 343, 3, 66, 33, 0, 342, 341, 1, 0, 0, 0, 342, 343, 1, 0, 0, 0, 343, 39, 1, 0, 0, 0, 344, 345, 5, 98, 0, 0, 345, 346, 3, 118, 59, 0, 346, 41, 1, 0, 0, 0, 347, 348, 5, 85, 0, 0, 348, 351, 5, 104, 0, 0, 349, 350, 5, 98, 0, 0, 350, 352, 5, 82, 0, 0, 351, 349, 1, 0, 0, 0, 351, 352, 1, 0, 0, 0, 352, 43, 1, 0, 0, 0, 353, 354, 5, 32, 0, 0, 354, 355, 3, 68, 34, 0, 355, 45, 1, 0, 0, 0, 356, 358, 7, 1, 0, 0, 357, 356, 1, 0, 0, 0, 357, 358, 1, 0, 0, 0, 358, 359, 1, 0, 0, 0, 359, 360, 5, 5, 0, 0, 360, 361, 5, 45, 0, 0, 361, 362, 3, 104, 52, 0, 362, 47, 1, 0, 0, 0, 363, 364, 5, 97, 0, 0, 364, 365, 3, 150, 75, 0, 365, 366, 5, 6, 0, 0, 366, 367, 5, 126, 0, 0, 367, 368, 3, 88, 44, 0, 368, 378, 5, 144, 0, 0, 369, 370, 5, 112, 0, 0, 370, 371, 3, 150, 75, 0, 371, 372, 5, 6, 0, 0, 372, 373, 5, 126, 0, 0, 373, 374, 3, 88, 44, 0, 374, 375, 5, 144, 0, 0, 375, 377, 1, 0, 0, 0, 376, 369, 1, 0, 0, 0, 377, 380, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 378, 379, 1, 0, 0, 0, 379, 49, 1, 0, 0, 0, 380, 378, 1, 0, 0, 0, 381, 382, 5, 67, 0, 0, 382, 383, 3, 106, 53, 0, 383, 51, 1, 0, 0, 0, 384, 385, 5, 95, 0, 0, 385, 386, 3, 106, 53, 0, 386, 53, 1, 0, 0, 0, 387, 388, 5, 34, 0, 0, 388, 395, 5, 11, 0, 0, 389, 390, 7, 0, 0, 0, 390, 391, 5, 126, 0, 0, 391, 392, 3, 104, 52, 0, 392, 393, 5, 144, 0, 0, 393, 396, 1, 0, 0, 0, 394, 396, 3, 104, 52, 0, 395, 389, 1, 0, 0, 0, 395, 394, 1, 0, 0, 0, 396, 55, 1, 0, 0, 0, 397, 398, 5, 35, 0, 0, 398, 399, 3, 106, 53, 0, 399, 57, 1, 0, 0, 0, 400, 401, 5, 62, 0, 0, 401, 402, 5, 11, 0, 0, 402, 403, 3, 78, 39, 0, 403, 59, 1, 0, 0, 0, 404, 405, 5, 62, 0, 0, 405, 406, 5, 11, 0, 0, 406, 407, 3, 104, 52, 0, 407, 61, 1, 0, 0, 0, 408, 409, 5, 52, 0, 0, 409, 412, 3, 106, 53, 0, 410, 411, 5, 112, 0, 0, 411, 413, 3, 106, 53, 0, 412, 410, 1, 0, 0, 0, 412, 413, 1, 0, 0, 0, 413, 418, 1, 0, 0, 0, 414, 415, 5, 98, 0, 0, 415, 419, 5, 82, 0, 0, 416, 417, 5, 11, 0, 0, 417, 419, 3, 104, 52, 0, 418, 414, 1, 0, 0, 0, 418, 416, 1, 0, 0, 0, 418, 419, 1, 0, 0, 0, 419, 438, 1, 0, 0, 0, 420, 421, 5, 52, 0, 0, 421, 424, 3, 106, 53, 0, 422, 423, 5, 98, 0, 0, 423, 425, 5, 82, 0, 0, 424, 422, 1, 0, 0, 0, 424, 425, 1, 0, 0, 0, 425, 426, 1, 0, 0, 0, 426, 427, 5, 59, 0, 0, 427, 428, 3, 106, 53, 0, 428, 438, 1, 0, 0, 0, 429, 430, 5, 52, 0, 0, 430, 431, 3, 106, 53, 0, 431, 432, 5, 59, 0, 0, 432, 435, 3, 106, 53, 0, 433, 434, 5, 11, 0, 0, 434, 436, 3, 104, 52, 0, 435, 433, 1, 0, 0, 0, 435, 436, 1, 0, 0, 0, 436, 438, 1, 0, 0, 0, 437, 408, 1, 0, 0, 0, 437, 420, 1, 0, 0, 0, 437, 429, 1, 0, 0, 0, 438, 63, 1, 0, 0, 0, 439, 440, 5, 59, 0, 0, 440, 441, 3, 106, 53, 0, 441, 65, 1, 0, 0, 0, 442, 443, 5, 79, 0, 0, 443, 444, 3, 84, 42, 0, 444, 67, 1, 0, 0, 0, 445, 446, 6, 34, -1, 0, 446, 448, 3, 126, 63, 0, 447, 449, 5, 27, 0, 0, 448, 447, 1, 0, 0, 0, 448, 449, 1, 0, 0, 0, 449, 451, 1, 0, 0, 0, 450, 452, 3, 76, 38, 0, 451, 450, 1, 0, 0, 0, 451, 452, 1, 0, 0, 0, 452, 458, 1, 0, 0, 0, 453, 454, 5, 126, 0, 0, 454, 455, 3, 68, 34, 0, 455, 456, 5, 144, 0, 0, 456, 458, 1, 0, 0, 0, 457, 445, 1, 0, 0, 0, 457, 453, 1, 0, 0, 0, 458, 473, 1, 0, 0, 0, 459, 460, 10, 3, 0, 0, 460, 461, 3, 72, 36, 0, 461, 462, 3, 68, 34, 4, 462, 472, 1, 0, 0, 0, 463, 465, 10, 4, 0, 0, 464, 466, 3, 70, 35, 0, 465, 464, 1, 0, 0, 0, 465, 466, 1, 0, 0, 0, 466, 467, 1, 0, 0, 0, 467, 468, 5, 45, 0, 0, 468, 469, 3, 68, 34, 0, 469, 470, 3, 74, 37, 0, 470, 472, 1, 0, 0, 0, 471, 459, 1, 0, 0, 0, 471, 463, 1, 0, 0, 0, 472, 475, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 473, 474, 1, 0, 0, 0, 474, 69, 1, 0, 0, 0, 475, 473, 1, 0, 0, 0, 476, 478, 7, 2, 0, 0, 477, 476, 1, 0, 0, 0, 477, 478, 1, 0, 0, 0, 478, 479, 1, 0, 0, 0, 479, 486, 5, 42, 0, 0, 480, 482, 5, 42, 0, 0, 481, 483, 7, 2, 0, 0, 482, 481, 1, 0, 0, 0, 482, 483, 1, 0, 0, 0, 483, 486, 1, 0, 0, 0, 484, 486, 7, 2, 0, 0, 485, 477, 1, 0, 0, 0, 485, 480, 1, 0, 0, 0, 485, 484, 1, 0, 0, 0, 486, 520, 1, 0, 0, 0, 487, 489, 7, 3, 0, 0, 488, 487, 1, 0, 0, 0, 488, 489, 1, 0, 0, 0, 489, 490, 1, 0, 0, 0, 490, 492, 7, 4, 0, 0, 491, 493, 5, 63, 0, 0, 492, 491, 1, 0, 0, 0, 492, 493, 1, 0, 0, 0, 493, 502, 1, 0, 0, 0, 494, 496, 7, 4, 0, 0, 495, 497, 5, 63, 0, 0, 496, 495, 1, 0, 0, 0, 496, 497, 1, 0, 0, 0, 497, 499, 1, 0, 0, 0, 498, 500, 7, 3, 0, 0, 499, 498, 1, 0, 0, 0, 499, 500, 1, 0, 0, 0, 500, 502, 1, 0, 0, 0, 501, 488, 1, 0, 0, 0, 501, 494, 1, 0, 0, 0, 502, 520, 1, 0, 0, 0, 503, 505, 7, 5, 0, 0, 504, 503, 1, 0, 0, 0, 504, 505, 1, 0, 0, 0, 505, 506, 1, 0, 0, 0, 506, 508, 5, 33, 0, 0, 507, 509, 5, 63, 0, 0, 508, 507, 1, 0, 0, 0, 508, 509, 1, 0, 0, 0, 509, 518, 1, 0, 0, 0, 510, 512, 5, 33, 0, 0, 511, 513, 5, 63, 0, 0, 512, 511, 1, 0, 0, 0, 512, 513, 1, 0, 0, 0, 513, 515, 1, 0, 0, 0, 514, 516, 7, 5, 0, 0, 515, 514, 1, 0, 0, 0, 515, 516, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 504, 1, 0, 0, 0, 517, 510, 1, 0, 0, 0, 518, 520, 1, 0, 0, 0, 519, 485, 1, 0, 0, 0, 519, 501, 1, 0, 0, 0, 519, 517, 1, 0, 0, 0, 520, 71, 1, 0, 0, 0, 521, 522, 5, 16, 0, 0, 522, 525, 5, 45, 0, 0, 523, 525, 5, 112, 0, 0, 524, 521, 1, 0, 0, 0, 524, 523, 1, 0, 0, 0, 525, 73, 1, 0, 0, 0, 526, 527, 5, 60, 0, 0, 527, 536, 3, 104, 52, 0, 528, 529, 5, 92, 0, 0, 529, 530, 5, 126, 0, 0, 530, 531, 3, 104, 52, 0, 531, 532, 5, 144, 0, 0, 532, 536, 1, 0, 0, 0, 533, 534, 5, 92, 0, 0, 534, 536, 3, 104, 52, 0, 535, 526, 1, 0, 0, 0, 535, 528, 1, 0, 0, 0, 535, 533, 1, 0, 0, 0, 536, 75, 1, 0, 0, 0, 537, 538, 5, 75, 0, 0, 538, 541, 3, 82, 41, 0, 539, 540, 5, 59, 0, 0, 540, 542, 3, 82, 41, 0, 541, 539, 1, 0, 0, 0, 541, 542, 1, 0, 0, 0, 542, 77, 1, 0, 0, 0, 543, 548, 3, 80, 40, 0, 544, 545, 5, 112, 0, 0, 545, 547, 3, 80, 40, 0, 546, 544, 1, 0, 0, 0, 547, 550, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 548, 549, 1, 0, 0, 0, 549, 79, 1, 0, 0, 0, 550, 548, 1, 0, 0, 0, 551, 553, 3, 106, 53, 0, 552, 554, 7, 6, 0, 0, 553, 552, 1, 0, 0, 0, 553, 554, 1, 0, 0, 0, 554, 557, 1, 0, 0, 0, 555, 556, 5, 58, 0, 0, 556, 558, 7, 7, 0, 0, 557, 555, 1, 0, 0, 0, 557, 558, 1, 0, 0, 0, 558, 561, 1, 0, 0, 0, 559, 560, 5, 15, 0, 0, 560, 562, 5, 106, 0, 0, 561, 559, 1, 0, 0, 0, 561, 562, 1, 0, 0, 0, 562, 81, 1, 0, 0, 0, 563, 570, 3, 154, 77, 0, 564, 567, 3, 138, 69, 0, 565, 566, 5, 146, 0, 0, 566, 568, 3, 138, 69, 0, 567, 565, 1, 0, 0, 0, 567, 568, 1, 0, 0, 0, 568, 570, 1, 0, 0, 0, 569, 563, 1, 0, 0, 0, 569, 564, 1, 0, 0, 0, 570, 83, 1, 0, 0, 0, 571, 576, 3, 86, 43, 0, 572, 573, 5, 112, 0, 0, 573, 575, 3, 86, 43, 0, 574, 572, 1, 0, 0, 0, 575, 578, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 576, 577, 1, 0, 0, 0, 577, 85, 1, 0, 0, 0, 578, 576, 1, 0, 0, 0, 579, 580, 3, 150, 75, 0, 580, 581, 5, 118, 0, 0, 581, 582, 3, 140, 70, 0, 582, 87, 1, 0, 0, 0, 583, 585, 3, 90, 45, 0, 584, 583, 1, 0, 0, 0, 584, 585, 1, 0, 0, 0, 585, 587, 1, 0, 0, 0, 586, 588, 3, 92, 46, 0, 587, 586, 1, 0, 0, 0, 587, 588, 1, 0, 0, 0, 588, 590, 1, 0, 0, 0, 589, 591, 3, 94, 47, 0, 590, 589, 1, 0, 0, 0, 590, 591, 1, 0, 0, 0, 591, 89, 1, 0, 0, 0, 592, 593, 5, 65, 0, 0, 593, 594, 5, 11, 0, 0, 594, 595, 3, 104, 52, 0, 595, 91, 1, 0, 0, 0, 596, 597, 5, 62, 0, 0, 597, 598, 5, 11, 0, 0, 598, 599, 3, 78, 39, 0, 599, 93, 1, 0, 0, 0, 600, 601, 7, 8, 0, 0, 601, 602, 3, 96, 48, 0, 602, 95, 1, 0, 0, 0, 603, 610, 3, 98, 49, 0, 604, 605, 5, 9, 0, 0, 605, 606, 3, 98, 49, 0, 606, 607, 5, 2, 0, 0, 607, 608, 3, 98, 49, 0, 608, 610, 1, 0, 0, 0, 609, 603, 1, 0, 0, 0, 609, 604, 1, 0, 0, 0, 610, 97, 1, 0, 0, 0, 611, 612, 5, 18, 0, 0, 612, 624, 5, 73, 0, 0, 613, 614, 5, 90, 0, 0, 614, 624, 5, 66, 0, 0, 615, 616, 5, 90, 0, 0, 616, 624, 5, 30, 0, 0, 617, 618, 3, 138, 69, 0, 618, 619, 5, 66, 0, 0, 619, 624, 1, 0, 0, 0, 620, 621, 3, 138, 69, 0, 621, 622, 5, 30, 0, 0, 622, 624, 1, 0, 0, 0, 623, 611, 1, 0, 0, 0, 623, 613, 1, 0, 0, 0, 623, 615, 1, 0, 0, 0, 623, 617, 1, 0, 0, 0, 623, 620, 1, 0, 0, 0, 624, 99, 1, 0, 0, 0, 625, 626, 3, 106, 53, 0, 626, 627, 5, 0, 0, 1, 627, 101, 1, 0, 0, 0, 628, 676, 3, 150, 75, 0, 629, 630, 3, 150, 75, 0, 630, 631, 5, 126, 0, 0, 631, 632, 3, 150, 75, 0, 632, 639, 3, 102, 51, 0, 633, 634, 5, 112, 0, 0, 634, 635, 3, 150, 75, 0, 635, 636, 3, 102, 51, 0, 636, 638, 1, 0, 0, 0, 637, 633, 1, 0, 0, 0, 638, 641, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 639, 640, 1, 0, 0, 0, 640, 642, 1, 0, 0, 0, 641, 639, 1, 0, 0, 0, 642, 643, 5, 144, 0, 0, 643, 676, 1, 0, 0, 0, 644, 645, 3, 150, 75, 0, 645, 646, 5, 126, 0, 0, 646, 651, 3, 152, 76, 0, 647, 648, 5, 112, 0, 0, 648, 650, 3, 152, 76, 0, 649, 647, 1, 0, 0, 0, 650, 653, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 651, 652, 1, 0, 0, 0, 652, 654, 1, 0, 0, 0, 653, 651, 1, 0, 0, 0, 654, 655, 5, 144, 0, 0, 655, 676, 1, 0, 0, 0, 656, 657, 3, 150, 75, 0, 657, 658, 5, 126, 0, 0, 658, 663, 3, 102, 51, 0, 659, 660, 5, 112, 0, 0, 660, 662, 3, 102, 51, 0, 661, 659, 1, 0, 0, 0, 662, 665, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 663, 664, 1, 0, 0, 0, 664, 666, 1, 0, 0, 0, 665, 663, 1, 0, 0, 0, 666, 667, 5, 144, 0, 0, 667, 676, 1, 0, 0, 0, 668, 669, 3, 150, 75, 0, 669, 671, 5, 126, 0, 0, 670, 672, 3, 104, 52, 0, 671, 670, 1, 0, 0, 0, 671, 672, 1, 0, 0, 0, 672, 673, 1, 0, 0, 0, 673, 674, 5, 144, 0, 0, 674, 676, 1, 0, 0, 0, 675, 628, 1, 0, 0, 0, 675, 629, 1, 0, 0, 0, 675, 644, 1, 0, 0, 0, 675, 656, 1, 0, 0, 0, 675, 668, 1, 0, 0, 0, 676, 103, 1, 0, 0, 0, 677, 682, 3, 106, 53, 0, 678, 679, 5, 112, 0, 0, 679, 681, 3, 106, 53, 0, 680, 678, 1, 0, 0, 0, 681, 684, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 682, 683, 1, 0, 0, 0, 683, 105, 1, 0, 0, 0, 684, 682, 1, 0, 0, 0, 685, 686, 6, 53, -1, 0, 686, 688, 5, 12, 0, 0, 687, 689, 3, 106, 53, 0, 688, 687, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 695, 1, 0, 0, 0, 690, 691, 5, 94, 0, 0, 691, 692, 3, 106, 53, 0, 692, 693, 5, 81, 0, 0, 693, 694, 3, 106, 53, 0, 694, 696, 1, 0, 0, 0, 695, 690, 1, 0, 0, 0, 696, 697, 1, 0, 0, 0, 697, 695, 1, 0, 0, 0, 697, 698, 1, 0, 0, 0, 698, 701, 1, 0, 0, 0, 699, 700, 5, 24, 0, 0, 700, 702, 3, 106, 53, 0, 701, 699, 1, 0, 0, 0, 701, 702, 1, 0, 0, 0, 702, 703, 1, 0, 0, 0, 703, 704, 5, 25, 0, 0, 704, 835, 1, 0, 0, 0, 705, 706, 5, 13, 0, 0, 706, 707, 5, 126, 0, 0, 707, 708, 3, 106, 53, 0, 708, 709, 5, 6, 0, 0, 709, 710, 3, 102, 51, 0, 710, 711, 5, 144, 0, 0, 711, 835, 1, 0, 0, 0, 712, 713, 5, 19, 0, 0, 713, 835, 5, 106, 0, 0, 714, 715, 5, 43, 0, 0, 715, 716, 3, 106, 53, 0, 716, 717, 3, 142, 71, 0, 717, 835, 1, 0, 0, 0, 718, 719, 5, 80, 0, 0, 719, 720, 5, 126, 0, 0, 720, 721, 3, 106, 53, 0, 721, 722, 5, 32, 0, 0, 722, 725, 3, 106, 53, 0, 723, 724, 5, 31, 0, 0, 724, 726, 3, 106, 53, 0, 725, 723, 1, 0, 0, 0, 725, 726, 1, 0, 0, 0, 726, 727, 1, 0, 0, 0, 727, 728, 5, 144, 0, 0, 728, 835, 1, 0, 0, 0, 729, 730, 5, 83, 0, 0, 730, 835, 5, 106, 0, 0, 731, 732, 5, 88, 0, 0, 732, 733, 5, 126, 0, 0, 733, 734, 7, 9, 0, 0, 734, 735, 3, 156, 78, 0, 735, 736, 5, 32, 0, 0, 736, 737, 3, 106, 53, 0, 737, 738, 5, 144, 0, 0, 738, 835, 1, 0, 0, 0, 739, 740, 3, 150, 75, 0, 740, 742, 5, 126, 0, 0, 741, 743, 3, 104, 52, 0, 742, 741, 1, 0, 0, 0, 742, 743, 1, 0, 0, 0, 743, 744, 1, 0, 0, 0, 744, 745, 5, 144, 0, 0, 745, 754, 1, 0, 0, 0, 746, 748, 5, 126, 0, 0, 747, 749, 5, 23, 0, 0, 748, 747, 1, 0, 0, 0, 748, 749, 1, 0, 0, 0, 749, 751, 1, 0, 0, 0, 750, 752, 3, 108, 54, 0, 751, 750, 1, 0, 0, 0, 751, 752, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 755, 5, 144, 0, 0, 754, 746, 1, 0, 0, 0, 754, 755, 1, 0, 0, 0, 755, 756, 1, 0, 0, 0, 756, 757, 5, 64, 0, 0, 757, 758, 5, 126, 0, 0, 758, 759, 3, 88, 44, 0, 759, 760, 5, 144, 0, 0, 760, 835, 1, 0, 0, 0, 761, 762, 3, 150, 75, 0, 762, 764, 5, 126, 0, 0, 763, 765, 3, 104, 52, 0, 764, 763, 1, 0, 0, 0, 764, 765, 1, 0, 0, 0, 765, 766, 1, 0, 0, 0, 766, 767, 5, 144, 0, 0, 767, 776, 1, 0, 0, 0, 768, 770, 5, 126, 0, 0, 769, 771, 5, 23, 0, 0, 770, 769, 1, 0, 0, 0, 770, 771, 1, 0, 0, 0, 771, 773, 1, 0, 0, 0, 772, 774, 3, 108, 54, 0, 773, 772, 1, 0, 0, 0, 773, 774, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 777, 5, 144, 0, 0, 776, 768, 1, 0, 0, 0, 776, 777, 1, 0, 0, 0, 777, 778, 1, 0, 0, 0, 778, 779, 5, 64, 0, 0, 779, 780, 3, 150, 75, 0, 780, 835, 1, 0, 0, 0, 781, 787, 3, 150, 75, 0, 782, 784, 5, 126, 0, 0, 783, 785, 3, 104, 52, 0, 784, 783, 1, 0, 0, 0, 784, 785, 1, 0, 0, 0, 785, 786, 1, 0, 0, 0, 786, 788, 5, 144, 0, 0, 787, 782, 1, 0, 0, 0, 787, 788, 1, 0, 0, 0, 788, 789, 1, 0, 0, 0, 789, 791, 5, 126, 0, 0, 790, 792, 5, 23, 0, 0, 791, 790, 1, 0, 0, 0, 791, 792, 1, 0, 0, 0, 792, 794, 1, 0, 0, 0, 793, 795, 3, 108, 54, 0, 794, 793, 1, 0, 0, 0, 794, 795, 1, 0, 0, 0, 795, 796, 1, 0, 0, 0, 796, 797, 5, 144, 0, 0, 797, 835, 1, 0, 0, 0, 798, 835, 3, 114, 57, 0, 799, 835, 3, 158, 79, 0, 800, 835, 3, 140, 70, 0, 801, 802, 5, 114, 0, 0, 802, 835, 3, 106, 53, 19, 803, 804, 5, 56, 0, 0, 804, 835, 3, 106, 53, 13, 805, 806, 3, 130, 65, 0, 806, 807, 5, 116, 0, 0, 807, 809, 1, 0, 0, 0, 808, 805, 1, 0, 0, 0, 808, 809, 1, 0, 0, 0, 809, 810, 1, 0, 0, 0, 810, 835, 5, 108, 0, 0, 811, 812, 5, 126, 0, 0, 812, 813, 3, 34, 17, 0, 813, 814, 5, 144, 0, 0, 814, 835, 1, 0, 0, 0, 815, 816, 5, 126, 0, 0, 816, 817, 3, 106, 53, 0, 817, 818, 5, 144, 0, 0, 818, 835, 1, 0, 0, 0, 819, 820, 5, 126, 0, 0, 820, 821, 3, 104, 52, 0, 821, 822, 5, 144, 0, 0, 822, 835, 1, 0, 0, 0, 823, 825, 5, 125, 0, 0, 824, 826, 3, 104, 52, 0, 825, 824, 1, 0, 0, 0, 825, 826, 1, 0, 0, 0, 826, 827, 1, 0, 0, 0, 827, 835, 5, 143, 0, 0, 828, 830, 5, 124, 0, 0, 829, 831, 3, 30, 15, 0, 830, 829, 1, 0, 0, 0, 830, 831, 1, 0, 0, 0, 831, 832, 1, 0, 0, 0, 832, 835, 5, 142, 0, 0, 833, 835, 3, 122, 61, 0, 834, 685, 1, 0, 0, 0, 834, 705, 1, 0, 0, 0, 834, 712, 1, 0, 0, 0, 834, 714, 1, 0, 0, 0, 834, 718, 1, 0, 0, 0, 834, 729, 1, 0, 0, 0, 834, 731, 1, 0, 0, 0, 834, 739, 1, 0, 0, 0, 834, 761, 1, 0, 0, 0, 834, 781, 1, 0, 0, 0, 834, 798, 1, 0, 0, 0, 834, 799, 1, 0, 0, 0, 834, 800, 1, 0, 0, 0, 834, 801, 1, 0, 0, 0, 834, 803, 1, 0, 0, 0, 834, 808, 1, 0, 0, 0, 834, 811, 1, 0, 0, 0, 834, 815, 1, 0, 0, 0, 834, 819, 1, 0, 0, 0, 834, 823, 1, 0, 0, 0, 834, 828, 1, 0, 0, 0, 834, 833, 1, 0, 0, 0, 835, 928, 1, 0, 0, 0, 836, 840, 10, 18, 0, 0, 837, 841, 5, 108, 0, 0, 838, 841, 5, 146, 0, 0, 839, 841, 5, 133, 0, 0, 840, 837, 1, 0, 0, 0, 840, 838, 1, 0, 0, 0, 840, 839, 1, 0, 0, 0, 841, 842, 1, 0, 0, 0, 842, 927, 3, 106, 53, 19, 843, 847, 10, 17, 0, 0, 844, 848, 5, 134, 0, 0, 845, 848, 5, 114, 0, 0, 846, 848, 5, 113, 0, 0, 847, 844, 1, 0, 0, 0, 847, 845, 1, 0, 0, 0, 847, 846, 1, 0, 0, 0, 848, 849, 1, 0, 0, 0, 849, 927, 3, 106, 53, 18, 850, 875, 10, 16, 0, 0, 851, 876, 5, 117, 0, 0, 852, 876, 5, 118, 0, 0, 853, 876, 5, 129, 0, 0, 854, 876, 5, 127, 0, 0, 855, 876, 5, 128, 0, 0, 856, 876, 5, 119, 0, 0, 857, 876, 5, 120, 0, 0, 858, 860, 5, 56, 0, 0, 859, 858, 1, 0, 0, 0, 859, 860, 1, 0, 0, 0, 860, 861, 1, 0, 0, 0, 861, 863, 5, 40, 0, 0, 862, 864, 5, 14, 0, 0, 863, 862, 1, 0, 0, 0, 863, 864, 1, 0, 0, 0, 864, 876, 1, 0, 0, 0, 865, 867, 5, 56, 0, 0, 866, 865, 1, 0, 0, 0, 866, 867, 1, 0, 0, 0, 867, 868, 1, 0, 0, 0, 868, 876, 7, 10, 0, 0, 869, 876, 5, 140, 0, 0, 870, 876, 5, 141, 0, 0, 871, 876, 5, 131, 0, 0, 872, 876, 5, 122, 0, 0, 873, 876, 5, 123, 0, 0, 874, 876, 5, 130, 0, 0, 875, 851, 1, 0, 0, 0, 875, 852, 1, 0, 0, 0, 875, 853, 1, 0, 0, 0, 875, 854, 1, 0, 0, 0, 875, 855, 1, 0, 0, 0, 875, 856, 1, 0, 0, 0, 875, 857, 1, 0, 0, 0, 875, 859, 1, 0, 0, 0, 875, 866, 1, 0, 0, 0, 875, 869, 1, 0, 0, 0, 875, 870, 1, 0, 0, 0, 875, 871, 1, 0, 0, 0, 875, 872, 1, 0, 0, 0, 875, 873, 1, 0, 0, 0, 875, 874, 1, 0, 0, 0, 876, 877, 1, 0, 0, 0, 877, 927, 3, 106, 53, 17, 878, 879, 10, 14, 0, 0, 879, 880, 5, 132, 0, 0, 880, 927, 3, 106, 53, 15, 881, 882, 10, 12, 0, 0, 882, 883, 5, 2, 0, 0, 883, 927, 3, 106, 53, 13, 884, 885, 10, 11, 0, 0, 885, 886, 5, 61, 0, 0, 886, 927, 3, 106, 53, 12, 887, 889, 10, 10, 0, 0, 888, 890, 5, 56, 0, 0, 889, 888, 1, 0, 0, 0, 889, 890, 1, 0, 0, 0, 890, 891, 1, 0, 0, 0, 891, 892, 5, 9, 0, 0, 892, 893, 3, 106, 53, 0, 893, 894, 5, 2, 0, 0, 894, 895, 3, 106, 53, 11, 895, 927, 1, 0, 0, 0, 896, 897, 10, 9, 0, 0, 897, 898, 5, 135, 0, 0, 898, 899, 3, 106, 53, 0, 899, 900, 5, 111, 0, 0, 900, 901, 3, 106, 53, 9, 901, 927, 1, 0, 0, 0, 902, 903, 10, 22, 0, 0, 903, 904, 5, 125, 0, 0, 904, 905, 3, 106, 53, 0, 905, 906, 5, 143, 0, 0, 906, 927, 1, 0, 0, 0, 907, 908, 10, 21, 0, 0, 908, 909, 5, 116, 0, 0, 909, 927, 5, 104, 0, 0, 910, 911, 10, 20, 0, 0, 911, 912, 5, 116, 0, 0, 912, 927, 3, 150, 75, 0, 913, 914, 10, 15, 0, 0, 914, 916, 5, 44, 0, 0, 915, 917, 5, 56, 0, 0, 916, 915, 1, 0, 0, 0, 916, 917, 1, 0, 0, 0, 917, 918, 1, 0, 0, 0, 918, 927, 5, 57, 0, 0, 919, 924, 10, 8, 0, 0, 920, 921, 5, 6, 0, 0, 921, 925, 3, 150, 75, 0, 922, 923, 5, 6, 0, 0, 923, 925, 5, 106, 0, 0, 924, 920, 1, 0, 0, 0, 924, 922, 1, 0, 0, 0, 925, 927, 1, 0, 0, 0, 926, 836, 1, 0, 0, 0, 926, 843, 1, 0, 0, 0, 926, 850, 1, 0, 0, 0, 926, 878, 1, 0, 0, 0, 926, 881, 1, 0, 0, 0, 926, 884, 1, 0, 0, 0, 926, 887, 1, 0, 0, 0, 926, 896, 1, 0, 0, 0, 926, 902, 1, 0, 0, 0, 926, 907, 1, 0, 0, 0, 926, 910, 1, 0, 0, 0, 926, 913, 1, 0, 0, 0, 926, 919, 1, 0, 0, 0, 927, 930, 1, 0, 0, 0, 928, 926, 1, 0, 0, 0, 928, 929, 1, 0, 0, 0, 929, 107, 1, 0, 0, 0, 930, 928, 1, 0, 0, 0, 931, 936, 3, 110, 55, 0, 932, 933, 5, 112, 0, 0, 933, 935, 3, 110, 55, 0, 934, 932, 1, 0, 0, 0, 935, 938, 1, 0, 0, 0, 936, 934, 1, 0, 0, 0, 936, 937, 1, 0, 0, 0, 937, 109, 1, 0, 0, 0, 938, 936, 1, 0, 0, 0, 939, 942, 3, 112, 56, 0, 940, 942, 3, 106, 53, 0, 941, 939, 1, 0, 0, 0, 941, 940, 1, 0, 0, 0, 942, 111, 1, 0, 0, 0, 943, 944, 5, 126, 0, 0, 944, 949, 3, 150, 75, 0, 945, 946, 5, 112, 0, 0, 946, 948, 3, 150, 75, 0, 947, 945, 1, 0, 0, 0, 948, 951, 1, 0, 0, 0, 949, 947, 1, 0, 0, 0, 949, 950, 1, 0, 0, 0, 950, 952, 1, 0, 0, 0, 951, 949, 1, 0, 0, 0, 952, 953, 5, 144, 0, 0, 953, 963, 1, 0, 0, 0, 954, 959, 3, 150, 75, 0, 955, 956, 5, 112, 0, 0, 956, 958, 3, 150, 75, 0, 957, 955, 1, 0, 0, 0, 958, 961, 1, 0, 0, 0, 959, 957, 1, 0, 0, 0, 959, 960, 1, 0, 0, 0, 960, 963, 1, 0, 0, 0, 961, 959, 1, 0, 0, 0, 962, 943, 1, 0, 0, 0, 962, 954, 1, 0, 0, 0, 963, 964, 1, 0, 0, 0, 964, 965, 5, 107, 0, 0, 965, 966, 3, 106, 53, 0, 966, 113, 1, 0, 0, 0, 967, 968, 5, 128, 0, 0, 968, 972, 3, 150, 75, 0, 969, 971, 3, 116, 58, 0, 970, 969, 1, 0, 0, 0, 971, 974, 1, 0, 0, 0, 972, 970, 1, 0, 0, 0, 972, 973, 1, 0, 0, 0, 973, 975, 1, 0, 0, 0, 974, 972, 1, 0, 0, 0, 975, 976, 5, 146, 0, 0, 976, 977, 5, 120, 0, 0, 977, 996, 1, 0, 0, 0, 978, 979, 5, 128, 0, 0, 979, 983, 3, 150, 75, 0, 980, 982, 3, 116, 58, 0, 981, 980, 1, 0, 0, 0, 982, 985, 1, 0, 0, 0, 983, 981, 1, 0, 0, 0, 983, 984, 1, 0, 0, 0, 984, 986, 1, 0, 0, 0, 985, 983, 1, 0, 0, 0, 986, 988, 5, 120, 0, 0, 987, 989, 3, 114, 57, 0, 988, 987, 1, 0, 0, 0, 988, 989, 1, 0, 0, 0, 989, 990, 1, 0, 0, 0, 990, 991, 5, 128, 0, 0, 991, 992, 5, 146, 0, 0, 992, 993, 3, 150, 75, 0, 993, 994, 5, 120, 0, 0, 994, 996, 1, 0, 0, 0, 995, 967, 1, 0, 0, 0, 995, 978, 1, 0, 0, 0, 996, 115, 1, 0, 0, 0, 997, 998, 3, 150, 75, 0, 998, 999, 5, 118, 0, 0, 999, 1000, 3, 156, 78, 0, 1000, 1009, 1, 0, 0, 0, 1001, 1002, 3, 150, 75, 0, 1002, 1003, 5, 118, 0, 0, 1003, 1004, 5, 124, 0, 0, 1004, 1005, 3, 106, 53, 0, 1005, 1006, 5, 142, 0, 0, 1006, 1009, 1, 0, 0, 0, 1007, 1009, 3, 150, 75, 0, 1008, 997, 1, 0, 0, 0, 1008, 1001, 1, 0, 0, 0, 1008, 1007, 1, 0, 0, 0, 1009, 117, 1, 0, 0, 0, 1010, 1015, 3, 120, 60, 0, 1011, 1012, 5, 112, 0, 0, 1012, 1014, 3, 120, 60, 0, 1013, 1011, 1, 0, 0, 0, 1014, 1017, 1, 0, 0, 0, 1015, 1013, 1, 0, 0, 0, 1015, 1016, 1, 0, 0, 0, 1016, 119, 1, 0, 0, 0, 1017, 1015, 1, 0, 0, 0, 1018, 1019, 3, 150, 75, 0, 1019, 1020, 5, 6, 0, 0, 1020, 1021, 5, 126, 0, 0, 1021, 1022, 3, 34, 17, 0, 1022, 1023, 5, 144, 0, 0, 1023, 1029, 1, 0, 0, 0, 1024, 1025, 3, 106, 53, 0, 1025, 1026, 5, 6, 0, 0, 1026, 1027, 3, 150, 75, 0, 1027, 1029, 1, 0, 0, 0, 1028, 1018, 1, 0, 0, 0, 1028, 1024, 1, 0, 0, 0, 1029, 121, 1, 0, 0, 0, 1030, 1038, 3, 154, 77, 0, 1031, 1032, 3, 130, 65, 0, 1032, 1033, 5, 116, 0, 0, 1033, 1035, 1, 0, 0, 0, 1034, 1031, 1, 0, 0, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1036, 1, 0, 0, 0, 1036, 1038, 3, 124, 62, 0, 1037, 1030, 1, 0, 0, 0, 1037, 1034, 1, 0, 0, 0, 1038, 123, 1, 0, 0, 0, 1039, 1044, 3, 150, 75, 0, 1040, 1041, 5, 116, 0, 0, 1041, 1043, 3, 150, 75, 0, 1042, 1040, 1, 0, 0, 0, 1043, 1046, 1, 0, 0, 0, 1044, 1042, 1, 0, 0, 0, 1044, 1045, 1, 0, 0, 0, 1045, 125, 1, 0, 0, 0, 1046, 1044, 1, 0, 0, 0, 1047, 1048, 6, 63, -1, 0, 1048, 1057, 3, 130, 65, 0, 1049, 1057, 3, 128, 64, 0, 1050, 1051, 5, 126, 0, 0, 1051, 1052, 3, 34, 17, 0, 1052, 1053, 5, 144, 0, 0, 1053, 1057, 1, 0, 0, 0, 1054, 1057, 3, 114, 57, 0, 1055, 1057, 3, 154, 77, 0, 1056, 1047, 1, 0, 0, 0, 1056, 1049, 1, 0, 0, 0, 1056, 1050, 1, 0, 0, 0, 1056, 1054, 1, 0, 0, 0, 1056, 1055, 1, 0, 0, 0, 1057, 1066, 1, 0, 0, 0, 1058, 1062, 10, 3, 0, 0, 1059, 1063, 3, 148, 74, 0, 1060, 1061, 5, 6, 0, 0, 1061, 1063, 3, 150, 75, 0, 1062, 1059, 1, 0, 0, 0, 1062, 1060, 1, 0, 0, 0, 1063, 1065, 1, 0, 0, 0, 1064, 1058, 1, 0, 0, 0, 1065, 1068, 1, 0, 0, 0, 1066, 1064, 1, 0, 0, 0, 1066, 1067, 1, 0, 0, 0, 1067, 127, 1, 0, 0, 0, 1068, 1066, 1, 0, 0, 0, 1069, 1070, 3, 150, 75, 0, 1070, 1072, 5, 126, 0, 0, 1071, 1073, 3, 132, 66, 0, 1072, 1071, 1, 0, 0, 0, 1072, 1073, 1, 0, 0, 0, 1073, 1074, 1, 0, 0, 0, 1074, 1075, 5, 144, 0, 0, 1075, 129, 1, 0, 0, 0, 1076, 1077, 3, 134, 67, 0, 1077, 1078, 5, 116, 0, 0, 1078, 1080, 1, 0, 0, 0, 1079, 1076, 1, 0, 0, 0, 1079, 1080, 1, 0, 0, 0, 1080, 1081, 1, 0, 0, 0, 1081, 1082, 3, 150, 75, 0, 1082, 131, 1, 0, 0, 0, 1083, 1088, 3, 106, 53, 0, 1084, 1085, 5, 112, 0, 0, 1085, 1087, 3, 106, 53, 0, 1086, 1084, 1, 0, 0, 0, 1087, 1090, 1, 0, 0, 0, 1088, 1086, 1, 0, 0, 0, 1088, 1089, 1, 0, 0, 0, 1089, 133, 1, 0, 0, 0, 1090, 1088, 1, 0, 0, 0, 1091, 1092, 3, 150, 75, 0, 1092, 135, 1, 0, 0, 0, 1093, 1102, 5, 102, 0, 0, 1094, 1095, 5, 116, 0, 0, 1095, 1102, 7, 11, 0, 0, 1096, 1097, 5, 104, 0, 0, 1097, 1099, 5, 116, 0, 0, 1098, 1100, 7, 11, 0, 0, 1099, 1098, 1, 0, 0, 0, 1099, 1100, 1, 0, 0, 0, 1100, 1102, 1, 0, 0, 0, 1101, 1093, 1, 0, 0, 0, 1101, 1094, 1, 0, 0, 0, 1101, 1096, 1, 0, 0, 0, 1102, 137, 1, 0, 0, 0, 1103, 1105, 7, 12, 0, 0, 1104, 1103, 1, 0, 0, 0, 1104, 1105, 1, 0, 0, 0, 1105, 1112, 1, 0, 0, 0, 1106, 1113, 3, 136, 68, 0, 1107, 1113, 5, 103, 0, 0, 1108, 1113, 5, 104, 0, 0, 1109, 1113, 5, 105, 0, 0, 1110, 1113, 5, 41, 0, 0, 1111, 1113, 5, 55, 0, 0, 1112, 1106, 1, 0, 0, 0, 1112, 1107, 1, 0, 0, 0, 1112, 1108, 1, 0, 0, 0, 1112, 1109, 1, 0, 0, 0, 1112, 1110, 1, 0, 0, 0, 1112, 1111, 1, 0, 0, 0, 1113, 139, 1, 0, 0, 0, 1114, 1118, 3, 138, 69, 0, 1115, 1118, 5, 106, 0, 0, 1116, 1118, 5, 57, 0, 0, 1117, 1114, 1, 0, 0, 0, 1117, 1115, 1, 0, 0, 0, 1117, 1116, 1, 0, 0, 0, 1118, 141, 1, 0, 0, 0, 1119, 1120, 7, 13, 0, 0, 1120, 143, 1, 0, 0, 0, 1121, 1122, 7, 14, 0, 0, 1122, 145, 1, 0, 0, 0, 1123, 1124, 7, 15, 0, 0, 1124, 147, 1, 0, 0, 0, 1125, 1128, 5, 101, 0, 0, 1126, 1128, 3, 146, 73, 0, 1127, 1125, 1, 0, 0, 0, 1127, 1126, 1, 0, 0, 0, 1128, 149, 1, 0, 0, 0, 1129, 1133, 5, 101, 0, 0, 1130, 1133, 3, 142, 71, 0, 1131, 1133, 3, 144, 72, 0, 1132, 1129, 1, 0, 0, 0, 1132, 1130, 1, 0, 0, 0, 1132, 1131, 1, 0, 0, 0, 1133, 151, 1, 0, 0, 0, 1134, 1135, 3, 156, 78, 0, 1135, 1136, 5, 118, 0, 0, 1136, 1137, 3, 138, 69, 0, 1137, 153, 1, 0, 0, 0, 1138, 1139, 5, 124, 0, 0, 1139, 1140, 3, 150, 75, 0, 1140, 1141, 5, 142, 0, 0, 1141, 155, 1, 0, 0, 0, 1142, 1145, 5, 106, 0, 0, 1143, 1145, 3, 158, 79, 0, 1144, 1142, 1, 0, 0, 0, 1144, 1143, 1, 0, 0, 0, 1145, 157, 1, 0, 0, 0, 1146, 1150, 5, 137, 0, 0, 1147, 1149, 3, 160, 80, 0, 1148, 1147, 1, 0, 0, 0, 1149, 1152, 1, 0, 0, 0, 1150, 1148, 1, 0, 0, 0, 1150, 1151, 1, 0, 0, 0, 1151, 1153, 1, 0, 0, 0, 1152, 1150, 1, 0, 0, 0, 1153, 1154, 5, 139, 0, 0, 1154, 159, 1, 0, 0, 0, 1155, 1156, 5, 152, 0, 0, 1156, 1157, 3, 106, 53, 0, 1157, 1158, 5, 142, 0, 0, 1158, 1161, 1, 0, 0, 0, 1159, 1161, 5, 151, 0, 0, 1160, 1155, 1, 0, 0, 0, 1160, 1159, 1, 0, 0, 0, 1161, 161, 1, 0, 0, 0, 1162, 1166, 5, 138, 0, 0, 1163, 1165, 3, 164, 82, 0, 1164, 1163, 1, 0, 0, 0, 1165, 1168, 1, 0, 0, 0, 1166, 1164, 1, 0, 0, 0, 1166, 1167, 1, 0, 0, 0, 1167, 1169, 1, 0, 0, 0, 1168, 1166, 1, 0, 0, 0, 1169, 1170, 5, 0, 0, 1, 1170, 163, 1, 0, 0, 0, 1171, 1172, 5, 154, 0, 0, 1172, 1173, 3, 106, 53, 0, 1173, 1174, 5, 142, 0, 0, 1174, 1177, 1, 0, 0, 0, 1175, 1177, 5, 153, 0, 0, 1176, 1171, 1, 0, 0, 0, 1176, 1175, 1, 0, 0, 0, 1177, 165, 1, 0, 0, 0, 145, 169, 176, 185, 192, 203, 207, 210, 219, 227, 233, 245, 253, 267, 273, 283, 292, 295, 299, 302, 306, 309, 312, 315, 318, 322, 326, 329, 332, 335, 339, 342, 351, 357, 378, 395, 412, 418, 424, 435, 437, 448, 451, 457, 465, 471, 473, 477, 482, 485, 488, 492, 496, 499, 501, 504, 508, 512, 515, 517, 519, 524, 535, 541, 548, 553, 557, 561, 567, 569, 576, 584, 587, 590, 609, 623, 639, 651, 663, 671, 675, 682, 688, 697, 701, 725, 742, 748, 751, 754, 764, 770, 773, 776, 784, 787, 791, 794, 808, 825, 830, 834, 840, 847, 859, 863, 866, 875, 889, 916, 924, 926, 928, 936, 941, 949, 959, 962, 972, 983, 988, 995, 1008, 1015, 1028, 1034, 1037, 1044, 1056, 1062, 1066, 1072, 1079, 1088, 1099, 1101, 1104, 1112, 1117, 1127, 1132, 1144, 1150, 1160, 1166, 1176] \ No newline at end of file diff --git a/hogql_parser/HogQLParserBaseVisitor.h b/hogql_parser/HogQLParserBaseVisitor.h index a01c37dc8c33e..52c96aae23565 100644 --- a/hogql_parser/HogQLParserBaseVisitor.h +++ b/hogql_parser/HogQLParserBaseVisitor.h @@ -31,10 +31,6 @@ class HogQLParserBaseVisitor : public HogQLParserVisitor { return visitChildren(ctx); } - virtual std::any visitVarAssignment(HogQLParser::VarAssignmentContext *ctx) override { - return visitChildren(ctx); - } - virtual std::any visitIdentifierList(HogQLParser::IdentifierListContext *ctx) override { return visitChildren(ctx); } @@ -43,7 +39,7 @@ class HogQLParserBaseVisitor : public HogQLParserVisitor { return visitChildren(ctx); } - virtual std::any visitExprStmt(HogQLParser::ExprStmtContext *ctx) override { + virtual std::any visitReturnStmt(HogQLParser::ReturnStmtContext *ctx) override { return visitChildren(ctx); } @@ -55,11 +51,15 @@ class HogQLParserBaseVisitor : public HogQLParserVisitor { return visitChildren(ctx); } - virtual std::any visitReturnStmt(HogQLParser::ReturnStmtContext *ctx) override { + virtual std::any visitFuncStmt(HogQLParser::FuncStmtContext *ctx) override { return visitChildren(ctx); } - virtual std::any visitFuncStmt(HogQLParser::FuncStmtContext *ctx) override { + virtual std::any visitVarAssignment(HogQLParser::VarAssignmentContext *ctx) override { + return visitChildren(ctx); + } + + virtual std::any visitExprStmt(HogQLParser::ExprStmtContext *ctx) override { return visitChildren(ctx); } diff --git a/hogql_parser/HogQLParserVisitor.h b/hogql_parser/HogQLParserVisitor.h index d68806e5c78da..c1d69d7f8eff4 100644 --- a/hogql_parser/HogQLParserVisitor.h +++ b/hogql_parser/HogQLParserVisitor.h @@ -27,22 +27,22 @@ class HogQLParserVisitor : public antlr4::tree::AbstractParseTreeVisitor { virtual std::any visitVarDecl(HogQLParser::VarDeclContext *context) = 0; - virtual std::any visitVarAssignment(HogQLParser::VarAssignmentContext *context) = 0; - virtual std::any visitIdentifierList(HogQLParser::IdentifierListContext *context) = 0; virtual std::any visitStatement(HogQLParser::StatementContext *context) = 0; - virtual std::any visitExprStmt(HogQLParser::ExprStmtContext *context) = 0; + virtual std::any visitReturnStmt(HogQLParser::ReturnStmtContext *context) = 0; virtual std::any visitIfStmt(HogQLParser::IfStmtContext *context) = 0; virtual std::any visitWhileStmt(HogQLParser::WhileStmtContext *context) = 0; - virtual std::any visitReturnStmt(HogQLParser::ReturnStmtContext *context) = 0; - virtual std::any visitFuncStmt(HogQLParser::FuncStmtContext *context) = 0; + virtual std::any visitVarAssignment(HogQLParser::VarAssignmentContext *context) = 0; + + virtual std::any visitExprStmt(HogQLParser::ExprStmtContext *context) = 0; + virtual std::any visitEmptyStmt(HogQLParser::EmptyStmtContext *context) = 0; virtual std::any visitBlock(HogQLParser::BlockContext *context) = 0; diff --git a/hogql_parser/parser.cpp b/hogql_parser/parser.cpp index 274aa741ae24a..c1a85b8d8cfd6 100644 --- a/hogql_parser/parser.cpp +++ b/hogql_parser/parser.cpp @@ -1059,9 +1059,7 @@ class HogQLParseTreeConverter : public HogQLParserBaseVisitor { VISIT(ColumnExprAlias) { string alias; - if (ctx->alias()) { - alias = visitAsString(ctx->alias()); - } else if (ctx->identifier()) { + if (ctx->identifier()) { alias = visitAsString(ctx->identifier()); } else if (ctx->STRING_LITERAL()) { alias = parse_string_literal_ctx(ctx->STRING_LITERAL()); diff --git a/hogvm/__tests__/__snapshots__/functions.hoge b/hogvm/__tests__/__snapshots__/functions.hoge index 40a8579bcab4a..cc66c005c1230 100644 --- a/hogvm/__tests__/__snapshots__/functions.hoge +++ b/hogvm/__tests__/__snapshots__/functions.hoge @@ -1,10 +1,9 @@ ["_h", 32, "-- test functions --", 2, "print", 1, 35, 41, "add", 2, 6, 36, 0, 36, 1, 6, 38, 41, "add2", 2, 9, 36, 0, 36, 1, 6, 36, 2, 38, 35, 41, "mult", 2, 6, 36, 0, 36, 1, 8, 38, 41, "noArgs", 0, 12, 32, "basdfasdf", 33, 3, 33, 2, 6, 36, -1, 38, 35, 35, 41, "empty", 0, 2, 31, 38, 41, "empty2", 0, 4, 29, 35, 31, 38, 41, "empty3", 0, 8, 29, 35, 29, 35, 29, -35, 31, 38, 41, "noReturn", 0, 14, 33, 1, 33, 2, 36, 1, 36, 0, 6, 31, 38, 35, 35, 35, 33, 4, 33, 3, 2, "add", 2, 2, -"print", 1, 35, 33, 1, 33, 1, 2, "add", 2, 33, 100, 33, 4, 33, 3, 2, "add", 2, 6, 6, 2, "print", 1, 35, 33, -1, 2, -"noArgs", 0, 2, "ifNull", 2, 2, "print", 1, 35, 33, -1, 2, "empty", 0, 2, "ifNull", 2, 2, "print", 1, 35, 33, -1, 2, -"empty2", 0, 2, "ifNull", 2, 2, "print", 1, 35, 33, -1, 2, "empty3", 0, 2, "ifNull", 2, 2, "print", 1, 35, 33, -1, 2, -"noReturn", 0, 2, "ifNull", 2, 2, "print", 1, 35, 33, 2, 33, 1, 33, 2, 2, "add", 2, 33, 100, 33, 4, 33, 3, 2, "add", 2, -6, 6, 2, "mult", 2, 2, "print", 1, 35, 33, 10, 33, 1, 33, 2, 2, "add2", 2, 33, 100, 33, 4, 33, 3, 2, "add2", 2, 6, 6, 2, -"mult", 2, 2, "print", 1, 35] +1, 38, 35, 35, 41, "empty", 0, 2, 31, 38, 41, "empty2", 0, 2, 31, 38, 41, "empty3", 0, 2, 31, 38, 41, "noReturn", 0, 14, +33, 1, 33, 2, 36, 1, 36, 0, 6, 31, 38, 35, 35, 35, 33, 4, 33, 3, 2, "add", 2, 2, "print", 1, 35, 33, 1, 33, 1, 2, "add", +2, 33, 100, 33, 4, 33, 3, 2, "add", 2, 6, 6, 2, "print", 1, 35, 33, -1, 2, "noArgs", 0, 2, "ifNull", 2, 2, "print", 1, +35, 33, -1, 2, "empty", 0, 2, "ifNull", 2, 2, "print", 1, 35, 33, -1, 2, "empty2", 0, 2, "ifNull", 2, 2, "print", 1, 35, +33, -1, 2, "empty3", 0, 2, "ifNull", 2, 2, "print", 1, 35, 33, -1, 2, "noReturn", 0, 2, "ifNull", 2, 2, "print", 1, 35, +33, 2, 33, 1, 33, 2, 2, "add", 2, 33, 100, 33, 4, 33, 3, 2, "add", 2, 6, 6, 2, "mult", 2, 2, "print", 1, 35, 33, 10, 33, +1, 33, 2, 2, "add2", 2, 33, 100, 33, 4, 33, 3, 2, "add2", 2, 6, 6, 2, "mult", 2, 2, "print", 1, 35] diff --git a/hogvm/__tests__/arrays.hog b/hogvm/__tests__/arrays.hog index 86c695e7a7918..af6f0c81decd8 100644 --- a/hogvm/__tests__/arrays.hog +++ b/hogvm/__tests__/arrays.hog @@ -1,12 +1,12 @@ -print([]); -print([1, 2, 3]); -print([1, '2', 3]); -print([1, [2, 3], 4]); -print([1, [2, [3, 4]], 5]); +print([]) +print([1, 2, 3]) +print([1, '2', 3]) +print([1, [2, 3], 4]) +print([1, [2, [3, 4]], 5]) -let a := [1, 2, 3]; -print(a[1]); -print([1, 2, 3][1]); -print([1, [2, [3, 4]], 5][1][1][1]); -print([1, [2, [3, 4]], 5][1][1][1] + 1); -print([1, [2, [3, 4]], 5].1.1.1); +let a := [1, 2, 3] +print(a[1]) +print([1, 2, 3][1]) +print([1, [2, [3, 4]], 5][1][1][1]) +print([1, [2, [3, 4]], 5][1][1][1] + 1) +print([1, [2, [3, 4]], 5].1.1.1) diff --git a/hogvm/__tests__/dicts.hog b/hogvm/__tests__/dicts.hog index d76b174bb608c..c1cdf03fbef81 100644 --- a/hogvm/__tests__/dicts.hog +++ b/hogvm/__tests__/dicts.hog @@ -1,13 +1,13 @@ -print({}); -print({'key': 'value'}); -print({'key': 'value', 'other': 'thing'}); -print({'key': {'otherKey': 'value'}}); -print({key: 'value'}); +print({}) +print({'key': 'value'}) +print({'key': 'value', 'other': 'thing'}) +print({'key': {'otherKey': 'value'}}) +print({key: 'value'}) -let key := 3; -print({key: 'value'}); +let key := 3 +print({key: 'value'}) -print({'key': 'value'}.key); -print({'key': 'value'}['key']); -print({'key': {'otherKey': 'value'}}.key.otherKey); -print({'key': {'otherKey': 'value'}}['key'].otherKey); +print({'key': 'value'}.key) +print({'key': 'value'}['key']) +print({'key': {'otherKey': 'value'}}.key.otherKey) +print({'key': {'otherKey': 'value'}}['key'].otherKey) diff --git a/hogvm/__tests__/functions.hog b/hogvm/__tests__/functions.hog index 57f7491002e45..bc4c677b8f35b 100644 --- a/hogvm/__tests__/functions.hog +++ b/hogvm/__tests__/functions.hog @@ -1,35 +1,35 @@ -print('-- test functions --'); +print('-- test functions --') fn add(a, b) { - return a + b; + return a + b } fn add2(a, b) { - let c := a + b; - return c; + let c := a + b + return c } fn mult(a, b) { - return a * b; + return a * b } fn noArgs() { - let url := 'basdfasdf'; - let second := 2 + 3; - return second; + let url := 'basdfasdf' + let second := 2 + 3 + return second } fn empty() {} -fn empty2() {;} -fn empty3() {;;;} +fn empty2() {} +fn empty3() {} fn noReturn() { - let a := 1; - let b := 2; - let c := a + b; + let a := 1 + let b := 2 + let c := a + b } -print(add(3, 4)); -print(add(3, 4) + 100 + add(1, 1)); -print(noArgs() ?? -1); -print(empty() ?? -1); -print(empty2() ?? -1); -print(empty3() ?? -1); -print(noReturn() ?? -1); -print(mult(add(3, 4) + 100 + add(2, 1), 2)); -print(mult(add2(3, 4) + 100 + add2(2, 1), 10)); +print(add(3, 4)) +print(add(3, 4) + 100 + add(1, 1)) +print(noArgs() ?? -1) +print(empty() ?? -1) +print(empty2() ?? -1) +print(empty3() ?? -1) +print(noReturn() ?? -1) +print(mult(add(3, 4) + 100 + add(2, 1), 2)) +print(mult(add2(3, 4) + 100 + add2(2, 1), 10)) diff --git a/hogvm/__tests__/ifElse.hog b/hogvm/__tests__/ifElse.hog index 85b584a2049c4..ea68bd00e81b6 100644 --- a/hogvm/__tests__/ifElse.hog +++ b/hogvm/__tests__/ifElse.hog @@ -1,15 +1,15 @@ -print('-- test if else --'); +print('-- test if else --') { - if (true) print(1); else print(2); - if (true) print(1); else print(2); - if (false) print(1); else print(2); - if (true) { print(1); } else { print(2); } + if (true) print(1) else print(2) + if (true) print(1) else print(2) + if (false) print(1) else print(2) + if (true) { print(1) } else { print(2) } - let a := true; + let a := true if (a) { - let a := 3; - print(a + 2); + let a := 3 + print(a + 2) } else { - print(2); + print(2) } } diff --git a/hogvm/__tests__/json.hog b/hogvm/__tests__/json.hog index 3ad74f8f70857..faa7aa7a1b667 100644 --- a/hogvm/__tests__/json.hog +++ b/hogvm/__tests__/json.hog @@ -1,8 +1,8 @@ ---- Commented out because python and JS add different spaces to their JSON output --- print(jsonStringify({'$browser': 'Chrome', '$os': 'Windows' })); --- print(jsonStringify({'$browser': 'Chrome', '$os': 'Windows' }, 3)); +-- print(jsonStringify({'$browser': 'Chrome', '$os': 'Windows' })) +-- print(jsonStringify({'$browser': 'Chrome', '$os': 'Windows' }, 3)) -print(jsonParse('[1,2,3]')); +print(jsonParse('[1,2,3]')) let event := { 'event': '$pageview', @@ -10,6 +10,6 @@ let event := { '$browser': 'Chrome', '$os': 'Windows' } -}; -let json := jsonStringify(event); -print(jsonParse(json)); +} +let json := jsonStringify(event) +print(jsonParse(json)) diff --git a/hogvm/__tests__/loops.hog b/hogvm/__tests__/loops.hog index ecd8e9a0c9f8e..3f59f9d7c1937 100644 --- a/hogvm/__tests__/loops.hog +++ b/hogvm/__tests__/loops.hog @@ -1,9 +1,9 @@ -print('-- test while loop --'); +print('-- test while loop --') { - let i := 0; + let i := 0 while (i < 3) { - i := i + 1; - print(i); + i := i + 1 + print(i) } - print(i); + print(i) } diff --git a/hogvm/__tests__/mandelbrot.hog b/hogvm/__tests__/mandelbrot.hog index b9af4409053b9..6ac17b2989d57 100644 --- a/hogvm/__tests__/mandelbrot.hog +++ b/hogvm/__tests__/mandelbrot.hog @@ -1,44 +1,44 @@ fn mandelbrot(re, im, max_iter) { - let z_re := 0.0; - let z_im := 0.0; - let n := 0; + let z_re := 0.0 + let z_im := 0.0 + let n := 0 while (z_re*z_re + z_im*z_im <= 4 and n < max_iter) { - let temp_re := z_re * z_re - z_im * z_im + re; - let temp_im := 2 * z_re * z_im + im; - z_re := temp_re; - z_im := temp_im; - n := n + 1; + let temp_re := z_re * z_re - z_im * z_im + re + let temp_im := 2 * z_re * z_im + im + z_re := temp_re + z_im := temp_im + n := n + 1 } if (n == max_iter) { - return ' '; + return ' ' } else { - return '#'; + return '#' } } fn main() { - let width := 80; - let height := 24; - let xmin := -2.0; - let xmax := 1.0; - let ymin := -1.0; - let ymax := 1.0; - let max_iter := 30; + let width := 80 + let height := 24 + let xmin := -2.0 + let xmax := 1.0 + let ymin := -1.0 + let ymax := 1.0 + let max_iter := 30 - let y := 0; + let y := 0 while(y < height) { - let row := ''; - let x := 0; + let row := '' + let x := 0 while (x < width) { - let re := x / width * (xmax - xmin) + xmin; - let im := y / height * (ymax - ymin) + ymin; - let letter := mandelbrot(re, im, max_iter); - row := concat(row, letter); - x := x + 1; + let re := x / width * (xmax - xmin) + xmin + let im := y / height * (ymax - ymin) + ymin + let letter := mandelbrot(re, im, max_iter) + row := concat(row, letter) + x := x + 1 } - print(row); - y := y + 1; + print(row) + y := y + 1 } } -main(); \ No newline at end of file +main() \ No newline at end of file diff --git a/hogvm/__tests__/mandelbrot.hoge b/hogvm/__tests__/mandelbrot.hoge deleted file mode 100644 index 211995efe2bc9..0000000000000 --- a/hogvm/__tests__/mandelbrot.hoge +++ /dev/null @@ -1,8 +0,0 @@ -["_h", 41, "mandelbrot", 3, 93, 34, 0.0, 34, 0.0, 33, 0, 36, 0, 36, 5, 15, 33, 4, 36, 4, 36, 4, 8, 36, 3, 36, 3, 8, 6, -16, 3, 2, 40, 44, 36, 2, 36, 4, 36, 4, 8, 36, 3, 36, 3, 8, 7, 6, 36, 1, 36, 4, 36, 3, 33, 2, 8, 8, 6, 36, 6, 37, 3, 36, -7, 37, 4, 33, 1, 36, 5, 6, 37, 5, 35, 35, 39, -67, 36, 0, 36, 5, 11, 40, 5, 32, " ", 38, 39, 3, 32, "#", 38, 31, 38, 35, -35, 35, 41, "main", 0, 119, 33, 80, 33, 24, 34, -2.0, 34, 1.0, 34, -1.0, 34, 1.0, 33, 30, 33, 0, 36, 1, 36, 7, 15, 40, -86, 32, "", 33, 0, 36, 0, 36, 9, 15, 40, 58, 36, 2, 36, 2, 36, 3, 7, 36, 0, 36, 9, 9, 8, 6, 36, 4, 36, 4, 36, 5, 7, 36, -1, 36, 7, 9, 8, 6, 36, 6, 36, 11, 36, 10, 2, "mandelbrot", 3, 36, 12, 36, 8, 2, "concat", 2, 37, 8, 33, 1, 36, 9, 6, 37, -9, 35, 35, 35, 39, -65, 36, 8, 2, "print", 1, 35, 33, 1, 36, 7, 6, 37, 7, 35, 35, 39, -93, 31, 38, 35, 35, 35, 35, 35, -35, 35, 35, 2, "main", 0, 35] diff --git a/hogvm/__tests__/operations.hog b/hogvm/__tests__/operations.hog index c89918d9ed62c..25eca892ab190 100644 --- a/hogvm/__tests__/operations.hog +++ b/hogvm/__tests__/operations.hog @@ -1,70 +1,70 @@ fn test(val) { - print(jsonStringify(val)); + print(jsonStringify(val)) } -print('-- test the most common expressions --'); -test(1 + 2); -- 3 -test(1 - 2); -- -1 -test(3 * 2); -- 6 -test(3 / 2); -- 1.5 -test(3 % 2); -- 1 -test(1 and 2); -- true -test(1 or 0); -- true -test(1 and 0); -- false -test(1 or (0 and 1) or 2); -- true -test((1 and 0) and 1); -- false -test((1 or 2) and (1 or 2)); -- true -test(true); -- true -test(not true); -- false -test(false); -- false -test(null); -- null -test(3.14); -- 3.14 -test(1 = 2); -- false -test(1 == 2); -- false -test(1 != 2); -- true -test(1 < 2); -- true -test(1 <= 2); -- true -test(1 > 2); -- false -test(1 >= 2); -- false -test('a' like 'b'); -- false -test('baa' like '%a%'); -- true -test('baa' like '%x%'); -- false -test('baa' ilike '%A%'); -- true -test('baa' ilike '%C%'); -- false -test('a' ilike 'b'); -- false -test('a' not like 'b'); -- true -test('a' not ilike 'b'); -- true -test('a' in 'car'); -- true -test('a' in 'foo'); -- false -test('a' not in 'car'); -- false -test(properties.bla); -- null -test(properties.foo); -- "bar" -test(ifNull(properties.foo, false)); -- "bar" -test(ifNull(properties.nullValue, false)); -- false -test(concat('arg', 'another')); -- 'arganother' -test(concat(1, NULL)); -- '1' -test(concat(true, false)); -- 'truefalse' -test(match('test', 'e.*')); -- true -test(match('test', '^e.*')); -- false -test(match('test', 'x.*')); -- false -test('test' =~ 'e.*'); -- true -test('test' !~ 'e.*'); -- false -test('test' =~ '^e.*'); -- false -test('test' !~ '^e.*'); -- true -test('test' =~ 'x.*'); -- false -test('test' !~ 'x.*'); -- true -test('test' ~* 'EST'); -- true -test('test' =~* 'EST'); -- true -test('test' !~* 'EST'); -- false -test(toString(1)); -- '1' -test(toString(1.5)); -- '1.5' -test(toString(true)); -- 'true' -test(toString(null)); -- 'null' -test(toString('string')); -- 'string' -test(toInt('1')); -- 1 -test(toInt('bla')); -- null -test(toFloat('1.2')); -- 1.2 -test(toFloat('bla')); -- null -test(toUUID('asd')); -- 'asd' -test(1 == null); -- false -test(1 != null); -- true +print('-- test the most common expressions --') +test(1 + 2) -- 3 +test(1 - 2) -- -1 +test(3 * 2) -- 6 +test(3 / 2) -- 1.5 +test(3 % 2) -- 1 +test(1 and 2) -- true +test(1 or 0) -- true +test(1 and 0) -- false +test(1 or (0 and 1) or 2) -- true +test((1 and 0) and 1) -- false +test((1 or 2) and (1 or 2)) -- true +test(true) -- true +test(not true) -- false +test(false) -- false +test(null) -- null +test(3.14) -- 3.14 +test(1 = 2) -- false +test(1 == 2) -- false +test(1 != 2) -- true +test(1 < 2) -- true +test(1 <= 2) -- true +test(1 > 2) -- false +test(1 >= 2) -- false +test('a' like 'b') -- false +test('baa' like '%a%') -- true +test('baa' like '%x%') -- false +test('baa' ilike '%A%') -- true +test('baa' ilike '%C%') -- false +test('a' ilike 'b') -- false +test('a' not like 'b') -- true +test('a' not ilike 'b') -- true +test('a' in 'car') -- true +test('a' in 'foo') -- false +test('a' not in 'car') -- false +test(properties.bla) -- null +test(properties.foo) -- "bar" +test(ifNull(properties.foo, false)) -- "bar" +test(ifNull(properties.nullValue, false)) -- false +test(concat('arg', 'another')) -- 'arganother' +test(concat(1, NULL)) -- '1' +test(concat(true, false)) -- 'truefalse' +test(match('test', 'e.*')) -- true +test(match('test', '^e.*')) -- false +test(match('test', 'x.*')) -- false +test('test' =~ 'e.*') -- true +test('test' !~ 'e.*') -- false +test('test' =~ '^e.*') -- false +test('test' !~ '^e.*') -- true +test('test' =~ 'x.*') -- false +test('test' !~ 'x.*') -- true +test('test' ~* 'EST') -- true +test('test' =~* 'EST') -- true +test('test' !~* 'EST') -- false +test(toString(1)) -- '1' +test(toString(1.5)) -- '1.5' +test(toString(true)) -- 'true' +test(toString(null)) -- 'null' +test(toString('string')) -- 'string' +test(toInt('1')) -- 1 +test(toInt('bla')) -- null +test(toFloat('1.2')) -- 1.2 +test(toFloat('bla')) -- null +test(toUUID('asd')) -- 'asd' +test(1 == null) -- false +test(1 != null) -- true diff --git a/hogvm/__tests__/properties.hog b/hogvm/__tests__/properties.hog index bd9310b74a58b..c3dde6b274810 100644 --- a/hogvm/__tests__/properties.hog +++ b/hogvm/__tests__/properties.hog @@ -1,49 +1,49 @@ { - let r := [1, 2, {'d': (1, 3, 42, 6)}]; - print(r.2.d.1); + let r := [1, 2, {'d': (1, 3, 42, 6)}] + print(r.2.d.1) } { - let r := [1, 2, {'d': (1, 3, 42, 6)}]; - print(r[2].d[2]); + let r := [1, 2, {'d': (1, 3, 42, 6)}] + print(r[2].d[2]) } { - let r := [1, 2, {'d': (1, 3, 42, 6)}]; - print(r.2['d'][3]); + let r := [1, 2, {'d': (1, 3, 42, 6)}] + print(r.2['d'][3]) } { - let r := {'d': (1, 3, 42, 6)}; - print(r.d.1); + let r := {'d': (1, 3, 42, 6)} + print(r.d.1) } { - let r := [1, 2, {'d': [1, 3, 42, 3]}]; - r.2.d.2 := 3; - print(r.2.d.2); + let r := [1, 2, {'d': [1, 3, 42, 3]}] + r.2.d.2 := 3 + print(r.2.d.2) } { - let r := [1, 2, {'d': [1, 3, 42, 3]}]; - r[2].d[2] := 3; - print(r[2].d[2]); + let r := [1, 2, {'d': [1, 3, 42, 3]}] + r[2].d[2] := 3 + print(r[2].d[2]) } { - let r := [1, 2, {'d': [1, 3, 42, 3]}]; - r[2].c := [666]; - print(r[2]); + let r := [1, 2, {'d': [1, 3, 42, 3]}] + r[2].c := [666] + print(r[2]) } { - let r := [1, 2, {'d': [1, 3, 42, 3]}]; - r[2].d[2] := 3; - print(r[2].d); + let r := [1, 2, {'d': [1, 3, 42, 3]}] + r[2].d[2] := 3 + print(r[2].d) } { - let r := [1, 2, {'d': [1, 3, 42, 3]}]; - r.2['d'] := ['a', 'b', 'c', 'd']; - print(r[2].d[2]); + let r := [1, 2, {'d': [1, 3, 42, 3]}] + r.2['d'] := ['a', 'b', 'c', 'd'] + print(r[2].d[2]) } { - let r := [1, 2, {'d': [1, 3, 42, 3]}]; - let g := 'd'; - r.2[g] := ['a', 'b', 'c', 'd']; - print(r[2].d[2]); + let r := [1, 2, {'d': [1, 3, 42, 3]}] + let g := 'd' + r.2[g] := ['a', 'b', 'c', 'd'] + print(r[2].d[2]) } { let event := { @@ -52,9 +52,9 @@ '$browser': 'Chrome', '$os': 'Windows' } - }; - event['properties']['$browser'] := 'Firefox'; - print(event); + } + event['properties']['$browser'] := 'Firefox' + print(event) } { let event := { @@ -63,9 +63,9 @@ '$browser': 'Chrome', '$os': 'Windows' } - }; - event.properties.$browser := 'Firefox'; - print(event); + } + event.properties.$browser := 'Firefox' + print(event) } { let event := { @@ -74,7 +74,7 @@ '$browser': 'Chrome', '$os': 'Windows' } - }; - let config := {}; - print(event); + } + let config := {} + print(event) } diff --git a/hogvm/__tests__/stl.hog b/hogvm/__tests__/stl.hog index d8cfdc24621bf..111b42e3abff9 100644 --- a/hogvm/__tests__/stl.hog +++ b/hogvm/__tests__/stl.hog @@ -1,4 +1,4 @@ -print('-- empty, notEmpty, length, lower, upper, reverse --'); -if (empty('') and notEmpty('234')) print(length('123')); -if (lower('Tdd4gh') == 'tdd4gh') print(upper('test')); -print(reverse('spinner')); +print('-- empty, notEmpty, length, lower, upper, reverse --') +if (empty('') and notEmpty('234')) print(length('123')) +if (lower('Tdd4gh') == 'tdd4gh') print(upper('test')) +print(reverse('spinner')) diff --git a/hogvm/__tests__/tuples.hog b/hogvm/__tests__/tuples.hog index 31fc9a7a64684..e7bd601c7ed2f 100644 --- a/hogvm/__tests__/tuples.hog +++ b/hogvm/__tests__/tuples.hog @@ -1,9 +1,9 @@ -print((1, 2, 3)); -print((1, '2', 3)); -print((1, (2, 3), 4)); -print((1, (2, (3, 4)), 5)); -let a := (1, 2, 3); -print(a[1]); -print((1, (2, (3, 4)), 5)[1][1][1]); -print((1, (2, (3, 4)), 5).1.1.1); -print((1, (2, (3, 4)), 5)[1][1][1] + 1); +print((1, 2, 3)) +print((1, '2', 3)) +print((1, (2, 3), 4)) +print((1, (2, (3, 4)), 5)) +let a := (1, 2, 3) +print(a[1]) +print((1, (2, (3, 4)), 5)[1][1][1]) +print((1, (2, (3, 4)), 5).1.1.1) +print((1, (2, (3, 4)), 5)[1][1][1] + 1) diff --git a/hogvm/__tests__/variables.hog b/hogvm/__tests__/variables.hog index 8f4ac488616fd..d8c1e310cf30e 100644 --- a/hogvm/__tests__/variables.hog +++ b/hogvm/__tests__/variables.hog @@ -1,16 +1,16 @@ -print('-- test variables --'); +print('-- test variables --') { - let a := 1 + 2; - print(a); + let a := 1 + 2 + print(a) - let b := a + 4; - print(b); + let b := a + 4 + print(b) } -print('-- test variable reassignment --'); +print('-- test variable reassignment --') { - let a := 1; - a := a + 3; - a := a * 2; - print(a); + let a := 1 + a := a + 3 + a := a * 2 + print(a) } \ No newline at end of file diff --git a/hogvm/python/execute.py b/hogvm/python/execute.py index ae821a82419d7..2a17447c9bd62 100644 --- a/hogvm/python/execute.py +++ b/hogvm/python/execute.py @@ -56,6 +56,9 @@ def pop_stack(): if next_token() != HOGQL_BYTECODE_IDENTIFIER: raise HogVMException(f"Invalid bytecode. Must start with '{HOGQL_BYTECODE_IDENTIFIER}'") + if len(bytecode) == 1: + return BytecodeResult(result=None, stdout=stdout, bytecode=bytecode) + def check_timeout(): if time.time() - start_time > timeout and not debug: raise HogVMException(f"Execution timed out after {timeout} seconds. Performed {ops} ops.") diff --git a/hogvm/typescript/src/index.ts b/hogvm/typescript/src/index.ts new file mode 100644 index 0000000000000..20f547aef5f9f --- /dev/null +++ b/hogvm/typescript/src/index.ts @@ -0,0 +1,3 @@ +export * from './execute' +export * from './operation' +export * from './utils' diff --git a/mypy-baseline.txt b/mypy-baseline.txt index d3e4f2d3fc605..300c901c82196 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -26,67 +26,18 @@ posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Un posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unpacked dict entry 1 has incompatible type "dict[str, Any] | None"; expected "SupportsKeysAndGetItem[str, Any]" [dict-item] posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unpacked dict entry 0 has incompatible type "dict[str, Any] | None"; expected "SupportsKeysAndGetItem[str, ResolveParamConfig | IncrementalParamConfig | Any]" [dict-item] posthog/temporal/data_imports/pipelines/rest_source/config_setup.py:0: error: Unpacked dict entry 1 has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "SupportsKeysAndGetItem[str, ResolveParamConfig | IncrementalParamConfig | Any]" [dict-item] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Not all union combinations were tried because there are too many unions [misc] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 2 to "source" has incompatible type "str | None"; expected "str" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 3 to "source" has incompatible type "str | None"; expected "str" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 4 to "source" has incompatible type "int | None"; expected "int" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 6 to "source" has incompatible type "Schema | None"; expected "Schema" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 7 to "source" has incompatible type "Literal['evolve', 'discard_value', 'freeze', 'discard_row'] | TSchemaContractDict | None"; expected "Literal['evolve', 'discard_value', 'freeze', 'discard_row'] | TSchemaContractDict" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 8 to "source" has incompatible type "type[BaseConfiguration] | None"; expected "type[BaseConfiguration]" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "build_resource_dependency_graph" has incompatible type "EndpointResourceBase | None"; expected "EndpointResourceBase" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Need type annotation for "resources" (hint: "resources: dict[, ] = ...") [var-annotated] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible types in assignment (expression has type "ResolvedParam | None", variable has type "ResolvedParam") [assignment] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible types in assignment (expression has type "list[str] | None", variable has type "list[str]") [assignment] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "setup_incremental_object" has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "dict[str, Any]" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Statement is unreachable [unreachable] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "exclude_keys" has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "Mapping[str, Any]" [arg-type] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible default for argument "incremental_param" (default has type "IncrementalParam | None", argument has type "IncrementalParam") [assignment] -posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument "module" to "SourceInfo" has incompatible type Module | None; expected Module [arg-type] posthog/hogql/database/schema/numbers.py:0: error: Incompatible types in assignment (expression has type "dict[str, IntegerDatabaseField]", variable has type "dict[str, FieldOrTable]") [assignment] posthog/hogql/database/schema/numbers.py:0: note: "Dict" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance posthog/hogql/database/schema/numbers.py:0: note: Consider using "Mapping" instead, which is covariant in the value type posthog/hogql/ast.py:0: error: Incompatible return value type (got "bool | None", expected "bool") [return-value] -posthog/hogql/visitor.py:0: error: Statement is unreachable [unreachable] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Type | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Type | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Type | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "RatioExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Constant | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "JoinConstraint | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "JoinExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "JoinExpr | None"; expected "AST" [arg-type] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "CTE") [assignment] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "CTE") [assignment] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "CTE") [assignment] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "OrderExpr", variable has type "CTE") [assignment] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "CTE") [assignment] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "WindowExpr", variable has type "CTE") [assignment] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "FieldAliasType", variable has type "BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType | SelectViewType") [assignment] posthog/hogql/visitor.py:0: error: Incompatible types in assignment (expression has type "Type", variable has type "BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType | SelectViewType") [assignment] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "WindowFrameExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "WindowFrameExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "WindowExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Constant | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "RatioExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "JoinExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "JoinConstraint | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "SampleExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "JoinExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument "select" to "SelectQuery" has incompatible type "list[Expr] | None"; expected "list[Expr]" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "Expr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "WindowFrameExpr | None"; expected "AST" [arg-type] -posthog/hogql/visitor.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "WindowFrameExpr | None"; expected "AST" [arg-type] posthog/hogql/resolver_utils.py:0: error: Argument 1 to "lookup_field_by_name" has incompatible type "SelectQueryType | SelectUnionQueryType"; expected "SelectQueryType" [arg-type] posthog/hogql/parser.py:0: error: Item "None" of "list[Expr] | None" has no attribute "__iter__" (not iterable) [union-attr] posthog/hogql/parser.py:0: error: "None" has no attribute "text" [attr-defined] @@ -214,21 +165,15 @@ posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item " posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "select" [union-attr] posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item "SelectUnionQuery" of "SelectQuery | SelectUnionQuery" has no attribute "group_by" [union-attr] posthog/hogql_queries/insights/trends/aggregation_operations.py:0: error: Item "None" of "list[Expr] | Any | None" has no attribute "append" [union-attr] -posthog/hogql/resolver.py:0: error: Argument 1 of "visit" is incompatible with supertype "Visitor"; supertype defines the argument type as "AST" [override] +posthog/hogql/resolver.py:0: error: Argument 1 of "visit" is incompatible with supertype "Visitor"; supertype defines the argument type as "AST | None" [override] posthog/hogql/resolver.py:0: note: This violates the Liskov substitution principle posthog/hogql/resolver.py:0: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides posthog/hogql/resolver.py:0: error: List comprehension has incompatible type List[SelectQueryType | None]; expected List[SelectQueryType] [misc] posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "JoinExpr | None") [assignment] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "JoinExpr | None"; expected "Expr" [arg-type] posthog/hogql/resolver.py:0: error: Need type annotation for "columns_with_visible_alias" (hint: "columns_with_visible_alias: dict[, ] = ...") [var-annotated] posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Type | None", target has type "Type") [assignment] posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Type | None", target has type "Type") [assignment] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "Expr | None"; expected "Expr" [arg-type] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "Expr | None"; expected "Expr" [arg-type] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "Expr | None"; expected "Expr" [arg-type] posthog/hogql/resolver.py:0: error: List comprehension has incompatible type List[Expr]; expected List[OrderExpr] [misc] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "Expr | None"; expected "Expr" [arg-type] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "Expr | None"; expected "Expr" [arg-type] posthog/hogql/resolver.py:0: error: Value expression in dictionary comprehension has incompatible type "Expr"; expected type "WindowExpr" [misc] posthog/hogql/resolver.py:0: error: Statement is unreachable [unreachable] posthog/hogql/resolver.py:0: error: Argument 2 to "lookup_cte_by_name" has incompatible type "str | int"; expected "str" [arg-type] @@ -245,12 +190,9 @@ posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression posthog/hogql/resolver.py:0: error: Invalid index type "str | int" for "dict[str, BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType | SelectViewType]"; expected type "str" [index] posthog/hogql/resolver.py:0: error: Argument 1 to "clone_expr" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "Expr" [arg-type] posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "JoinExpr | None") [assignment] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "JoinExpr | None"; expected "Expr" [arg-type] posthog/hogql/resolver.py:0: error: Statement is unreachable [unreachable] posthog/hogql/resolver.py:0: error: Item "None" of "JoinExpr | None" has no attribute "join_type" [union-attr] posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SampleExpr | None") [assignment] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "SampleExpr | None"; expected "Expr" [arg-type] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Visitor" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "AST" [arg-type] posthog/hogql/resolver.py:0: error: Argument "select_query_type" to "SelectViewType" has incompatible type "SelectQueryType | None"; expected "SelectQueryType | SelectUnionQueryType" [arg-type] posthog/hogql/resolver.py:0: error: Item "None" of "SelectQuery | SelectUnionQuery | Field | None" has no attribute "type" [union-attr] posthog/hogql/resolver.py:0: error: Argument "select_query_type" to "SelectQueryAliasType" has incompatible type "Type | Any | None"; expected "SelectQueryType | SelectUnionQueryType" [arg-type] @@ -258,9 +200,7 @@ posthog/hogql/resolver.py:0: error: Item "None" of "SelectQuery | SelectUnionQue posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Type | Any | None", variable has type "BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType | SelectViewType | None") [assignment] posthog/hogql/resolver.py:0: error: Argument 1 to "append" of "list" has incompatible type "BaseTableType | SelectUnionQueryType | SelectQueryType | SelectQueryAliasType | SelectViewType | None"; expected "SelectQueryType | SelectUnionQueryType" [arg-type] posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "JoinExpr | None") [assignment] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "JoinExpr | None"; expected "Expr" [arg-type] posthog/hogql/resolver.py:0: error: Incompatible types in assignment (expression has type "Expr", variable has type "SampleExpr | None") [assignment] -posthog/hogql/resolver.py:0: error: Argument 1 to "visit" of "Resolver" has incompatible type "SampleExpr | None"; expected "Expr" [arg-type] posthog/hogql/resolver.py:0: error: Argument 2 to "convert_hogqlx_tag" has incompatible type "int | None"; expected "int" [arg-type] posthog/hogql/resolver.py:0: error: Statement is unreachable [unreachable] posthog/hogql/resolver.py:0: error: Item "None" of "Type | None" has no attribute "resolve_constant_type" [union-attr] @@ -309,10 +249,6 @@ posthog/hogql/printer.py:0: error: Argument 1 to "len" has incompatible type "li posthog/hogql/printer.py:0: error: Item "None" of "list[Expr] | None" has no attribute "__iter__" (not iterable) [union-attr] posthog/hogql/printer.py:0: error: Right operand of "and" is never evaluated [unreachable] posthog/hogql/printer.py:0: error: Subclass of "TableType" and "LazyTableType" cannot exist: would have incompatible method signatures [unreachable] -posthog/hogql/printer.py:0: error: Argument 1 to "visit" of "_Printer" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "AST" [arg-type] -posthog/hogql/printer.py:0: error: Argument 1 to "visit" of "_Printer" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "AST" [arg-type] -posthog/hogql/printer.py:0: error: Argument 1 to "visit" of "_Printer" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "AST" [arg-type] -posthog/hogql/printer.py:0: error: Argument 1 to "visit" of "_Printer" has incompatible type "SelectQuery | SelectUnionQuery | Field | None"; expected "AST" [arg-type] posthog/hogql/printer.py:0: error: Argument 1 to "_print_escaped_string" of "_Printer" has incompatible type "int | float | UUID | date | None"; expected "float | int | str | list[Any] | tuple[Any, ...] | datetime | date" [arg-type] posthog/hogql/printer.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[str | int]"; expected "Iterable[str]" [arg-type] posthog/hogql/printer.py:0: error: Name "args" already defined on line 0 [no-redef] @@ -625,6 +561,21 @@ posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "DateTime | Date | datetime | date | str | float | int | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Item "None" of "DateTime | None" has no attribute "int_timestamp" [union-attr] posthog/temporal/data_imports/pipelines/zendesk/helpers.py:0: error: Argument 1 to "ensure_pendulum_datetime" has incompatible type "str | None"; expected "DateTime | Date | datetime | date | str | float | int" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Not all union combinations were tried because there are too many unions [misc] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 2 to "source" has incompatible type "str | None"; expected "str" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 3 to "source" has incompatible type "str | None"; expected "str" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 4 to "source" has incompatible type "int | None"; expected "int" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 6 to "source" has incompatible type "Schema | None"; expected "Schema" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 7 to "source" has incompatible type "Literal['evolve', 'discard_value', 'freeze', 'discard_row'] | TSchemaContractDict | None"; expected "Literal['evolve', 'discard_value', 'freeze', 'discard_row'] | TSchemaContractDict" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 8 to "source" has incompatible type "type[BaseConfiguration] | None"; expected "type[BaseConfiguration]" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "build_resource_dependency_graph" has incompatible type "EndpointResourceBase | None"; expected "EndpointResourceBase" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible types in assignment (expression has type "list[str] | None", variable has type "list[str]") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "setup_incremental_object" has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "dict[str, Any]" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument "base_url" to "RESTClient" has incompatible type "str | None"; expected "str" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument 1 to "exclude_keys" has incompatible type "dict[str, ResolveParamConfig | IncrementalParamConfig | Any] | None"; expected "Mapping[str, Any]" [arg-type] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Incompatible default for argument "resolved_param" (default has type "ResolvedParam | None", argument has type "ResolvedParam") [assignment] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/data_imports/pipelines/rest_source/__init__.py:0: error: Argument "module" to "SourceInfo" has incompatible type Module | None; expected Module [arg-type] posthog/tasks/exports/test/test_csv_exporter.py:0: error: Function is missing a return type annotation [no-untyped-def] posthog/tasks/exports/test/test_csv_exporter.py:0: error: Function is missing a type annotation [no-untyped-def] posthog/tasks/exports/test/test_csv_exporter.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def] @@ -721,6 +672,13 @@ posthog/temporal/tests/batch_exports/test_run_updates.py:0: error: Unused "type: posthog/temporal/tests/batch_exports/test_run_updates.py:0: error: Unused "type: ignore" comment [unused-ignore] posthog/temporal/tests/batch_exports/test_run_updates.py:0: error: Unused "type: ignore" comment [unused-ignore] posthog/temporal/tests/batch_exports/test_batch_exports.py:0: error: TypedDict key must be a string literal; expected one of ("_timestamp", "created_at", "distinct_id", "elements", "elements_chain", ...) [literal-required] +posthog/temporal/data_imports/pipelines/stripe/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/data_imports/pipelines/stripe/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/data_imports/pipelines/stripe/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/data_imports/pipelines/stripe/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/data_imports/pipelines/stripe/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/data_imports/pipelines/stripe/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] +posthog/temporal/data_imports/pipelines/stripe/__init__.py:0: error: Unused "type: ignore" comment [unused-ignore] posthog/session_recordings/session_recording_api.py:0: error: Argument "team_id" to "get_realtime_snapshots" has incompatible type "int"; expected "str" [arg-type] posthog/queries/app_metrics/test/test_app_metrics.py:0: error: Argument 3 to "AppMetricsErrorDetailsQuery" has incompatible type "AppMetricsRequestSerializer"; expected "AppMetricsErrorsRequestSerializer" [arg-type] posthog/queries/app_metrics/test/test_app_metrics.py:0: error: Argument 3 to "AppMetricsErrorDetailsQuery" has incompatible type "AppMetricsRequestSerializer"; expected "AppMetricsErrorsRequestSerializer" [arg-type] diff --git a/plugin-server/src/cdp/cdp-processed-events-consumer.ts b/plugin-server/src/cdp/cdp-processed-events-consumer.ts new file mode 100644 index 0000000000000..b98e07ed79638 --- /dev/null +++ b/plugin-server/src/cdp/cdp-processed-events-consumer.ts @@ -0,0 +1,278 @@ +import { features, librdkafkaVersion, Message } from 'node-rdkafka' +import { Histogram } from 'prom-client' + +import { KAFKA_EVENTS_JSON, KAFKA_LOG_ENTRIES } from '../config/kafka-topics' +import { BatchConsumer, startBatchConsumer } from '../kafka/batch-consumer' +import { createRdConnectionConfigFromEnvVars, createRdProducerConfigFromEnvVars } from '../kafka/config' +import { createKafkaProducer } from '../kafka/producer' +import { addSentryBreadcrumbsEventListeners } from '../main/ingestion-queues/kafka-metrics' +import { runInstrumentedFunction } from '../main/utils' +import { GroupTypeToColumnIndex, Hub, PluginsServerConfig, RawClickHouseEvent, TeamId } from '../types' +import { KafkaProducerWrapper } from '../utils/db/kafka-producer-wrapper' +import { PostgresRouter } from '../utils/db/postgres' +import { status } from '../utils/status' +import { AppMetrics } from '../worker/ingestion/app-metrics' +import { GroupTypeManager } from '../worker/ingestion/group-type-manager' +import { OrganizationManager } from '../worker/ingestion/organization-manager' +import { TeamManager } from '../worker/ingestion/team-manager' +import { RustyHook } from '../worker/rusty-hook' +import { HogExecutor } from './hog-executor' +import { HogFunctionManager } from './hog-function-manager' +import { HogFunctionInvocationGlobals, HogFunctionInvocationResult, HogFunctionLogEntry } from './types' +import { convertToHogFunctionInvocationGlobals } from './utils' + +// Must require as `tsc` strips unused `import` statements and just requiring this seems to init some globals +require('@sentry/tracing') + +// WARNING: Do not change this - it will essentially reset the consumer +const KAFKA_CONSUMER_GROUP_ID = 'cdp-function-executor' +const BUCKETS_KB_WRITTEN = [0, 128, 512, 1024, 5120, 10240, 20480, 51200, 102400, 204800, Infinity] + +const histogramKafkaBatchSize = new Histogram({ + name: 'cdp_function_executor_batch_size', + help: 'The size of the batches we are receiving from Kafka', + buckets: [0, 50, 100, 250, 500, 750, 1000, 1500, 2000, 3000, Infinity], +}) + +const histogramKafkaBatchSizeKb = new Histogram({ + name: 'cdp_function_executor_batch_size_kb', + help: 'The size in kb of the batches we are receiving from Kafka', + buckets: BUCKETS_KB_WRITTEN, +}) + +export interface TeamIDWithConfig { + teamId: TeamId | null + consoleLogIngestionEnabled: boolean +} + +export class CdpProcessedEventsConsumer { + batchConsumer?: BatchConsumer + teamManager: TeamManager + organizationManager: OrganizationManager + groupTypeManager: GroupTypeManager + hogFunctionManager: HogFunctionManager + hogExecutor: HogExecutor + appMetrics?: AppMetrics + topic: string + consumerGroupId: string + isStopping = false + + private kafkaProducer?: KafkaProducerWrapper + + private promises: Set> = new Set() + + constructor(private config: PluginsServerConfig, private hub?: Hub) { + this.topic = KAFKA_EVENTS_JSON + this.consumerGroupId = KAFKA_CONSUMER_GROUP_ID + + const postgres = hub?.postgres ?? new PostgresRouter(config) + + this.teamManager = new TeamManager(postgres, config) + this.organizationManager = new OrganizationManager(postgres, this.teamManager) + this.groupTypeManager = new GroupTypeManager(postgres, this.teamManager) + this.hogFunctionManager = new HogFunctionManager(postgres, config) + const rustyHook = this.hub?.rustyHook ?? new RustyHook(this.config) + this.hogExecutor = new HogExecutor(this.config, this.hogFunctionManager, rustyHook) + } + + private scheduleWork(promise: Promise): Promise { + this.promises.add(promise) + void promise.finally(() => this.promises.delete(promise)) + return promise + } + + public async consume(event: HogFunctionInvocationGlobals): Promise { + return await this.hogExecutor!.executeMatchingFunctions(event) + } + + public async handleEachBatch(messages: Message[], heartbeat: () => void): Promise { + status.info('🔁', `cdp-function-executor - handling batch`, { + size: messages.length, + }) + await runInstrumentedFunction({ + statsKey: `cdpFunctionExecutor.handleEachBatch`, + sendTimeoutGuardToSentry: false, + func: async () => { + histogramKafkaBatchSize.observe(messages.length) + histogramKafkaBatchSizeKb.observe(messages.reduce((acc, m) => (m.value?.length ?? 0) + acc, 0) / 1024) + + const events: HogFunctionInvocationGlobals[] = [] + + await runInstrumentedFunction({ + statsKey: `cdpFunctionExecutor.handleEachBatch.parseKafkaMessages`, + func: async () => { + // TODO: Early exit for events without associated hooks + + await Promise.all( + messages.map(async (message) => { + try { + const clickHouseEvent = JSON.parse(message.value!.toString()) as RawClickHouseEvent + + if (!this.hogFunctionManager.teamHasHogFunctions(clickHouseEvent.team_id)) { + // No need to continue if the team doesn't have any functions + return + } + + let groupTypes: GroupTypeToColumnIndex | undefined = undefined + + if ( + await this.organizationManager.hasAvailableFeature( + clickHouseEvent.team_id, + 'group_analytics' + ) + ) { + // If the organization has group analytics enabled then we enrich the event with group data + groupTypes = await this.groupTypeManager.fetchGroupTypes( + clickHouseEvent.team_id + ) + } + + const team = await this.teamManager.fetchTeam(clickHouseEvent.team_id) + if (!team) { + return + } + events.push( + convertToHogFunctionInvocationGlobals( + clickHouseEvent, + team, + this.config.SITE_URL ?? 'http://localhost:8000', + groupTypes + ) + ) + } catch (e) { + status.error('Error parsing message', e) + } + }) + ) + }, + }) + heartbeat() + + const invocationResults: HogFunctionInvocationResult[] = [] + + if (!events.length) { + return + } + + await runInstrumentedFunction({ + statsKey: `cdpFunctionExecutor.handleEachBatch.consumeBatch`, + func: async () => { + const results = await Promise.all(events.map((e) => this.consume(e))) + invocationResults.push(...results.flat()) + }, + }) + + heartbeat() + + // TODO: Follow up - process metrics from the invocationResults + await runInstrumentedFunction({ + statsKey: `cdpFunctionExecutor.handleEachBatch.queueMetrics`, + func: async () => { + const allLogs = invocationResults.reduce((acc, result) => { + return [...acc, ...result.logs] + }, [] as HogFunctionLogEntry[]) + + await Promise.all( + allLogs.map((x) => + this.kafkaProducer!.produce({ + topic: KAFKA_LOG_ENTRIES, + value: Buffer.from(JSON.stringify(x)), + key: x.instance_id, + waitForAck: true, + }) + ) + ) + + if (allLogs.length) { + status.info('🔁', `cdp-function-executor - produced logs`, { + size: allLogs.length, + }) + } + }, + }) + }, + }) + } + + public async start(): Promise { + status.info('🔁', 'cdp-function-executor - starting', { + librdKafkaVersion: librdkafkaVersion, + kafkaCapabilities: features, + }) + + // NOTE: This is the only place where we need to use the shared server config + const globalConnectionConfig = createRdConnectionConfigFromEnvVars(this.config) + const globalProducerConfig = createRdProducerConfigFromEnvVars(this.config) + + await this.hogFunctionManager.start() + + this.kafkaProducer = new KafkaProducerWrapper( + await createKafkaProducer(globalConnectionConfig, globalProducerConfig) + ) + + this.appMetrics = + this.hub?.appMetrics ?? + new AppMetrics( + this.kafkaProducer, + this.config.APP_METRICS_FLUSH_FREQUENCY_MS, + this.config.APP_METRICS_FLUSH_MAX_QUEUE_SIZE + ) + this.kafkaProducer.producer.connect() + + this.batchConsumer = await startBatchConsumer({ + connectionConfig: createRdConnectionConfigFromEnvVars(this.config), + groupId: this.consumerGroupId, + topic: this.topic, + autoCommit: true, + sessionTimeout: this.config.KAFKA_CONSUMPTION_SESSION_TIMEOUT_MS, + maxPollIntervalMs: this.config.KAFKA_CONSUMPTION_MAX_POLL_INTERVAL_MS, + // the largest size of a message that can be fetched by the consumer. + // the largest size our MSK cluster allows is 20MB + // we only use 9 or 10MB but there's no reason to limit this 🤷️ + consumerMaxBytes: this.config.KAFKA_CONSUMPTION_MAX_BYTES, + consumerMaxBytesPerPartition: this.config.KAFKA_CONSUMPTION_MAX_BYTES_PER_PARTITION, + // our messages are very big, so we don't want to buffer too many + // queuedMinMessages: this.config.KAFKA_QUEUE_SIZE, + consumerMaxWaitMs: this.config.KAFKA_CONSUMPTION_MAX_WAIT_MS, + consumerErrorBackoffMs: this.config.KAFKA_CONSUMPTION_ERROR_BACKOFF_MS, + fetchBatchSize: this.config.INGESTION_BATCH_SIZE, + batchingTimeoutMs: this.config.KAFKA_CONSUMPTION_BATCHING_TIMEOUT_MS, + topicCreationTimeoutMs: this.config.KAFKA_TOPIC_CREATION_TIMEOUT_MS, + eachBatch: async (messages, { heartbeat }) => { + return await this.scheduleWork(this.handleEachBatch(messages, heartbeat)) + }, + callEachBatchWhenEmpty: false, + }) + + addSentryBreadcrumbsEventListeners(this.batchConsumer.consumer) + + this.batchConsumer.consumer.on('disconnected', async (err) => { + // since we can't be guaranteed that the consumer will be stopped before some other code calls disconnect + // we need to listen to disconnect and make sure we're stopped + status.info('🔁', 'cdp-function-executor batch consumer disconnected, cleaning up', { err }) + await this.stop() + }) + } + + public async stop(): Promise[]> { + status.info('🔁', 'cdp-function-executor - stopping') + this.isStopping = true + + // Mark as stopping so that we don't actually process any more incoming messages, but still keep the process alive + await this.batchConsumer?.stop() + + const promiseResults = await Promise.allSettled(this.promises) + + await this.kafkaProducer?.disconnect() + await this.hogFunctionManager.stop() + + status.info('👍', 'cdp-function-executor - stopped!') + + return promiseResults + } + + public isHealthy() { + // TODO: Maybe extend this to check if we are shutting down so we don't get killed early. + return this.batchConsumer?.isHealthy() + } +} diff --git a/plugin-server/src/cdp/hog-executor.ts b/plugin-server/src/cdp/hog-executor.ts new file mode 100644 index 0000000000000..29f7265e09907 --- /dev/null +++ b/plugin-server/src/cdp/hog-executor.ts @@ -0,0 +1,325 @@ +import { convertHogToJS, convertJSToHog, exec, ExecResult, VMState } from '@posthog/hogvm' +import { Webhook } from '@posthog/plugin-scaffold' +import { DateTime } from 'luxon' + +import { PluginsServerConfig, TimestampFormat } from '../types' +import { trackedFetch } from '../utils/fetch' +import { status } from '../utils/status' +import { castTimestampOrNow, UUIDT } from '../utils/utils' +import { RustyHook } from '../worker/rusty-hook' +import { HogFunctionManager } from './hog-function-manager' +import { + HogFunctionInvocation, + HogFunctionInvocationAsyncResponse, + HogFunctionInvocationGlobals, + HogFunctionInvocationResult, + HogFunctionLogEntry, + HogFunctionLogEntryLevel, + HogFunctionType, +} from './types' +import { convertToHogFunctionFilterGlobal } from './utils' + +export const formatInput = (bytecode: any, globals: HogFunctionInvocation['globals']): any => { + // Similar to how we generate the bytecode by iterating over the values, + // here we iterate over the object and replace the bytecode with the actual values + // bytecode is indicated as an array beginning with ["_h"] + + if (Array.isArray(bytecode) && bytecode[0] === '_h') { + const res = exec(bytecode, { + globals, + timeout: 100, + maxAsyncSteps: 0, + }) + + if (!res.finished) { + // NOT ALLOWED + throw new Error('Input fields must be simple sync values') + } + return convertHogToJS(res.result) + } + + if (Array.isArray(bytecode)) { + return bytecode.map((item) => formatInput(item, globals)) + } else if (typeof bytecode === 'object') { + return Object.fromEntries(Object.entries(bytecode).map(([key, value]) => [key, formatInput(value, globals)])) + } else { + return bytecode + } +} + +export class HogExecutor { + constructor( + private serverConfig: PluginsServerConfig, + private hogFunctionManager: HogFunctionManager, + private rustyHook: RustyHook + ) {} + + /** + * Intended to be invoked as a starting point from an event + */ + async executeMatchingFunctions(event: HogFunctionInvocationGlobals): Promise { + const allFunctionsForTeam = this.hogFunctionManager.getTeamHogFunctions(event.project.id) + + const filtersGlobals = convertToHogFunctionFilterGlobal(event) + + // Filter all functions based on the invocation + const functions = Object.fromEntries( + Object.entries(allFunctionsForTeam).filter(([_key, value]) => { + try { + const filters = value.filters + + if (!filters?.bytecode) { + // NOTE: If we don't have bytecode this indicates something went wrong. + // The model will always save a bytecode if it was compiled correctly + return false + } + + const filterResult = exec(filters.bytecode, { + globals: filtersGlobals, + timeout: 100, + maxAsyncSteps: 0, + }) + + if (typeof filterResult.result !== 'boolean') { + // NOTE: If the result is not a boolean we should not execute the function + return false + } + + return filterResult.result + } catch (error) { + status.error('🦔', `[HogExecutor] Error filtering function`, { + hogFunctionId: value.id, + hogFunctionName: value.name, + error: error.message, + }) + } + + return false + }) + ) + + if (!Object.keys(functions).length) { + return [] + } + + status.info( + '🦔', + `[HogExecutor] Found ${Object.keys(functions).length} matching functions out of ${ + Object.keys(allFunctionsForTeam).length + } for team` + ) + + const results: HogFunctionInvocationResult[] = [] + + for (const hogFunction of Object.values(functions)) { + // Add the source of the trigger to the globals + const modifiedGlobals: HogFunctionInvocationGlobals = { + ...event, + source: { + name: hogFunction.name ?? `Hog function: ${hogFunction.id}`, + url: `${event.project.url}/pipeline/destinations/hog-${hogFunction.id}/configuration/`, + }, + } + + const result = await this.execute(hogFunction, { + id: new UUIDT().toString(), + globals: modifiedGlobals, + }) + + results.push(result) + } + + return results + } + + /** + * Intended to be invoked as a continuation from an async function + */ + async executeAsyncResponse(invocation: HogFunctionInvocationAsyncResponse): Promise { + if (!invocation.hogFunctionId) { + throw new Error('No hog function id provided') + } + + const hogFunction = this.hogFunctionManager.getTeamHogFunctions(invocation.globals.project.id)[ + invocation.hogFunctionId + ] + + invocation.vmState.stack.push(convertJSToHog(invocation.response)) + + await this.execute(hogFunction, invocation, invocation.vmState) + } + + async execute( + hogFunction: HogFunctionType, + invocation: HogFunctionInvocation, + state?: VMState + ): Promise { + const loggingContext = { + hogFunctionId: hogFunction.id, + hogFunctionName: hogFunction.name, + hogFunctionUrl: invocation.globals.source?.url, + } + + status.info('🦔', `[HogExecutor] Executing function`, loggingContext) + + let error: any = null + const logs: HogFunctionLogEntry[] = [] + let lastTimestamp = DateTime.now() + + const log = (level: HogFunctionLogEntryLevel, message: string) => { + // TRICKY: The log entries table is de-duped by timestamp, so we need to ensure that the timestamps are unique + // It is unclear how this affects parallel execution environments + let now = DateTime.now() + if (now <= lastTimestamp) { + // Ensure that the timestamps are unique + now = lastTimestamp.plus(1) + } + lastTimestamp = now + + logs.push({ + team_id: hogFunction.team_id, + log_source: 'hog_function', + log_source_id: hogFunction.id, + instance_id: invocation.id, + timestamp: castTimestampOrNow(now, TimestampFormat.ClickHouse), + level, + message, + }) + } + + if (!state) { + log('debug', `Executing function`) + } else { + log('debug', `Resuming function`) + } + + try { + const globals = this.buildHogFunctionGlobals(hogFunction, invocation) + + const res = exec(state ?? hogFunction.bytecode, { + globals, + timeout: 100, // NOTE: This will likely be configurable in the future + maxAsyncSteps: 5, // NOTE: This will likely be configurable in the future + asyncFunctions: { + // We need to pass these in but they don't actually do anything as it is a sync exec + fetch: async () => Promise.resolve(), + }, + functions: { + print: (...args) => { + const message = args + .map((arg) => (typeof arg !== 'string' ? JSON.stringify(arg) : arg)) + .join(', ') + log('info', message) + }, + }, + }) + + if (!res.finished) { + log('debug', `Suspending function due to async function call '${res.asyncFunctionName}'`) + status.info('🦔', `[HogExecutor] Function returned not finished. Executing async function`, { + ...loggingContext, + asyncFunctionName: res.asyncFunctionName, + }) + switch (res.asyncFunctionName) { + case 'fetch': + await this.asyncFunctionFetch(hogFunction, invocation, res) + break + default: + status.error( + '🦔', + `[HogExecutor] Unknown async function: ${res.asyncFunctionName}`, + loggingContext + ) + // TODO: Log error somewhere + } + } else { + log('debug', `Function completed (${hogFunction.id}) (${hogFunction.name})!`) + } + } catch (err) { + error = err + status.error('🦔', `[HogExecutor] Error executing function ${hogFunction.id} - ${hogFunction.name}`, error) + } + + return { + ...invocation, + success: !error, + error, + logs, + } + } + + buildHogFunctionGlobals(hogFunction: HogFunctionType, invocation: HogFunctionInvocation): Record { + const builtInputs: Record = {} + + Object.entries(hogFunction.inputs).forEach(([key, item]) => { + // TODO: Replace this with iterator + builtInputs[key] = item.value + + if (item.bytecode) { + // Use the bytecode to compile the field + builtInputs[key] = formatInput(item.bytecode, invocation.globals) + } + }) + + return { + ...invocation.globals, + inputs: builtInputs, + } + } + + private async asyncFunctionFetch( + hogFunction: HogFunctionType, + invocation: HogFunctionInvocation, + execResult: ExecResult + ): Promise { + // TODO: validate the args + const args = (execResult.asyncFunctionArgs ?? []).map((arg) => convertHogToJS(arg)) + const url: string = args[0] + const options = args[1] + + const method = options.method || 'POST' + const headers = options.headers || { + 'Content-Type': 'application/json', + } + const body = options.body || {} + + const webhook: Webhook = { + url, + method: method, + headers: headers, + body: typeof body === 'string' ? body : JSON.stringify(body, undefined, 4), + } + + // NOTE: Purposefully disabled for now - once we have callback support we can re-enable + // const SPECIAL_CONFIG_ID = -3 // Hardcoded to mean Hog + // const success = await this.rustyHook.enqueueIfEnabledForTeam({ + // webhook: webhook, + // teamId: hogFunction.team_id, + // pluginId: SPECIAL_CONFIG_ID, + // pluginConfigId: SPECIAL_CONFIG_ID, + // }) + + const success = false + + // TODO: Temporary test code + if (!success) { + status.info('🦔', `[HogExecutor] Webhook not sent via rustyhook, sending directly instead`) + const fetchResponse = await trackedFetch(url, { + method: webhook.method, + body: webhook.body, + headers: webhook.headers, + timeout: this.serverConfig.EXTERNAL_REQUEST_TIMEOUT_MS, + }) + + await this.executeAsyncResponse({ + ...invocation, + hogFunctionId: hogFunction.id, + vmState: execResult.state!, + response: { + status: fetchResponse.status, + body: await fetchResponse.text(), + }, + }) + } + } +} diff --git a/plugin-server/src/cdp/hog-function-manager.ts b/plugin-server/src/cdp/hog-function-manager.ts new file mode 100644 index 0000000000000..52f349b1fcbdb --- /dev/null +++ b/plugin-server/src/cdp/hog-function-manager.ts @@ -0,0 +1,132 @@ +import * as schedule from 'node-schedule' + +import { PluginsServerConfig, Team } from '../types' +import { PostgresRouter, PostgresUse } from '../utils/db/postgres' +import { PubSub } from '../utils/pubsub' +import { status } from '../utils/status' +import { HogFunctionType } from './types' + +export type HogFunctionMap = Record +export type HogFunctionCache = Record + +export class HogFunctionManager { + private started: boolean + private ready: boolean + private cache: HogFunctionCache + private pubSub: PubSub + private refreshJob?: schedule.Job + + constructor(private postgres: PostgresRouter, private serverConfig: PluginsServerConfig) { + this.started = false + this.ready = false + this.cache = {} + + this.pubSub = new PubSub(this.serverConfig, { + 'reload-hog-function': async (message) => { + const { hogFunctionId, teamId } = JSON.parse(message) + await this.reloadHogFunction(teamId, hogFunctionId) + }, + }) + } + + public async start(): Promise { + // TRICKY - when running with individual capabilities, this won't run twice but locally or as a complete service it will... + if (this.started) { + return + } + this.started = true + await this.pubSub.start() + await this.reloadAllHogFunctions() + + // every 5 minutes all HogFunctionManager caches are reloaded for eventual consistency + this.refreshJob = schedule.scheduleJob('*/5 * * * *', async () => { + await this.reloadAllHogFunctions().catch((error) => { + status.error('🍿', 'Error reloading hog functions:', error) + }) + }) + this.ready = true + } + + public async stop(): Promise { + if (this.refreshJob) { + schedule.cancelJob(this.refreshJob) + } + + await this.pubSub.stop() + } + + public getTeamHogFunctions(teamId: Team['id']): HogFunctionMap { + if (!this.ready) { + throw new Error('HogFunctionManager is not ready! Run HogFunctionManager.start() before this') + } + return this.cache[teamId] || {} + } + + public teamHasHogFunctions(teamId: Team['id']): boolean { + return !!Object.keys(this.getTeamHogFunctions(teamId)).length + } + + public async reloadAllHogFunctions(): Promise { + this.cache = await fetchAllHogFunctionsGroupedByTeam(this.postgres) + status.info('🍿', 'Fetched all hog functions from DB anew') + } + + public async reloadHogFunction(teamId: Team['id'], id: HogFunctionType['id']): Promise { + status.info('🍿', `Reloading hog function ${id} from DB`) + const item = await fetchHogFunction(this.postgres, id) + if (item) { + this.cache[teamId][id] = item + } else { + delete this.cache[teamId][id] + } + } +} + +const HOG_FUNCTION_FIELDS = ['id', 'team_id', 'name', 'enabled', 'inputs', 'filters', 'bytecode'] + +export async function fetchAllHogFunctionsGroupedByTeam(client: PostgresRouter): Promise { + const items = ( + await client.query( + PostgresUse.COMMON_READ, + ` + SELECT ${HOG_FUNCTION_FIELDS.join(', ')} + FROM posthog_hogfunction + WHERE deleted = FALSE AND enabled = TRUE + `, + [], + 'fetchAllHogFunctions' + ) + ).rows + + const cache: HogFunctionCache = {} + for (const item of items) { + if (!cache[item.team_id]) { + cache[item.team_id] = {} + } + + cache[item.team_id][item.id] = item + } + + return cache +} + +export async function fetchHogFunction( + client: PostgresRouter, + id: HogFunctionType['id'] +): Promise { + const items: HogFunctionType[] = ( + await client.query( + PostgresUse.COMMON_READ, + `SELECT ${HOG_FUNCTION_FIELDS.join(', ')} + FROM posthog_hogfunction + WHERE id = $1 AND deleted = FALSE AND enabled = TRUE`, + [id], + 'fetchHogFunction' + ) + ).rows + if (!items.length) { + return null + } + + return items[0] +} diff --git a/plugin-server/src/cdp/types.ts b/plugin-server/src/cdp/types.ts new file mode 100644 index 0000000000000..65fce9d837d61 --- /dev/null +++ b/plugin-server/src/cdp/types.ts @@ -0,0 +1,161 @@ +import { VMState } from '@posthog/hogvm' + +import { ElementPropertyFilter, EventPropertyFilter, PersonPropertyFilter } from '../types' + +export type HogBytecode = any[] + +// subset of EntityFilter +export interface HogFunctionFilterBase { + id: string + name: string | null + order: number + properties: (EventPropertyFilter | PersonPropertyFilter | ElementPropertyFilter)[] +} + +export interface HogFunctionFilterEvent extends HogFunctionFilterBase { + type: 'events' + bytecode: HogBytecode +} + +export interface HogFunctionFilterAction extends HogFunctionFilterBase { + type: 'actions' + // Loaded at run time from Action model + bytecode?: HogBytecode +} + +export type HogFunctionFilter = HogFunctionFilterEvent | HogFunctionFilterAction + +export interface HogFunctionFilters { + events?: HogFunctionFilterEvent[] + actions?: HogFunctionFilterAction[] + filter_test_accounts?: boolean + // Loaded at run time from Team model + filter_test_accounts_bytecode?: boolean + bytecode?: HogBytecode +} + +export type HogFunctionInvocationGlobals = { + project: { + id: number + name: string + url: string + } + source?: { + name: string + url: string + } + event: { + uuid: string + name: string + distinct_id: string + properties: Record + timestamp: string + url: string + } + person?: { + uuid: string + properties: Record + url: string + } + groups?: Record< + string, + { + id: string // the "key" of the group + type: string + index: number + url: string + properties: Record + } + > +} + +export type HogFunctionFilterGlobals = { + // Filter Hog is built in the same way as analytics so the global object is meant to be an event + event: string + timestamp: string + elements_chain: string + properties: Record + + person?: { + properties: Record + } + + group_0?: { + properties: Record + } + group_1?: { + properties: Record + } + group_2?: { + properties: Record + } + group_3?: { + properties: Record + } + group_4?: { + properties: Record + } +} + +export type HogFunctionLogEntrySource = 'system' | 'hog' | 'console' +export type HogFunctionLogEntryLevel = 'debug' | 'info' | 'warn' | 'error' + +export interface HogFunctionLogEntry { + team_id: number + log_source: string // The kind of source (hog_function) + log_source_id: string // The id of the hog function + instance_id: string // The id of the specific invocation + timestamp: string + level: HogFunctionLogEntryLevel + message: string +} + +export type HogFunctionInvocation = { + id: string + globals: HogFunctionInvocationGlobals +} + +export type HogFunctionInvocationResult = HogFunctionInvocation & { + success: boolean + error?: any + logs: HogFunctionLogEntry[] +} + +export type HogFunctionInvocationAsyncRequest = HogFunctionInvocation & { + hogFunctionId: HogFunctionType['id'] + vmState: VMState +} + +export type HogFunctionInvocationAsyncResponse = HogFunctionInvocationAsyncRequest & { + response: any +} + +// Mostly copied from frontend types +export type HogFunctionInputSchemaType = { + type: 'string' | 'number' | 'boolean' | 'dictionary' | 'choice' | 'json' + key: string + label?: string + choices?: { value: string; label: string }[] + required?: boolean + default?: any + secret?: boolean + description?: string +} + +export type HogFunctionType = { + id: string + team_id: number + name: string + enabled: boolean + hog: string + bytecode: HogBytecode + inputs_schema: HogFunctionInputSchemaType[] + inputs: Record< + string, + { + value: any + bytecode?: HogBytecode | object + } + > + filters?: HogFunctionFilters | null +} diff --git a/plugin-server/src/cdp/utils.ts b/plugin-server/src/cdp/utils.ts new file mode 100644 index 0000000000000..82f2739944dc8 --- /dev/null +++ b/plugin-server/src/cdp/utils.ts @@ -0,0 +1,93 @@ +// NOTE: PostIngestionEvent is our context event - it should never be sent directly to an output, but rather transformed into a lightweight schema + +import { GroupTypeToColumnIndex, RawClickHouseEvent, Team } from '../types' +import { clickHouseTimestampToISO } from '../utils/utils' +import { HogFunctionFilterGlobals, HogFunctionInvocationGlobals } from './types' + +// that we can keep to as a contract +export function convertToHogFunctionInvocationGlobals( + event: RawClickHouseEvent, + team: Team, + siteUrl: string, + groupTypes?: GroupTypeToColumnIndex +): HogFunctionInvocationGlobals { + const projectUrl = `${siteUrl}/project/${team.id}` + + const properties = event.properties ? JSON.parse(event.properties) : {} + if (event.elements_chain) { + properties['$elements_chain'] = event.elements_chain + } + + let groups: HogFunctionInvocationGlobals['groups'] = undefined + + if (groupTypes) { + groups = {} + + for (const [groupType, columnIndex] of Object.entries(groupTypes)) { + const groupKey = (properties[`$groups`] || {})[groupType] + const groupProperties = event[`group${columnIndex}_properties`] + + // TODO: Check that groupProperties always exist if the event is in that group + if (groupKey && groupProperties) { + const properties = JSON.parse(groupProperties) + + groups[groupType] = { + id: groupKey, + index: columnIndex, + type: groupType, + url: `${projectUrl}/groups/${columnIndex}/${encodeURIComponent(groupKey)}`, + properties, + } + } + } + } + const context: HogFunctionInvocationGlobals = { + project: { + id: team.id, + name: team.name, + url: projectUrl, + }, + event: { + // TODO: Element chain! + uuid: event.uuid, + name: event.event!, + distinct_id: event.distinct_id, + properties, + timestamp: clickHouseTimestampToISO(event.timestamp), + // TODO: generate url + url: `${projectUrl}/events/${encodeURIComponent(event.uuid)}/${encodeURIComponent( + clickHouseTimestampToISO(event.timestamp) + )}`, + }, + person: event.person_id + ? { + uuid: event.person_id, + properties: event.person_properties ? JSON.parse(event.person_properties) : {}, + // TODO: IS this distinct_id or person_id? + url: `${projectUrl}/person/${encodeURIComponent(event.distinct_id)}`, + } + : undefined, + groups, + } + + return context +} + +export function convertToHogFunctionFilterGlobal(globals: HogFunctionInvocationGlobals): HogFunctionFilterGlobals { + const groups: Record = {} + + for (const [_groupType, group] of Object.entries(globals.groups || {})) { + groups[`group_${group.index}`] = { + properties: group.properties, + } + } + + return { + event: globals.event.name, + elements_chain: globals.event.properties['$elements_chain'], + timestamp: globals.event.timestamp, + properties: globals.event.properties, + person: globals.person ? { properties: globals.person.properties } : undefined, + ...groups, + } +} diff --git a/plugin-server/src/utils/db/db.ts b/plugin-server/src/utils/db/db.ts index a7cd6d0b23dd9..de57b602725cc 100644 --- a/plugin-server/src/utils/db/db.ts +++ b/plugin-server/src/utils/db/db.ts @@ -717,6 +717,12 @@ export class DB { update: Partial, tx?: TransactionClient ): Promise<[InternalPerson, ProducerRecord[]]> { + let versionString = 'COALESCE(version, 0)::numeric + 1' + if (update.version) { + versionString = update.version.toString() + delete update['version'] + } + const updateValues = Object.values(unparsePersonPartial(update)) // short circuit if there are no updates to be made @@ -727,11 +733,9 @@ export class DB { const values = [...updateValues, person.id].map(sanitizeJsonbValue) // Potentially overriding values badly if there was an update to the person after computing updateValues above - const queryString = `UPDATE posthog_person SET version = COALESCE(version, 0)::numeric + 1, ${Object.keys( - update - ).map((field, index) => `"${sanitizeSqlIdentifier(field)}" = $${index + 1}`)} WHERE id = $${ - Object.values(update).length + 1 - } + const queryString = `UPDATE posthog_person SET version = ${versionString}, ${Object.keys(update).map( + (field, index) => `"${sanitizeSqlIdentifier(field)}" = $${index + 1}` + )} WHERE id = $${Object.values(update).length + 1} RETURNING *` const { rows } = await this.postgres.query( diff --git a/plugin-server/src/utils/event.ts b/plugin-server/src/utils/event.ts index e49f8c0b1519e..59ccbc6707d2e 100644 --- a/plugin-server/src/utils/event.ts +++ b/plugin-server/src/utils/event.ts @@ -10,6 +10,7 @@ import { PostIngestionEvent, RawClickHouseEvent, } from '../types' +import { status } from '../utils/status' import { chainToElements } from './db/elements-chain' import { personInitialAndUTMProperties, sanitizeString } from './db/utils' import { @@ -249,13 +250,19 @@ export function formPipelineEvent(message: Message): PipelineEvent { // Track $set usage in events that aren't known to use it, before ingestion adds anything there if ( combinedEvent.properties && - !(combinedEvent.event in PERSON_EVENTS) && - !(combinedEvent.event in KNOWN_SET_EVENTS) && + !PERSON_EVENTS.has(combinedEvent.event) && + !KNOWN_SET_EVENTS.has(combinedEvent.event) && ('$set' in combinedEvent.properties || '$set_once' in combinedEvent.properties || '$unset' in combinedEvent.properties) ) { setUsageInNonPersonEventsCounter.inc() + if (Math.random() < 0.001) { + status.info('👀', 'Found $set usage in non-person event', { + event: combinedEvent.event, + team_id: combinedEvent.team_id, + }) + } } const event: PipelineEvent = normalizeEvent({ diff --git a/plugin-server/src/worker/ingestion/person-state.ts b/plugin-server/src/worker/ingestion/person-state.ts index d3bf32e21310b..24e279fd981da 100644 --- a/plugin-server/src/worker/ingestion/person-state.ts +++ b/plugin-server/src/worker/ingestion/person-state.ts @@ -545,6 +545,23 @@ export class PersonState { created_at: createdAt, properties: properties, is_identified: true, + + // By using the max version between the two Persons, we ensure that if + // this Person is later split, we can use `this_person.version + 1` for + // any split-off Persons and know that *that* version will be higher than + // any previously deleted Person, and so the new Person row will "win" and + // "undelete" the Person. + // + // For example: + // - Merge Person_1(version:7) into Person_2(version:2) + // - Person_1 is deleted + // - Person_2 attains version 8 via this code below + // - Person_2 is later split, which attempts to re-create Person_1 by using + // its `distinct_id` to generate the deterministic Person UUID. + // That new Person_1 will have a version _at least_ as high as 8, and + // so any previously existing rows in CH or otherwise from + // Person_1(version:7) will "lose" to this new Person_1. + version: Math.max(mergeInto.version, otherPerson.version) + 1, }, tx ) diff --git a/plugin-server/tests/cdp/cdp-processed-events-consumer.test.ts b/plugin-server/tests/cdp/cdp-processed-events-consumer.test.ts new file mode 100644 index 0000000000000..dd93f1521b0c9 --- /dev/null +++ b/plugin-server/tests/cdp/cdp-processed-events-consumer.test.ts @@ -0,0 +1,220 @@ +import { CdpProcessedEventsConsumer } from '../../src/cdp/cdp-processed-events-consumer' +import { HogFunctionType } from '../../src/cdp/types' +import { defaultConfig } from '../../src/config/config' +import { Hub, PluginsServerConfig, Team } from '../../src/types' +import { createHub } from '../../src/utils/db/hub' +import { getFirstTeam, resetTestDatabase } from '../helpers/sql' +import { HOG_EXAMPLES, HOG_FILTERS_EXAMPLES, HOG_INPUTS_EXAMPLES } from './examples' +import { createIncomingEvent, createMessage, insertHogFunction as _insertHogFunction } from './fixtures' + +const config: PluginsServerConfig = { + ...defaultConfig, +} + +const mockConsumer = { + on: jest.fn(), + commitSync: jest.fn(), + commit: jest.fn(), + queryWatermarkOffsets: jest.fn(), + committed: jest.fn(), + assignments: jest.fn(), + isConnected: jest.fn(() => true), + getMetadata: jest.fn(), +} + +jest.mock('../../src/kafka/batch-consumer', () => { + return { + startBatchConsumer: jest.fn(() => + Promise.resolve({ + join: () => ({ + finally: jest.fn(), + }), + stop: jest.fn(), + consumer: mockConsumer, + }) + ), + } +}) + +jest.mock('../../src/utils/fetch', () => { + return { + trackedFetch: jest.fn(() => Promise.resolve({ status: 200, text: () => Promise.resolve({}) })), + } +}) + +jest.mock('../../src/utils/db/kafka-producer-wrapper', () => { + const mockKafkaProducer = { + producer: { + connect: jest.fn(), + }, + disconnect: jest.fn(), + produce: jest.fn(), + } + return { + KafkaProducerWrapper: jest.fn(() => mockKafkaProducer), + } +}) + +const mockFetch: jest.Mock = require('../../src/utils/fetch').trackedFetch + +const mockProducer = require('../../src/utils/db/kafka-producer-wrapper').KafkaProducerWrapper() + +jest.setTimeout(1000) + +const noop = () => {} + +const decodeKafkaMessage = (message: any): any => { + return { + ...message, + value: JSON.parse(message.value.toString()), + } +} + +describe('CDP Processed Events Consuner', () => { + let processor: CdpProcessedEventsConsumer + let hub: Hub + let closeHub: () => Promise + let team: Team + + const insertHogFunction = async (hogFunction: Partial) => { + const item = await _insertHogFunction(hub.postgres, team, hogFunction) + // Trigger the reload that django would do + await processor.hogFunctionManager.reloadAllHogFunctions() + return item + } + + beforeEach(async () => { + await resetTestDatabase() + ;[hub, closeHub] = await createHub() + team = await getFirstTeam(hub) + + processor = new CdpProcessedEventsConsumer(config, hub) + await processor.start() + + mockFetch.mockClear() + }) + + afterEach(async () => { + jest.setTimeout(10000) + await processor.stop() + await closeHub() + }) + + afterAll(() => { + jest.useRealTimers() + }) + + describe('general event processing', () => { + /** + * Tests here are somewhat expensive so should mostly simulate happy paths and the more e2e scenarios + */ + it('can parse incoming messages correctly', async () => { + await insertHogFunction({ + ...HOG_EXAMPLES.simple_fetch, + ...HOG_INPUTS_EXAMPLES.simple_fetch, + ...HOG_FILTERS_EXAMPLES.no_filters, + }) + // Create a message that should be processed by this function + // Run the function and check that it was executed + await processor.handleEachBatch( + [ + createMessage( + createIncomingEvent(team.id, { + uuid: 'b3a1fe86-b10c-43cc-acaf-d208977608d0', + event: '$pageview', + properties: JSON.stringify({ + $lib_version: '1.0.0', + }), + }) + ), + ], + noop + ) + + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockFetch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "https://example.com/posthog-webhook", + Object { + "body": "{ + \\"event\\": { + \\"uuid\\": \\"b3a1fe86-b10c-43cc-acaf-d208977608d0\\", + \\"name\\": \\"$pageview\\", + \\"distinct_id\\": \\"distinct_id_1\\", + \\"properties\\": { + \\"$lib_version\\": \\"1.0.0\\", + \\"$elements_chain\\": \\"[]\\" + }, + \\"timestamp\\": null, + \\"url\\": \\"http://localhost:8000/project/2/events/b3a1fe86-b10c-43cc-acaf-d208977608d0/null\\" + }, + \\"groups\\": null, + \\"nested\\": { + \\"foo\\": \\"http://localhost:8000/project/2/events/b3a1fe86-b10c-43cc-acaf-d208977608d0/null\\" + }, + \\"person\\": null, + \\"event_url\\": \\"http://localhost:8000/project/2/events/b3a1fe86-b10c-43cc-acaf-d208977608d0/null-test\\" + }", + "headers": Object { + "version": "v=1.0.0", + }, + "method": "POST", + "timeout": 10000, + }, + ] + `) + }) + + it('generates logs and produces them to kafka', async () => { + await insertHogFunction({ + ...HOG_EXAMPLES.simple_fetch, + ...HOG_INPUTS_EXAMPLES.simple_fetch, + ...HOG_FILTERS_EXAMPLES.no_filters, + }) + + // Create a message that should be processed by this function + // Run the function and check that it was executed + await processor.handleEachBatch( + [ + createMessage( + createIncomingEvent(team.id, { + uuid: 'b3a1fe86-b10c-43cc-acaf-d208977608d0', + event: '$pageview', + properties: JSON.stringify({ + $lib_version: '1.0.0', + }), + }) + ), + ], + noop + ) + + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockProducer.produce).toHaveBeenCalledTimes(2) + + expect(decodeKafkaMessage(mockProducer.produce.mock.calls[0][0])).toMatchObject({ + key: expect.any(String), + topic: 'log_entries_test', + value: { + instance_id: expect.any(String), + level: 'debug', + log_source: 'hog_function', + log_source_id: expect.any(String), + message: 'Executing function', + team_id: 2, + timestamp: expect.any(String), + }, + waitForAck: true, + }) + + expect(decodeKafkaMessage(mockProducer.produce.mock.calls[1][0])).toMatchObject({ + topic: 'log_entries_test', + value: { + log_source: 'hog_function', + message: "Suspending function due to async function call 'fetch'", + team_id: 2, + }, + }) + }) + }) +}) diff --git a/plugin-server/tests/cdp/examples.ts b/plugin-server/tests/cdp/examples.ts new file mode 100644 index 0000000000000..9215d84c8026b --- /dev/null +++ b/plugin-server/tests/cdp/examples.ts @@ -0,0 +1,161 @@ +import { HogFunctionType } from '../../src/cdp/types' + +/** + * Hog functions are largely generated and built in the django service, making it tricky to test on this side. + * As such we have a bunch of prebuilt examples here for usage in tests. + */ +export const HOG_EXAMPLES: Record> = { + simple_fetch: { + hog: "fetch(inputs.url, {\n 'headers': inputs.headers,\n 'body': inputs.payload,\n 'method': inputs.method,\n 'payload': inputs.payload\n});", + bytecode: [ + '_h', + 32, + 'headers', + 32, + 'headers', + 32, + 'inputs', + 1, + 2, + 32, + 'body', + 32, + 'payload', + 32, + 'inputs', + 1, + 2, + 32, + 'method', + 32, + 'method', + 32, + 'inputs', + 1, + 2, + 32, + 'payload', + 32, + 'payload', + 32, + 'inputs', + 1, + 2, + 42, + 4, + 32, + 'url', + 32, + 'inputs', + 1, + 2, + 2, + 'fetch', + 2, + 35, + ], + }, +} + +export const HOG_INPUTS_EXAMPLES: Record> = { + simple_fetch: { + inputs_schema: [ + { key: 'url', type: 'string', label: 'Webhook URL', secret: false, required: true }, + { key: 'payload', type: 'json', label: 'JSON Payload', secret: false, required: true }, + { + key: 'method', + type: 'choice', + label: 'HTTP Method', + secret: false, + choices: [ + { label: 'POST', value: 'POST' }, + { label: 'PUT', value: 'PUT' }, + { label: 'PATCH', value: 'PATCH' }, + { label: 'GET', value: 'GET' }, + ], + required: true, + }, + { key: 'headers', type: 'dictionary', label: 'Headers', secret: false, required: false }, + ], + inputs: { + url: { + value: 'https://example.com/posthog-webhook', + bytecode: ['_h', 32, 'https://example.com/posthog-webhook'], + }, + method: { value: 'POST' }, + headers: { + value: { version: 'v={event.properties.$lib_version}' }, + bytecode: { + version: ['_h', 32, '$lib_version', 32, 'properties', 32, 'event', 1, 3, 32, 'v=', 2, 'concat', 2], + }, + }, + payload: { + value: { + event: '{event}', + groups: '{groups}', + nested: { foo: '{event.url}' }, + person: '{person}', + event_url: "{f'{event.url}-test'}", + }, + bytecode: { + event: ['_h', 32, 'event', 1, 1], + groups: ['_h', 32, 'groups', 1, 1], + nested: { foo: ['_h', 32, 'url', 32, 'event', 1, 2] }, + person: ['_h', 32, 'person', 1, 1], + event_url: ['_h', 32, '-test', 32, 'url', 32, 'event', 1, 2, 2, 'concat', 2], + }, + }, + }, + }, +} + +export const HOG_FILTERS_EXAMPLES: Record> = { + no_filters: { filters: { events: [], actions: [], bytecode: ['_h', 29] } }, + pageview_or_autocapture_filter: { + filters: { + events: [ + { + id: '$pageview', + name: '$pageview', + type: 'events', + order: 0, + properties: [{ key: '$current_url', type: 'event', value: 'posthog', operator: 'icontains' }], + }, + { id: '$autocapture', name: '$autocapture', type: 'events', order: 1 }, + ], + actions: [], + bytecode: [ + '_h', + 32, + '$autocapture', + 32, + 'event', + 1, + 1, + 11, + 3, + 1, + 32, + '%posthog%', + 32, + '$current_url', + 32, + 'properties', + 1, + 2, + 18, + 32, + '$pageview', + 32, + 'event', + 1, + 1, + 11, + 3, + 2, + 4, + 2, + ], + }, + }, +} diff --git a/plugin-server/tests/cdp/fixtures.ts b/plugin-server/tests/cdp/fixtures.ts new file mode 100644 index 0000000000000..8e6d836756cb5 --- /dev/null +++ b/plugin-server/tests/cdp/fixtures.ts @@ -0,0 +1,93 @@ +import { randomUUID } from 'crypto' +import { Message } from 'node-rdkafka' + +import { HogFunctionInvocationGlobals, HogFunctionType } from '../../src/cdp/types' +import { ClickHouseTimestamp, RawClickHouseEvent, Team } from '../../src/types' +import { PostgresRouter } from '../../src/utils/db/postgres' +import { insertRow } from '../helpers/sql' + +export const createHogFunction = (hogFunction: Partial) => { + const item: HogFunctionType = { + id: randomUUID(), + team_id: 1, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + created_by_id: 1001, + enabled: true, + deleted: false, + description: '', + hog: '', + ...hogFunction, + } + + return item +} + +export const createIncomingEvent = (teamId: number, data: Partial): RawClickHouseEvent => { + return { + team_id: teamId, + created_at: new Date().toISOString() as ClickHouseTimestamp, + elements_chain: '[]', + person_created_at: new Date().toISOString() as ClickHouseTimestamp, + person_properties: '{}', + distinct_id: 'distinct_id_1', + uuid: randomUUID(), + event: '$pageview', + timestamp: new Date().toISOString() as ClickHouseTimestamp, + properties: '{}', + ...data, + } +} + +export const createMessage = (event: RawClickHouseEvent, overrides: Partial = {}): Message => { + return { + partition: 1, + topic: 'test', + offset: 0, + timestamp: overrides.timestamp ?? Date.now(), + size: 1, + ...overrides, + value: Buffer.from(JSON.stringify(event)), + } +} + +export const insertHogFunction = async ( + postgres: PostgresRouter, + team: Team, + hogFunction: Partial = {} +) => { + const res = await insertRow( + postgres, + 'posthog_hogfunction', + createHogFunction({ + team_id: team.id, + ...hogFunction, + }) + ) + return res +} + +export const createHogExecutionGlobals = ( + data: Partial = {} +): HogFunctionInvocationGlobals => { + return { + ...data, + project: { + id: 1, + name: 'test', + url: 'http://localhost:8000/projects/1', + ...(data.project ?? {}), + }, + event: { + uuid: 'uuid', + name: 'test', + distinct_id: 'distinct_id', + url: 'http://localhost:8000/events/1', + properties: { + $lib_version: '1.2.3', + }, + timestamp: new Date().toISOString(), + ...(data.event ?? {}), + }, + } +} diff --git a/plugin-server/tests/cdp/hog-executor.test.ts b/plugin-server/tests/cdp/hog-executor.test.ts new file mode 100644 index 0000000000000..7f989ca01fdbd --- /dev/null +++ b/plugin-server/tests/cdp/hog-executor.test.ts @@ -0,0 +1,123 @@ +import { HogExecutor } from '../../src/cdp/hog-executor' +import { HogFunctionManager } from '../../src/cdp/hog-function-manager' +import { defaultConfig } from '../../src/config/config' +import { PluginsServerConfig } from '../../src/types' +import { RustyHook } from '../../src/worker/rusty-hook' +import { HOG_EXAMPLES, HOG_FILTERS_EXAMPLES, HOG_INPUTS_EXAMPLES } from './examples' +import { createHogExecutionGlobals, createHogFunction, insertHogFunction as _insertHogFunction } from './fixtures' + +const config: PluginsServerConfig = { + ...defaultConfig, +} + +jest.mock('../../src/utils/fetch', () => { + return { + trackedFetch: jest.fn(() => Promise.resolve({ status: 200, text: () => Promise.resolve({}) })), + } +}) + +const mockFetch = require('../../src/utils/fetch').trackedFetch + +describe('Hog Executor', () => { + jest.setTimeout(1000) + let executor: HogExecutor + + const mockFunctionManager = { + reloadAllHogFunctions: jest.fn(), + getTeamHogFunctions: jest.fn(), + } + + const mockRustyHook = { + enqueueIfEnabledForTeam: jest.fn(() => true), + } + + beforeEach(() => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-06-07T12:00:00.000Z').getTime()) + executor = new HogExecutor( + config, + mockFunctionManager as any as HogFunctionManager, + mockRustyHook as any as RustyHook + ) + }) + + describe('general event processing', () => { + /** + * Tests here are somewhat expensive so should mostly simulate happy paths and the more e2e scenarios + */ + it('can parse incoming messages correctly', async () => { + const fn = createHogFunction({ + ...HOG_EXAMPLES.simple_fetch, + ...HOG_INPUTS_EXAMPLES.simple_fetch, + ...HOG_FILTERS_EXAMPLES.no_filters, + }) + + mockFunctionManager.getTeamHogFunctions.mockReturnValue({ + [1]: fn, + }) + + // Create a message that should be processed by this function + // Run the function and check that it was executed + await executor.executeMatchingFunctions(createHogExecutionGlobals()) + + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockFetch.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "https://example.com/posthog-webhook", + Object { + "body": "{ + \\"event\\": { + \\"uuid\\": \\"uuid\\", + \\"name\\": \\"test\\", + \\"distinct_id\\": \\"distinct_id\\", + \\"url\\": \\"http://localhost:8000/events/1\\", + \\"properties\\": { + \\"$lib_version\\": \\"1.2.3\\" + }, + \\"timestamp\\": \\"2024-06-07T12:00:00.000Z\\" + }, + \\"groups\\": null, + \\"nested\\": { + \\"foo\\": \\"http://localhost:8000/events/1\\" + }, + \\"person\\": null, + \\"event_url\\": \\"http://localhost:8000/events/1-test\\" + }", + "headers": Object { + "version": "v=1.2.3", + }, + "method": "POST", + "timeout": 10000, + }, + ] + `) + }) + // NOTE: Will be fixed in follow up + it('can filters incoming messages correctly', async () => { + const fn = createHogFunction({ + ...HOG_EXAMPLES.simple_fetch, + ...HOG_INPUTS_EXAMPLES.simple_fetch, + ...HOG_FILTERS_EXAMPLES.pageview_or_autocapture_filter, + }) + + mockFunctionManager.getTeamHogFunctions.mockReturnValue({ + [1]: fn, + }) + + const resultsShouldntMatch = await executor.executeMatchingFunctions(createHogExecutionGlobals()) + expect(resultsShouldntMatch).toHaveLength(0) + + const resultsShouldMatch = await executor.executeMatchingFunctions( + createHogExecutionGlobals({ + event: { + name: '$pageview', + properties: { + $current_url: 'https://posthog.com', + }, + } as any, + }) + ) + expect(resultsShouldMatch).toHaveLength(1) + }) + }) +}) diff --git a/plugin-server/tests/worker/ingestion/person-state.test.ts b/plugin-server/tests/worker/ingestion/person-state.test.ts index ab921d71902cc..88b1f1dfabfc8 100644 --- a/plugin-server/tests/worker/ingestion/person-state.test.ts +++ b/plugin-server/tests/worker/ingestion/person-state.test.ts @@ -2207,7 +2207,10 @@ describe('PersonState.update()', () => { // then pros can be dropped, see https://docs.google.com/presentation/d/1Osz7r8bKkDD5yFzw0cCtsGVf1LTEifXS-dzuwaS8JGY // properties: { first: true, second: true, third: true }, created_at: timestamp, - version: 1, // the test intends for it to be a chain, so must get v1, we get v2 if second->first and third->first, but we want it to be third->second->first + // This is 2 because they all start with version 0, and then: x + // third -> second = max(third(0), second(0)) + 1 == version 1 + // second -> first = max(second(1), first(0)) + 1 == version 2 + version: 2, is_identified: true, }) ) @@ -2296,7 +2299,10 @@ describe('PersonState.update()', () => { uuid: firstUserUuid, // guaranteed to be merged into this based on timestamps properties: { first: true, second: true, third: true }, created_at: timestamp, - version: 1, // the test intends for it to be a chain, so must get v1, we get v2 if second->first and third->first, but we want it to be third->second->first + // This is 2 because they all start with version 0, and then: + // third -> second = max(third(0), second(0)) + 1 == version 1 + // second -> first = max(second(1), first(0)) + 1 == version 2 + version: 2, is_identified: true, }) ) diff --git a/posthog/api/hog_function.py b/posthog/api/hog_function.py new file mode 100644 index 0000000000000..77cb4c88e8a7b --- /dev/null +++ b/posthog/api/hog_function.py @@ -0,0 +1,182 @@ +import structlog +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import serializers, viewsets +from rest_framework.serializers import BaseSerializer + +from posthog.api.forbid_destroy_model import ForbidDestroyModel +from posthog.api.log_entries import LogEntryMixin +from posthog.api.routing import TeamAndOrgViewSetMixin +from posthog.api.shared import UserBasicSerializer +from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.parser import parse_program +from posthog.models.hog_functions.hog_function import HogFunction +from posthog.models.hog_functions.utils import generate_template_bytecode +from posthog.permissions import PostHogFeatureFlagPermission + + +logger = structlog.get_logger(__name__) + + +class InputsSchemaItemSerializer(serializers.Serializer): + type = serializers.ChoiceField(choices=["string", "boolean", "dictionary", "choice", "json"]) + key = serializers.CharField() + label = serializers.CharField(required=False) # type: ignore + choices = serializers.ListField(child=serializers.DictField(), required=False) + required = serializers.BooleanField(default=False) # type: ignore + default = serializers.JSONField(required=False) + secret = serializers.BooleanField(default=False) + description = serializers.CharField(required=False) + + # TODO Validate choices if type=choice + + +class AnyInputField(serializers.Field): + def to_internal_value(self, data): + return data + + def to_representation(self, value): + return value + + +class InputsItemSerializer(serializers.Serializer): + value = AnyInputField(required=False) + bytecode = serializers.ListField(required=False, read_only=True) + + def validate(self, attrs): + schema = self.context["schema"] + value = attrs.get("value") + + if schema.get("required") and not value: + raise serializers.ValidationError("This field is required.") + + if not value: + return attrs + + name: str = schema["key"] + item_type = schema["type"] + value = attrs["value"] + + # Validate each type + if item_type == "string": + if not isinstance(value, str): + raise serializers.ValidationError("Value must be a string.") + elif item_type == "boolean": + if not isinstance(value, bool): + raise serializers.ValidationError("Value must be a boolean.") + elif item_type == "dictionary": + if not isinstance(value, dict): + raise serializers.ValidationError("Value must be a dictionary.") + + try: + if value: + if item_type in ["string", "dictionary", "json"]: + attrs["bytecode"] = generate_template_bytecode(value) + except Exception as e: + raise serializers.ValidationError({"inputs": {name: f"Invalid template: {str(e)}"}}) + + return attrs + + +class HogFunctionMinimalSerializer(serializers.ModelSerializer): + created_by = UserBasicSerializer(read_only=True) + + class Meta: + model = HogFunction + fields = [ + "id", + "name", + "description", + "created_at", + "created_by", + "updated_at", + "enabled", + "hog", + "filters", + ] + read_only_fields = fields + + +class HogFunctionSerializer(HogFunctionMinimalSerializer): + class Meta: + model = HogFunction + fields = [ + "id", + "name", + "description", + "created_at", + "created_by", + "updated_at", + "enabled", + "hog", + "bytecode", + "inputs_schema", + "inputs", + "filters", + ] + read_only_fields = [ + "id", + "created_at", + "created_by", + "updated_at", + "bytecode", + ] + + def validate_inputs_schema(self, value): + if not isinstance(value, list): + raise serializers.ValidationError("inputs_schema must be a list of objects.") + + serializer = InputsSchemaItemSerializer(data=value, many=True) + + if not serializer.is_valid(): + raise serializers.ValidationError(serializer.errors) + + return serializer.validated_data or [] + + def validate(self, attrs): + team = self.context["get_team"]() + attrs["team"] = team + attrs["inputs_schema"] = attrs.get("inputs_schema", []) + attrs["inputs"] = attrs.get("inputs", {}) + attrs["filters"] = attrs.get("filters", {}) + + validated_inputs = {} + + for schema in attrs["inputs_schema"]: + value = attrs["inputs"].get(schema["key"], {}) + serializer = InputsItemSerializer(data=value, context={"schema": schema}) + + if not serializer.is_valid(): + first_error = next(iter(serializer.errors.values()))[0] + raise serializers.ValidationError({"inputs": {schema["key"]: first_error}}) + + validated_inputs[schema["key"]] = serializer.validated_data + + attrs["inputs"] = validated_inputs + + # Attempt to compile the hog + try: + program = parse_program(attrs["hog"]) + attrs["bytecode"] = create_bytecode(program, supported_functions={"fetch"}) + except Exception as e: + raise serializers.ValidationError({"hog": str(e)}) + + return attrs + + def create(self, validated_data: dict, *args, **kwargs) -> HogFunction: + request = self.context["request"] + validated_data["created_by"] = request.user + return super().create(validated_data=validated_data) + + +class HogFunctionViewSet(TeamAndOrgViewSetMixin, LogEntryMixin, ForbidDestroyModel, viewsets.ModelViewSet): + scope_object = "INTERNAL" # Keep internal until we are happy to release this GA + queryset = HogFunction.objects.all() + filter_backends = [DjangoFilterBackend] + filterset_fields = ["id", "team", "created_by", "enabled"] + + permission_classes = [PostHogFeatureFlagPermission] + posthog_feature_flag = {"hog-functions": ["create", "partial_update", "update"]} + log_source = "hog_function" + + def get_serializer_class(self) -> type[BaseSerializer]: + return HogFunctionMinimalSerializer if self.action == "list" else HogFunctionSerializer diff --git a/posthog/api/log_entries.py b/posthog/api/log_entries.py new file mode 100644 index 0000000000000..fda13747bf266 --- /dev/null +++ b/posthog/api/log_entries.py @@ -0,0 +1,121 @@ +import dataclasses +from datetime import datetime +from typing import Any, Optional, cast +from rest_framework import serializers, viewsets +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError +from rest_framework_dataclasses.serializers import DataclassSerializer + +from posthog.clickhouse.client.execute import sync_execute + + +@dataclasses.dataclass(frozen=True) +class LogEntry: + log_source_id: str + instance_id: str + timestamp: datetime + level: str + message: str + + +class LogEntrySerializer(DataclassSerializer): + class Meta: + dataclass = LogEntry + + +class LogEntryRequestSerializer(serializers.Serializer): + limit = serializers.IntegerField(required=False, default=50, max_value=500, min_value=1) + after = serializers.DateTimeField(required=False) + before = serializers.DateTimeField(required=False) + level = serializers.ListField(child=serializers.CharField(), required=False) + search = serializers.CharField(required=False) + instance_id = serializers.CharField(required=False) + + +def fetch_log_entries( + team_id: int, + log_source: str, + log_source_id: str, + limit: int, + instance_id: Optional[str] = None, + after: Optional[datetime] = None, + before: Optional[datetime] = None, + search: Optional[str] = None, + level: Optional[list[str]] = None, +) -> list[Any]: + """Fetch a list of batch export log entries from ClickHouse.""" + if level is None: + level = [] + clickhouse_where_parts: list[str] = [] + clickhouse_kwargs: dict[str, Any] = {} + + clickhouse_where_parts.append("log_source = %(log_source)s") + clickhouse_kwargs["log_source"] = log_source + clickhouse_where_parts.append("log_source_id = %(log_source_id)s") + clickhouse_kwargs["log_source_id"] = log_source_id + clickhouse_where_parts.append("team_id = %(team_id)s") + clickhouse_kwargs["team_id"] = team_id + + if instance_id: + clickhouse_where_parts.append("instance_id = %(instance_id)s") + clickhouse_kwargs["instance_id"] = instance_id + if after: + clickhouse_where_parts.append("timestamp > toDateTime64(%(after)s, 6)") + clickhouse_kwargs["after"] = after.isoformat().replace("+00:00", "") + if before: + clickhouse_where_parts.append("timestamp < toDateTime64(%(before)s, 6)") + clickhouse_kwargs["before"] = before.isoformat().replace("+00:00", "") + if search: + clickhouse_where_parts.append("message ILIKE %(search)s") + clickhouse_kwargs["search"] = f"%{search}%" + if len(level) > 0: + clickhouse_where_parts.append("upper(level) in %(levels)s") + clickhouse_kwargs["levels"] = level + + clickhouse_query = f""" + SELECT log_source_id, instance_id, timestamp, upper(level) as level, message FROM log_entries + WHERE {' AND '.join(clickhouse_where_parts)} ORDER BY timestamp DESC {f'LIMIT {limit}'} + """ + + return [LogEntry(*result) for result in cast(list, sync_execute(clickhouse_query, clickhouse_kwargs))] + + +class LogEntryMixin(viewsets.GenericViewSet): + log_source: str # Should be set by the inheriting class + + @action(detail=True, methods=["GET"]) + def logs(self, request: Request, *args, **kwargs): + obj = self.get_object() + + param_serializer = LogEntryRequestSerializer(data=request.query_params) + + if not self.log_source: + raise ValidationError("log_source not set on the viewset") + + if not param_serializer.is_valid(): + raise ValidationError(param_serializer.errors) + + params = param_serializer.validated_data + + data = fetch_log_entries( + team_id=self.team_id, # type: ignore + log_source=self.log_source, + log_source_id=str(obj.id), + limit=params["limit"], + # From request params + instance_id=params.get("instance_id"), + after=params.get("after"), + before=params.get("before"), + search=params.get("search"), + level=params.get("level"), + ) + + page = self.paginate_queryset(data) + if page is not None: + serializer = LogEntrySerializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = LogEntrySerializer(data, many=True) + return Response({"status": "not implemented"}) diff --git a/posthog/api/test/test_hog_function.py b/posthog/api/test/test_hog_function.py new file mode 100644 index 0000000000000..63b6fbc22ec93 --- /dev/null +++ b/posthog/api/test/test_hog_function.py @@ -0,0 +1,287 @@ +import json +from unittest.mock import ANY, patch + +from rest_framework import status + +from posthog.models.action.action import Action +from posthog.test.base import APIBaseTest, ClickhouseTestMixin, QueryMatchingTest + + +EXAMPLE_FULL = { + "name": "HogHook", + "hog": "fetch(inputs.url, {\n 'headers': inputs.headers,\n 'body': inputs.payload,\n 'method': inputs.method\n});", + "inputs_schema": [ + {"key": "url", "type": "string", "label": "Webhook URL", "required": True}, + {"key": "payload", "type": "json", "label": "JSON Payload", "required": True}, + { + "key": "method", + "type": "choice", + "label": "HTTP Method", + "choices": [ + {"label": "POST", "value": "POST"}, + {"label": "PUT", "value": "PUT"}, + {"label": "PATCH", "value": "PATCH"}, + {"label": "GET", "value": "GET"}, + ], + "required": True, + }, + {"key": "headers", "type": "dictionary", "label": "Headers", "required": False}, + ], + "inputs": { + "url": { + "value": "http://localhost:2080/0e02d917-563f-4050-9725-aad881b69937", + }, + "method": {"value": "POST"}, + "headers": { + "value": {"version": "v={event.properties.$lib_version}"}, + }, + "payload": { + "value": { + "event": "{event}", + "groups": "{groups}", + "nested": {"foo": "{event.url}"}, + "person": "{person}", + "event_url": "{f'{event.url}-test'}", + }, + }, + }, + "filters": { + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + "actions": [{"id": "9", "name": "Test Action", "type": "actions", "order": 1}], + "filter_test_accounts": True, + }, +} + + +class TestHogFunctionAPI(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest): + @patch("posthog.permissions.posthoganalytics.feature_enabled") + def test_create_hog_function_forbidden_if_not_in_flag(self, mock_feature_enabled): + mock_feature_enabled.return_value = False + + response = self.client.post( + f"/api/projects/{self.team.id}/hog_functions/", + data={ + "name": "Fetch URL", + "description": "Test description", + "hog": "fetch(inputs.url);", + }, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN, response.json() + + assert mock_feature_enabled.call_count == 1 + assert mock_feature_enabled.call_args[0][0] == ("hog-functions") + + @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) + def test_create_hog_function(self, *args): + response = self.client.post( + f"/api/projects/{self.team.id}/hog_functions/", + data={ + "name": "Fetch URL", + "description": "Test description", + "hog": "fetch(inputs.url);", + }, + ) + assert response.status_code == status.HTTP_201_CREATED, response.json() + assert response.json()["created_by"]["id"] == self.user.id + assert response.json() == { + "id": ANY, + "name": "Fetch URL", + "description": "Test description", + "created_at": ANY, + "created_by": ANY, + "updated_at": ANY, + "enabled": False, + "hog": "fetch(inputs.url);", + "bytecode": ["_h", 32, "url", 32, "inputs", 1, 2, 2, "fetch", 1, 35], + "inputs_schema": [], + "inputs": {}, + "filters": {"bytecode": ["_h", 29]}, + } + + @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) + def test_inputs_required(self, *args): + payload = { + "name": "Fetch URL", + "hog": "fetch(inputs.url);", + "inputs_schema": [ + {"key": "url", "type": "string", "label": "Webhook URL", "required": True}, + ], + } + # Check required + res = self.client.post(f"/api/projects/{self.team.id}/hog_functions/", data={**payload}) + assert res.status_code == status.HTTP_400_BAD_REQUEST, res.json() + assert res.json() == { + "type": "validation_error", + "code": "invalid_input", + "detail": "This field is required.", + "attr": "inputs__url", + } + + @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) + def test_inputs_mismatch_type(self, *args): + payload = { + "name": "Fetch URL", + "hog": "fetch(inputs.url);", + "inputs_schema": [ + {"key": "string", "type": "string"}, + {"key": "dictionary", "type": "dictionary"}, + {"key": "boolean", "type": "boolean"}, + ], + } + + bad_inputs = { + "string": 123, + "dictionary": 123, + "boolean": 123, + } + + for key, value in bad_inputs.items(): + res = self.client.post( + f"/api/projects/{self.team.id}/hog_functions/", data={**payload, "inputs": {key: {"value": value}}} + ) + assert res.json() == { + "type": "validation_error", + "code": "invalid_input", + "detail": f"Value must be a {key}.", + "attr": f"inputs__{key}", + }, f"Did not get error for {key}, got {res.json()}" + assert res.status_code == status.HTTP_400_BAD_REQUEST, res.json() + + @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) + def test_generates_hog_bytecode(self, *args): + response = self.client.post( + f"/api/projects/{self.team.id}/hog_functions/", + data={ + "name": "Fetch URL", + "hog": "let i := 0;\nwhile(i < 3) {\n i := i + 1;\n fetch(inputs.url, {\n 'headers': {\n 'x-count': f'{i}'\n },\n 'body': inputs.payload,\n 'method': inputs.method\n });\n}", + }, + ) + # JSON loads for one line comparison + assert response.json()["bytecode"] == json.loads( + '["_h", 33, 0, 33, 3, 36, 0, 15, 40, 45, 33, 1, 36, 0, 6, 37, 0, 32, "headers", 32, "x-count", 36, 0, 42, 1, 32, "body", 32, "payload", 32, "inputs", 1, 2, 32, "method", 32, "method", 32, "inputs", 1, 2, 42, 3, 32, "url", 32, "inputs", 1, 2, 2, "fetch", 2, 35, 39, -52, 35]' + ), response.json() + + @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) + def test_generates_inputs_bytecode(self, *args): + response = self.client.post(f"/api/projects/{self.team.id}/hog_functions/", data=EXAMPLE_FULL) + assert response.status_code == status.HTTP_201_CREATED, response.json() + assert response.json()["inputs"] == { + "url": { + "value": "http://localhost:2080/0e02d917-563f-4050-9725-aad881b69937", + "bytecode": ["_h", 32, "http://localhost:2080/0e02d917-563f-4050-9725-aad881b69937"], + }, + "payload": { + "value": { + "event": "{event}", + "groups": "{groups}", + "nested": {"foo": "{event.url}"}, + "person": "{person}", + "event_url": "{f'{event.url}-test'}", + }, + "bytecode": { + "event": ["_h", 32, "event", 1, 1], + "groups": ["_h", 32, "groups", 1, 1], + "nested": {"foo": ["_h", 32, "url", 32, "event", 1, 2]}, + "person": ["_h", 32, "person", 1, 1], + "event_url": ["_h", 32, "-test", 32, "url", 32, "event", 1, 2, 2, "concat", 2], + }, + }, + "method": {"value": "POST"}, + "headers": { + "value": {"version": "v={event.properties.$lib_version}"}, + "bytecode": { + "version": ["_h", 32, "$lib_version", 32, "properties", 32, "event", 1, 3, 32, "v=", 2, "concat", 2] + }, + }, + } + + @patch("posthog.permissions.posthoganalytics.feature_enabled", return_value=True) + def test_generates_filters_bytecode(self, *args): + action = Action.objects.create( + team=self.team, + name="test action", + steps_json=[{"event": "$pageview", "url": "docs", "url_matching": "contains"}], + ) + + self.team.test_account_filters = [ + { + "key": "email", + "value": "@posthog.com", + "operator": "not_icontains", + "type": "person", + } + ] + self.team.save() + response = self.client.post( + f"/api/projects/{self.team.id}/hog_functions/", + data={ + **EXAMPLE_FULL, + "filters": { + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + "actions": [{"id": f"{action.id}", "name": "Test Action", "type": "actions", "order": 1}], + "filter_test_accounts": True, + }, + }, + ) + assert response.status_code == status.HTTP_201_CREATED, response.json() + assert response.json()["filters"] == { + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + "actions": [{"id": f"{action.id}", "name": "Test Action", "type": "actions", "order": 1}], + "filter_test_accounts": True, + "bytecode": [ + "_h", + 32, + "%docs%", + 32, + "$current_url", + 32, + "properties", + 1, + 2, + 17, + 32, + "$pageview", + 32, + "event", + 1, + 1, + 11, + 3, + 2, + 32, + "%@posthog.com%", + 32, + "email", + 32, + "properties", + 32, + "person", + 1, + 3, + 20, + 3, + 2, + 32, + "$pageview", + 32, + "event", + 1, + 1, + 11, + 32, + "%@posthog.com%", + 32, + "email", + 32, + "properties", + 32, + "person", + 1, + 3, + 20, + 3, + 2, + 4, + 2, + ], + } diff --git a/posthog/clickhouse/client/limit.py b/posthog/clickhouse/client/limit.py new file mode 100644 index 0000000000000..7af284451816d --- /dev/null +++ b/posthog/clickhouse/client/limit.py @@ -0,0 +1,84 @@ +import time +from functools import wraps +from typing import Optional +from collections.abc import Callable + +from celery import current_task +from prometheus_client import Counter + +from posthog import redis + +CONCURRENT_TASKS_LIMIT_EXCEEDED_COUNTER = Counter( + "posthog_celery_task_concurrency_limit_exceeded", + "Number of times a Celery task exceeded the concurrency limit", + ["task_name", "limit", "key"], +) + +# Lua script for atomic check, remove expired if limit hit, and increment with TTL +lua_script = """ +local key = KEYS[1] +local current_time = tonumber(ARGV[1]) +local task_id = ARGV[2] +local max_concurrent_tasks = tonumber(ARGV[3]) +local ttl = tonumber(ARGV[4]) +local expiration_time = current_time + ttl + +-- Check the number of current running tasks +local running_tasks_count = redis.call('ZCARD', key) +if running_tasks_count >= max_concurrent_tasks then + -- Remove expired tasks if limit is hit + redis.call('ZREMRANGEBYSCORE', key, '-inf', current_time) + running_tasks_count = redis.call('ZCARD', key) + if running_tasks_count >= max_concurrent_tasks then + return 0 + end +end + +-- Add the new task with its expiration time +redis.call('ZADD', key, expiration_time, task_id) +return 1 +""" + + +class CeleryConcurrencyLimitExceeded(Exception): + pass + + +def limit_concurrency(max_concurrent_tasks: int, key: Optional[Callable] = None, ttl: int = 60 * 15) -> Callable: + def decorator(task_func): + @wraps(task_func) + def wrapper(*args, **kwargs): + task_name = current_task.name + redis_client = redis.get_client() + running_tasks_key = f"celery_running_tasks:{task_name}" + if key: + dynamic_key = key(*args, **kwargs) + running_tasks_key = f"{running_tasks_key}:{dynamic_key}" + else: + dynamic_key = None + task_id = f"{task_name}:{current_task.request.id}" + current_time = int(time.time()) + + # Atomically check, remove expired if limit hit, and add the new task + if ( + redis_client.eval(lua_script, 1, running_tasks_key, current_time, task_id, max_concurrent_tasks, ttl) + == 0 + ): + CONCURRENT_TASKS_LIMIT_EXCEEDED_COUNTER.labels( + task_name=task_name, limit=max_concurrent_tasks, key=dynamic_key + ).inc() + + raise CeleryConcurrencyLimitExceeded( + f"Exceeded maximum concurrent tasks limit: {max_concurrent_tasks} for key: {dynamic_key}" + ) + + try: + # Execute the task + return task_func(*args, **kwargs) + finally: + # Remove the task ID from the sorted set when the task finishes + redis_client.zrem(running_tasks_key, task_id) + + return wrapper + + return decorator diff --git a/posthog/clickhouse/client/test/test_execute_async.py b/posthog/clickhouse/client/test/test_execute_async.py index af27d8de620e6..83f42c2f9c6ea 100644 --- a/posthog/clickhouse/client/test/test_execute_async.py +++ b/posthog/clickhouse/client/test/test_execute_async.py @@ -160,7 +160,7 @@ def test_async_query_client_errors(self): result = client.get_query_status(self.team.id, query_id) self.assertTrue(result.error) assert result.error_message - self.assertRegex(result.error_message, "Unknown table") + self.assertRegex(result.error_message, "no viable alternative at input") def test_async_query_client_uuid(self): query = build_query("SELECT toUUID('00000000-0000-0000-0000-000000000000')") diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index d9e71e34e4cac..4940fda0a66d8 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -47,7 +47,7 @@ class Statement(Declaration): @dataclass(kw_only=True) class ExprStatement(Statement): - expr: Expr + expr: Optional[Expr] @dataclass(kw_only=True) diff --git a/posthog/hogql/autocomplete.py b/posthog/hogql/autocomplete.py index 0f73304061f33..b52268f7cfc9b 100644 --- a/posthog/hogql/autocomplete.py +++ b/posthog/hogql/autocomplete.py @@ -51,7 +51,7 @@ def __init__(self, expr: ast.Expr, start: int, end: int): self.end = end super().visit(expr) - def visit(self, node: AST): + def visit(self, node: AST | None): if node is not None and node.start is not None and node.end is not None: if self.start >= node.start and self.end <= node.end: self.node = node diff --git a/posthog/hogql/bytecode.py b/posthog/hogql/bytecode.py index 500f20dd496ea..f5ec49313d487 100644 --- a/posthog/hogql/bytecode.py +++ b/posthog/hogql/bytecode.py @@ -227,6 +227,8 @@ def visit_block(self, node: ast.Block): return response def visit_expr_statement(self, node: ast.ExprStatement): + if node.expr is None: + return [] response = self.visit(node.expr) response.append(Operation.POP) return response diff --git a/posthog/hogql/database/schema/util/where_clause_extractor.py b/posthog/hogql/database/schema/util/where_clause_extractor.py index 1e314f02d4993..7cb413960ca80 100644 --- a/posthog/hogql/database/schema/util/where_clause_extractor.py +++ b/posthog/hogql/database/schema/util/where_clause_extractor.py @@ -416,6 +416,9 @@ def visit_placeholder(self, node: ast.Placeholder) -> bool: def visit_alias(self, node: ast.Alias) -> bool: return self.visit(node.expr) + def visit_tuple(self, node: ast.Tuple) -> bool: + return all(self.visit(arg) for arg in node.exprs) + def is_simple_timestamp_field_expression(expr: ast.Expr, context: HogQLContext, tombstone_string: str) -> bool: return IsSimpleTimestampFieldExpressionVisitor(context, tombstone_string).visit(expr) @@ -515,6 +518,9 @@ def visit_alias(self, node: ast.Alias) -> bool: return self.visit(node.expr) + def visit_tuple(self, node: ast.Tuple) -> bool: + return all(self.visit(arg) for arg in node.exprs) + def rewrite_timestamp_field(expr: ast.Expr, context: HogQLContext) -> ast.Expr: return RewriteTimestampFieldVisitor(context).visit(expr) diff --git a/posthog/hogql/grammar/HogQLParser.g4 b/posthog/hogql/grammar/HogQLParser.g4 index 911f5827073d0..a5fcee76d7d31 100644 --- a/posthog/hogql/grammar/HogQLParser.g4 +++ b/posthog/hogql/grammar/HogQLParser.g4 @@ -6,32 +6,29 @@ options { program: declaration* EOF; -declaration - : varDecl - | statement ; + +declaration: varDecl | statement ; expression: columnExpr; -varDecl: LET identifier ( COLON EQ_SINGLE expression )? SEMICOLON ; -varAssignment: expression COLON EQ_SINGLE expression SEMICOLON ; +varDecl: LET identifier ( COLON EQ_SINGLE expression )? ; identifierList: identifier (COMMA identifier)*; statement : returnStmt - | emptyStmt - | exprStmt | ifStmt | whileStmt | funcStmt | varAssignment - | returnStmt + | exprStmt + | emptyStmt | block ; -exprStmt : expression SEMICOLON ; -ifStmt : IF LPAREN expression RPAREN statement - ( ELSE statement )? ; -whileStmt : WHILE LPAREN expression RPAREN statement; -returnStmt : RETURN expression SEMICOLON ; +returnStmt : RETURN expression? SEMICOLON?; +ifStmt : IF LPAREN expression RPAREN statement ( ELSE statement )? ; +whileStmt : WHILE LPAREN expression RPAREN statement SEMICOLON?; funcStmt : FN identifier LPAREN identifierList? RPAREN block; +varAssignment : expression COLON EQ_SINGLE expression ; +exprStmt : expression SEMICOLON?; emptyStmt : SEMICOLON ; block : LBRACE declaration* RBRACE ; @@ -184,8 +181,7 @@ columnExpr // TODO(ilezhankin): `BETWEEN a AND b AND c` is parsed in a wrong way: `BETWEEN (a AND b) AND c` | columnExpr NOT? BETWEEN columnExpr AND columnExpr # ColumnExprBetween | columnExpr QUERY columnExpr COLON columnExpr # ColumnExprTernaryOp - // Note: difference with ClickHouse: we also support "AS string" as a shortcut for naming columns - | columnExpr (alias | AS identifier | AS STRING_LITERAL) # ColumnExprAlias + | columnExpr (AS identifier | AS STRING_LITERAL) # ColumnExprAlias | (tableIdentifier DOT)? ASTERISK # ColumnExprAsterisk // single-column only | LPAREN selectUnionStmt RPAREN # ColumnExprSubquery // single-column only diff --git a/posthog/hogql/grammar/HogQLParser.interp b/posthog/hogql/grammar/HogQLParser.interp index 086eca220c32f..620eb0b471748 100644 --- a/posthog/hogql/grammar/HogQLParser.interp +++ b/posthog/hogql/grammar/HogQLParser.interp @@ -317,14 +317,14 @@ program declaration expression varDecl -varAssignment identifierList statement -exprStmt +returnStmt ifStmt whileStmt -returnStmt funcStmt +varAssignment +exprStmt emptyStmt block kvPair @@ -399,4 +399,4 @@ stringContentsFull atn: -[4, 1, 154, 1178, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 5, 5, 199, 8, 5, 10, 5, 12, 5, 202, 9, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 213, 8, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 225, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 241, 8, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 250, 8, 13, 10, 13, 12, 13, 253, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 264, 8, 15, 10, 15, 12, 15, 267, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 272, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 280, 8, 17, 10, 17, 12, 17, 283, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 291, 8, 18, 1, 19, 3, 19, 294, 8, 19, 1, 19, 1, 19, 3, 19, 298, 8, 19, 1, 19, 3, 19, 301, 8, 19, 1, 19, 1, 19, 3, 19, 305, 8, 19, 1, 19, 3, 19, 308, 8, 19, 1, 19, 3, 19, 311, 8, 19, 1, 19, 3, 19, 314, 8, 19, 1, 19, 3, 19, 317, 8, 19, 1, 19, 1, 19, 3, 19, 321, 8, 19, 1, 19, 1, 19, 3, 19, 325, 8, 19, 1, 19, 3, 19, 328, 8, 19, 1, 19, 3, 19, 331, 8, 19, 1, 19, 3, 19, 334, 8, 19, 1, 19, 1, 19, 3, 19, 338, 8, 19, 1, 19, 3, 19, 341, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 350, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 356, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 375, 8, 24, 10, 24, 12, 24, 378, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 394, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 411, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 417, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 423, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 434, 8, 31, 3, 31, 436, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 447, 8, 34, 1, 34, 3, 34, 450, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 456, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 464, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 470, 8, 34, 10, 34, 12, 34, 473, 9, 34, 1, 35, 3, 35, 476, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 481, 8, 35, 1, 35, 3, 35, 484, 8, 35, 1, 35, 3, 35, 487, 8, 35, 1, 35, 1, 35, 3, 35, 491, 8, 35, 1, 35, 1, 35, 3, 35, 495, 8, 35, 1, 35, 3, 35, 498, 8, 35, 3, 35, 500, 8, 35, 1, 35, 3, 35, 503, 8, 35, 1, 35, 1, 35, 3, 35, 507, 8, 35, 1, 35, 1, 35, 3, 35, 511, 8, 35, 1, 35, 3, 35, 514, 8, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 523, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 534, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 540, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 545, 8, 39, 10, 39, 12, 39, 548, 9, 39, 1, 40, 1, 40, 3, 40, 552, 8, 40, 1, 40, 1, 40, 3, 40, 556, 8, 40, 1, 40, 1, 40, 3, 40, 560, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 566, 8, 41, 3, 41, 568, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 573, 8, 42, 10, 42, 12, 42, 576, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 583, 8, 44, 1, 44, 3, 44, 586, 8, 44, 1, 44, 3, 44, 589, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 608, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 622, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 636, 8, 51, 10, 51, 12, 51, 639, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 648, 8, 51, 10, 51, 12, 51, 651, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 660, 8, 51, 10, 51, 12, 51, 663, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 670, 8, 51, 1, 51, 1, 51, 3, 51, 674, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 679, 8, 52, 10, 52, 12, 52, 682, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 687, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 694, 8, 53, 11, 53, 12, 53, 695, 1, 53, 1, 53, 3, 53, 700, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 724, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 741, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 747, 8, 53, 1, 53, 3, 53, 750, 8, 53, 1, 53, 3, 53, 753, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 763, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 769, 8, 53, 1, 53, 3, 53, 772, 8, 53, 1, 53, 3, 53, 775, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 783, 8, 53, 1, 53, 3, 53, 786, 8, 53, 1, 53, 1, 53, 3, 53, 790, 8, 53, 1, 53, 3, 53, 793, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 807, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 824, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 829, 8, 53, 1, 53, 1, 53, 3, 53, 833, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 839, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 846, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 858, 8, 53, 1, 53, 1, 53, 3, 53, 862, 8, 53, 1, 53, 3, 53, 865, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 874, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 888, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 915, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 924, 8, 53, 5, 53, 926, 8, 53, 10, 53, 12, 53, 929, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 934, 8, 54, 10, 54, 12, 54, 937, 9, 54, 1, 55, 1, 55, 3, 55, 941, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 947, 8, 56, 10, 56, 12, 56, 950, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 957, 8, 56, 10, 56, 12, 56, 960, 9, 56, 3, 56, 962, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 970, 8, 57, 10, 57, 12, 57, 973, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 981, 8, 57, 10, 57, 12, 57, 984, 9, 57, 1, 57, 1, 57, 3, 57, 988, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 995, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 1008, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 1013, 8, 59, 10, 59, 12, 59, 1016, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1028, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1034, 8, 61, 1, 61, 3, 61, 1037, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1042, 8, 62, 10, 62, 12, 62, 1045, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1056, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1062, 8, 63, 5, 63, 1064, 8, 63, 10, 63, 12, 63, 1067, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1072, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1079, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1086, 8, 66, 10, 66, 12, 66, 1089, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1099, 8, 68, 3, 68, 1101, 8, 68, 1, 69, 3, 69, 1104, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1112, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1117, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1127, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1132, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1144, 8, 78, 1, 79, 1, 79, 5, 79, 1148, 8, 79, 10, 79, 12, 79, 1151, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1160, 8, 80, 1, 81, 1, 81, 5, 81, 1164, 8, 81, 10, 81, 12, 81, 1167, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1176, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1314, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 189, 1, 0, 0, 0, 10, 195, 1, 0, 0, 0, 12, 212, 1, 0, 0, 0, 14, 214, 1, 0, 0, 0, 16, 217, 1, 0, 0, 0, 18, 226, 1, 0, 0, 0, 20, 232, 1, 0, 0, 0, 22, 236, 1, 0, 0, 0, 24, 245, 1, 0, 0, 0, 26, 247, 1, 0, 0, 0, 28, 256, 1, 0, 0, 0, 30, 260, 1, 0, 0, 0, 32, 271, 1, 0, 0, 0, 34, 275, 1, 0, 0, 0, 36, 290, 1, 0, 0, 0, 38, 293, 1, 0, 0, 0, 40, 342, 1, 0, 0, 0, 42, 345, 1, 0, 0, 0, 44, 351, 1, 0, 0, 0, 46, 355, 1, 0, 0, 0, 48, 361, 1, 0, 0, 0, 50, 379, 1, 0, 0, 0, 52, 382, 1, 0, 0, 0, 54, 385, 1, 0, 0, 0, 56, 395, 1, 0, 0, 0, 58, 398, 1, 0, 0, 0, 60, 402, 1, 0, 0, 0, 62, 435, 1, 0, 0, 0, 64, 437, 1, 0, 0, 0, 66, 440, 1, 0, 0, 0, 68, 455, 1, 0, 0, 0, 70, 517, 1, 0, 0, 0, 72, 522, 1, 0, 0, 0, 74, 533, 1, 0, 0, 0, 76, 535, 1, 0, 0, 0, 78, 541, 1, 0, 0, 0, 80, 549, 1, 0, 0, 0, 82, 567, 1, 0, 0, 0, 84, 569, 1, 0, 0, 0, 86, 577, 1, 0, 0, 0, 88, 582, 1, 0, 0, 0, 90, 590, 1, 0, 0, 0, 92, 594, 1, 0, 0, 0, 94, 598, 1, 0, 0, 0, 96, 607, 1, 0, 0, 0, 98, 621, 1, 0, 0, 0, 100, 623, 1, 0, 0, 0, 102, 673, 1, 0, 0, 0, 104, 675, 1, 0, 0, 0, 106, 832, 1, 0, 0, 0, 108, 930, 1, 0, 0, 0, 110, 940, 1, 0, 0, 0, 112, 961, 1, 0, 0, 0, 114, 994, 1, 0, 0, 0, 116, 1007, 1, 0, 0, 0, 118, 1009, 1, 0, 0, 0, 120, 1027, 1, 0, 0, 0, 122, 1036, 1, 0, 0, 0, 124, 1038, 1, 0, 0, 0, 126, 1055, 1, 0, 0, 0, 128, 1068, 1, 0, 0, 0, 130, 1078, 1, 0, 0, 0, 132, 1082, 1, 0, 0, 0, 134, 1090, 1, 0, 0, 0, 136, 1100, 1, 0, 0, 0, 138, 1103, 1, 0, 0, 0, 140, 1116, 1, 0, 0, 0, 142, 1118, 1, 0, 0, 0, 144, 1120, 1, 0, 0, 0, 146, 1122, 1, 0, 0, 0, 148, 1126, 1, 0, 0, 0, 150, 1131, 1, 0, 0, 0, 152, 1133, 1, 0, 0, 0, 154, 1137, 1, 0, 0, 0, 156, 1143, 1, 0, 0, 0, 158, 1145, 1, 0, 0, 0, 160, 1159, 1, 0, 0, 0, 162, 1161, 1, 0, 0, 0, 164, 1175, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 12, 6, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 188, 5, 145, 0, 0, 188, 7, 1, 0, 0, 0, 189, 190, 3, 4, 2, 0, 190, 191, 5, 111, 0, 0, 191, 192, 5, 118, 0, 0, 192, 193, 3, 4, 2, 0, 193, 194, 5, 145, 0, 0, 194, 9, 1, 0, 0, 0, 195, 200, 3, 150, 75, 0, 196, 197, 5, 112, 0, 0, 197, 199, 3, 150, 75, 0, 198, 196, 1, 0, 0, 0, 199, 202, 1, 0, 0, 0, 200, 198, 1, 0, 0, 0, 200, 201, 1, 0, 0, 0, 201, 11, 1, 0, 0, 0, 202, 200, 1, 0, 0, 0, 203, 213, 3, 20, 10, 0, 204, 213, 3, 24, 12, 0, 205, 213, 3, 14, 7, 0, 206, 213, 3, 16, 8, 0, 207, 213, 3, 18, 9, 0, 208, 213, 3, 22, 11, 0, 209, 213, 3, 8, 4, 0, 210, 213, 3, 20, 10, 0, 211, 213, 3, 26, 13, 0, 212, 203, 1, 0, 0, 0, 212, 204, 1, 0, 0, 0, 212, 205, 1, 0, 0, 0, 212, 206, 1, 0, 0, 0, 212, 207, 1, 0, 0, 0, 212, 208, 1, 0, 0, 0, 212, 209, 1, 0, 0, 0, 212, 210, 1, 0, 0, 0, 212, 211, 1, 0, 0, 0, 213, 13, 1, 0, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 145, 0, 0, 216, 15, 1, 0, 0, 0, 217, 218, 5, 38, 0, 0, 218, 219, 5, 126, 0, 0, 219, 220, 3, 4, 2, 0, 220, 221, 5, 144, 0, 0, 221, 224, 3, 12, 6, 0, 222, 223, 5, 24, 0, 0, 223, 225, 3, 12, 6, 0, 224, 222, 1, 0, 0, 0, 224, 225, 1, 0, 0, 0, 225, 17, 1, 0, 0, 0, 226, 227, 5, 96, 0, 0, 227, 228, 5, 126, 0, 0, 228, 229, 3, 4, 2, 0, 229, 230, 5, 144, 0, 0, 230, 231, 3, 12, 6, 0, 231, 19, 1, 0, 0, 0, 232, 233, 5, 70, 0, 0, 233, 234, 3, 4, 2, 0, 234, 235, 5, 145, 0, 0, 235, 21, 1, 0, 0, 0, 236, 237, 5, 29, 0, 0, 237, 238, 3, 150, 75, 0, 238, 240, 5, 126, 0, 0, 239, 241, 3, 10, 5, 0, 240, 239, 1, 0, 0, 0, 240, 241, 1, 0, 0, 0, 241, 242, 1, 0, 0, 0, 242, 243, 5, 144, 0, 0, 243, 244, 3, 26, 13, 0, 244, 23, 1, 0, 0, 0, 245, 246, 5, 145, 0, 0, 246, 25, 1, 0, 0, 0, 247, 251, 5, 124, 0, 0, 248, 250, 3, 2, 1, 0, 249, 248, 1, 0, 0, 0, 250, 253, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 252, 1, 0, 0, 0, 252, 254, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 254, 255, 5, 142, 0, 0, 255, 27, 1, 0, 0, 0, 256, 257, 3, 4, 2, 0, 257, 258, 5, 111, 0, 0, 258, 259, 3, 4, 2, 0, 259, 29, 1, 0, 0, 0, 260, 265, 3, 28, 14, 0, 261, 262, 5, 112, 0, 0, 262, 264, 3, 28, 14, 0, 263, 261, 1, 0, 0, 0, 264, 267, 1, 0, 0, 0, 265, 263, 1, 0, 0, 0, 265, 266, 1, 0, 0, 0, 266, 31, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 268, 272, 3, 34, 17, 0, 269, 272, 3, 38, 19, 0, 270, 272, 3, 114, 57, 0, 271, 268, 1, 0, 0, 0, 271, 269, 1, 0, 0, 0, 271, 270, 1, 0, 0, 0, 272, 273, 1, 0, 0, 0, 273, 274, 5, 0, 0, 1, 274, 33, 1, 0, 0, 0, 275, 281, 3, 36, 18, 0, 276, 277, 5, 91, 0, 0, 277, 278, 5, 1, 0, 0, 278, 280, 3, 36, 18, 0, 279, 276, 1, 0, 0, 0, 280, 283, 1, 0, 0, 0, 281, 279, 1, 0, 0, 0, 281, 282, 1, 0, 0, 0, 282, 35, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 284, 291, 3, 38, 19, 0, 285, 286, 5, 126, 0, 0, 286, 287, 3, 34, 17, 0, 287, 288, 5, 144, 0, 0, 288, 291, 1, 0, 0, 0, 289, 291, 3, 154, 77, 0, 290, 284, 1, 0, 0, 0, 290, 285, 1, 0, 0, 0, 290, 289, 1, 0, 0, 0, 291, 37, 1, 0, 0, 0, 292, 294, 3, 40, 20, 0, 293, 292, 1, 0, 0, 0, 293, 294, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 297, 5, 77, 0, 0, 296, 298, 5, 23, 0, 0, 297, 296, 1, 0, 0, 0, 297, 298, 1, 0, 0, 0, 298, 300, 1, 0, 0, 0, 299, 301, 3, 42, 21, 0, 300, 299, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 304, 3, 104, 52, 0, 303, 305, 3, 44, 22, 0, 304, 303, 1, 0, 0, 0, 304, 305, 1, 0, 0, 0, 305, 307, 1, 0, 0, 0, 306, 308, 3, 46, 23, 0, 307, 306, 1, 0, 0, 0, 307, 308, 1, 0, 0, 0, 308, 310, 1, 0, 0, 0, 309, 311, 3, 50, 25, 0, 310, 309, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 313, 1, 0, 0, 0, 312, 314, 3, 52, 26, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 316, 1, 0, 0, 0, 315, 317, 3, 54, 27, 0, 316, 315, 1, 0, 0, 0, 316, 317, 1, 0, 0, 0, 317, 320, 1, 0, 0, 0, 318, 319, 5, 98, 0, 0, 319, 321, 7, 0, 0, 0, 320, 318, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 324, 1, 0, 0, 0, 322, 323, 5, 98, 0, 0, 323, 325, 5, 86, 0, 0, 324, 322, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 327, 1, 0, 0, 0, 326, 328, 3, 56, 28, 0, 327, 326, 1, 0, 0, 0, 327, 328, 1, 0, 0, 0, 328, 330, 1, 0, 0, 0, 329, 331, 3, 48, 24, 0, 330, 329, 1, 0, 0, 0, 330, 331, 1, 0, 0, 0, 331, 333, 1, 0, 0, 0, 332, 334, 3, 58, 29, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 337, 1, 0, 0, 0, 335, 338, 3, 62, 31, 0, 336, 338, 3, 64, 32, 0, 337, 335, 1, 0, 0, 0, 337, 336, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 340, 1, 0, 0, 0, 339, 341, 3, 66, 33, 0, 340, 339, 1, 0, 0, 0, 340, 341, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 343, 5, 98, 0, 0, 343, 344, 3, 118, 59, 0, 344, 41, 1, 0, 0, 0, 345, 346, 5, 85, 0, 0, 346, 349, 5, 104, 0, 0, 347, 348, 5, 98, 0, 0, 348, 350, 5, 82, 0, 0, 349, 347, 1, 0, 0, 0, 349, 350, 1, 0, 0, 0, 350, 43, 1, 0, 0, 0, 351, 352, 5, 32, 0, 0, 352, 353, 3, 68, 34, 0, 353, 45, 1, 0, 0, 0, 354, 356, 7, 1, 0, 0, 355, 354, 1, 0, 0, 0, 355, 356, 1, 0, 0, 0, 356, 357, 1, 0, 0, 0, 357, 358, 5, 5, 0, 0, 358, 359, 5, 45, 0, 0, 359, 360, 3, 104, 52, 0, 360, 47, 1, 0, 0, 0, 361, 362, 5, 97, 0, 0, 362, 363, 3, 150, 75, 0, 363, 364, 5, 6, 0, 0, 364, 365, 5, 126, 0, 0, 365, 366, 3, 88, 44, 0, 366, 376, 5, 144, 0, 0, 367, 368, 5, 112, 0, 0, 368, 369, 3, 150, 75, 0, 369, 370, 5, 6, 0, 0, 370, 371, 5, 126, 0, 0, 371, 372, 3, 88, 44, 0, 372, 373, 5, 144, 0, 0, 373, 375, 1, 0, 0, 0, 374, 367, 1, 0, 0, 0, 375, 378, 1, 0, 0, 0, 376, 374, 1, 0, 0, 0, 376, 377, 1, 0, 0, 0, 377, 49, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 379, 380, 5, 67, 0, 0, 380, 381, 3, 106, 53, 0, 381, 51, 1, 0, 0, 0, 382, 383, 5, 95, 0, 0, 383, 384, 3, 106, 53, 0, 384, 53, 1, 0, 0, 0, 385, 386, 5, 34, 0, 0, 386, 393, 5, 11, 0, 0, 387, 388, 7, 0, 0, 0, 388, 389, 5, 126, 0, 0, 389, 390, 3, 104, 52, 0, 390, 391, 5, 144, 0, 0, 391, 394, 1, 0, 0, 0, 392, 394, 3, 104, 52, 0, 393, 387, 1, 0, 0, 0, 393, 392, 1, 0, 0, 0, 394, 55, 1, 0, 0, 0, 395, 396, 5, 35, 0, 0, 396, 397, 3, 106, 53, 0, 397, 57, 1, 0, 0, 0, 398, 399, 5, 62, 0, 0, 399, 400, 5, 11, 0, 0, 400, 401, 3, 78, 39, 0, 401, 59, 1, 0, 0, 0, 402, 403, 5, 62, 0, 0, 403, 404, 5, 11, 0, 0, 404, 405, 3, 104, 52, 0, 405, 61, 1, 0, 0, 0, 406, 407, 5, 52, 0, 0, 407, 410, 3, 106, 53, 0, 408, 409, 5, 112, 0, 0, 409, 411, 3, 106, 53, 0, 410, 408, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 416, 1, 0, 0, 0, 412, 413, 5, 98, 0, 0, 413, 417, 5, 82, 0, 0, 414, 415, 5, 11, 0, 0, 415, 417, 3, 104, 52, 0, 416, 412, 1, 0, 0, 0, 416, 414, 1, 0, 0, 0, 416, 417, 1, 0, 0, 0, 417, 436, 1, 0, 0, 0, 418, 419, 5, 52, 0, 0, 419, 422, 3, 106, 53, 0, 420, 421, 5, 98, 0, 0, 421, 423, 5, 82, 0, 0, 422, 420, 1, 0, 0, 0, 422, 423, 1, 0, 0, 0, 423, 424, 1, 0, 0, 0, 424, 425, 5, 59, 0, 0, 425, 426, 3, 106, 53, 0, 426, 436, 1, 0, 0, 0, 427, 428, 5, 52, 0, 0, 428, 429, 3, 106, 53, 0, 429, 430, 5, 59, 0, 0, 430, 433, 3, 106, 53, 0, 431, 432, 5, 11, 0, 0, 432, 434, 3, 104, 52, 0, 433, 431, 1, 0, 0, 0, 433, 434, 1, 0, 0, 0, 434, 436, 1, 0, 0, 0, 435, 406, 1, 0, 0, 0, 435, 418, 1, 0, 0, 0, 435, 427, 1, 0, 0, 0, 436, 63, 1, 0, 0, 0, 437, 438, 5, 59, 0, 0, 438, 439, 3, 106, 53, 0, 439, 65, 1, 0, 0, 0, 440, 441, 5, 79, 0, 0, 441, 442, 3, 84, 42, 0, 442, 67, 1, 0, 0, 0, 443, 444, 6, 34, -1, 0, 444, 446, 3, 126, 63, 0, 445, 447, 5, 27, 0, 0, 446, 445, 1, 0, 0, 0, 446, 447, 1, 0, 0, 0, 447, 449, 1, 0, 0, 0, 448, 450, 3, 76, 38, 0, 449, 448, 1, 0, 0, 0, 449, 450, 1, 0, 0, 0, 450, 456, 1, 0, 0, 0, 451, 452, 5, 126, 0, 0, 452, 453, 3, 68, 34, 0, 453, 454, 5, 144, 0, 0, 454, 456, 1, 0, 0, 0, 455, 443, 1, 0, 0, 0, 455, 451, 1, 0, 0, 0, 456, 471, 1, 0, 0, 0, 457, 458, 10, 3, 0, 0, 458, 459, 3, 72, 36, 0, 459, 460, 3, 68, 34, 4, 460, 470, 1, 0, 0, 0, 461, 463, 10, 4, 0, 0, 462, 464, 3, 70, 35, 0, 463, 462, 1, 0, 0, 0, 463, 464, 1, 0, 0, 0, 464, 465, 1, 0, 0, 0, 465, 466, 5, 45, 0, 0, 466, 467, 3, 68, 34, 0, 467, 468, 3, 74, 37, 0, 468, 470, 1, 0, 0, 0, 469, 457, 1, 0, 0, 0, 469, 461, 1, 0, 0, 0, 470, 473, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 471, 472, 1, 0, 0, 0, 472, 69, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 474, 476, 7, 2, 0, 0, 475, 474, 1, 0, 0, 0, 475, 476, 1, 0, 0, 0, 476, 477, 1, 0, 0, 0, 477, 484, 5, 42, 0, 0, 478, 480, 5, 42, 0, 0, 479, 481, 7, 2, 0, 0, 480, 479, 1, 0, 0, 0, 480, 481, 1, 0, 0, 0, 481, 484, 1, 0, 0, 0, 482, 484, 7, 2, 0, 0, 483, 475, 1, 0, 0, 0, 483, 478, 1, 0, 0, 0, 483, 482, 1, 0, 0, 0, 484, 518, 1, 0, 0, 0, 485, 487, 7, 3, 0, 0, 486, 485, 1, 0, 0, 0, 486, 487, 1, 0, 0, 0, 487, 488, 1, 0, 0, 0, 488, 490, 7, 4, 0, 0, 489, 491, 5, 63, 0, 0, 490, 489, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 500, 1, 0, 0, 0, 492, 494, 7, 4, 0, 0, 493, 495, 5, 63, 0, 0, 494, 493, 1, 0, 0, 0, 494, 495, 1, 0, 0, 0, 495, 497, 1, 0, 0, 0, 496, 498, 7, 3, 0, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 500, 1, 0, 0, 0, 499, 486, 1, 0, 0, 0, 499, 492, 1, 0, 0, 0, 500, 518, 1, 0, 0, 0, 501, 503, 7, 5, 0, 0, 502, 501, 1, 0, 0, 0, 502, 503, 1, 0, 0, 0, 503, 504, 1, 0, 0, 0, 504, 506, 5, 33, 0, 0, 505, 507, 5, 63, 0, 0, 506, 505, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 516, 1, 0, 0, 0, 508, 510, 5, 33, 0, 0, 509, 511, 5, 63, 0, 0, 510, 509, 1, 0, 0, 0, 510, 511, 1, 0, 0, 0, 511, 513, 1, 0, 0, 0, 512, 514, 7, 5, 0, 0, 513, 512, 1, 0, 0, 0, 513, 514, 1, 0, 0, 0, 514, 516, 1, 0, 0, 0, 515, 502, 1, 0, 0, 0, 515, 508, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 483, 1, 0, 0, 0, 517, 499, 1, 0, 0, 0, 517, 515, 1, 0, 0, 0, 518, 71, 1, 0, 0, 0, 519, 520, 5, 16, 0, 0, 520, 523, 5, 45, 0, 0, 521, 523, 5, 112, 0, 0, 522, 519, 1, 0, 0, 0, 522, 521, 1, 0, 0, 0, 523, 73, 1, 0, 0, 0, 524, 525, 5, 60, 0, 0, 525, 534, 3, 104, 52, 0, 526, 527, 5, 92, 0, 0, 527, 528, 5, 126, 0, 0, 528, 529, 3, 104, 52, 0, 529, 530, 5, 144, 0, 0, 530, 534, 1, 0, 0, 0, 531, 532, 5, 92, 0, 0, 532, 534, 3, 104, 52, 0, 533, 524, 1, 0, 0, 0, 533, 526, 1, 0, 0, 0, 533, 531, 1, 0, 0, 0, 534, 75, 1, 0, 0, 0, 535, 536, 5, 75, 0, 0, 536, 539, 3, 82, 41, 0, 537, 538, 5, 59, 0, 0, 538, 540, 3, 82, 41, 0, 539, 537, 1, 0, 0, 0, 539, 540, 1, 0, 0, 0, 540, 77, 1, 0, 0, 0, 541, 546, 3, 80, 40, 0, 542, 543, 5, 112, 0, 0, 543, 545, 3, 80, 40, 0, 544, 542, 1, 0, 0, 0, 545, 548, 1, 0, 0, 0, 546, 544, 1, 0, 0, 0, 546, 547, 1, 0, 0, 0, 547, 79, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 549, 551, 3, 106, 53, 0, 550, 552, 7, 6, 0, 0, 551, 550, 1, 0, 0, 0, 551, 552, 1, 0, 0, 0, 552, 555, 1, 0, 0, 0, 553, 554, 5, 58, 0, 0, 554, 556, 7, 7, 0, 0, 555, 553, 1, 0, 0, 0, 555, 556, 1, 0, 0, 0, 556, 559, 1, 0, 0, 0, 557, 558, 5, 15, 0, 0, 558, 560, 5, 106, 0, 0, 559, 557, 1, 0, 0, 0, 559, 560, 1, 0, 0, 0, 560, 81, 1, 0, 0, 0, 561, 568, 3, 154, 77, 0, 562, 565, 3, 138, 69, 0, 563, 564, 5, 146, 0, 0, 564, 566, 3, 138, 69, 0, 565, 563, 1, 0, 0, 0, 565, 566, 1, 0, 0, 0, 566, 568, 1, 0, 0, 0, 567, 561, 1, 0, 0, 0, 567, 562, 1, 0, 0, 0, 568, 83, 1, 0, 0, 0, 569, 574, 3, 86, 43, 0, 570, 571, 5, 112, 0, 0, 571, 573, 3, 86, 43, 0, 572, 570, 1, 0, 0, 0, 573, 576, 1, 0, 0, 0, 574, 572, 1, 0, 0, 0, 574, 575, 1, 0, 0, 0, 575, 85, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 577, 578, 3, 150, 75, 0, 578, 579, 5, 118, 0, 0, 579, 580, 3, 140, 70, 0, 580, 87, 1, 0, 0, 0, 581, 583, 3, 90, 45, 0, 582, 581, 1, 0, 0, 0, 582, 583, 1, 0, 0, 0, 583, 585, 1, 0, 0, 0, 584, 586, 3, 92, 46, 0, 585, 584, 1, 0, 0, 0, 585, 586, 1, 0, 0, 0, 586, 588, 1, 0, 0, 0, 587, 589, 3, 94, 47, 0, 588, 587, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 89, 1, 0, 0, 0, 590, 591, 5, 65, 0, 0, 591, 592, 5, 11, 0, 0, 592, 593, 3, 104, 52, 0, 593, 91, 1, 0, 0, 0, 594, 595, 5, 62, 0, 0, 595, 596, 5, 11, 0, 0, 596, 597, 3, 78, 39, 0, 597, 93, 1, 0, 0, 0, 598, 599, 7, 8, 0, 0, 599, 600, 3, 96, 48, 0, 600, 95, 1, 0, 0, 0, 601, 608, 3, 98, 49, 0, 602, 603, 5, 9, 0, 0, 603, 604, 3, 98, 49, 0, 604, 605, 5, 2, 0, 0, 605, 606, 3, 98, 49, 0, 606, 608, 1, 0, 0, 0, 607, 601, 1, 0, 0, 0, 607, 602, 1, 0, 0, 0, 608, 97, 1, 0, 0, 0, 609, 610, 5, 18, 0, 0, 610, 622, 5, 73, 0, 0, 611, 612, 5, 90, 0, 0, 612, 622, 5, 66, 0, 0, 613, 614, 5, 90, 0, 0, 614, 622, 5, 30, 0, 0, 615, 616, 3, 138, 69, 0, 616, 617, 5, 66, 0, 0, 617, 622, 1, 0, 0, 0, 618, 619, 3, 138, 69, 0, 619, 620, 5, 30, 0, 0, 620, 622, 1, 0, 0, 0, 621, 609, 1, 0, 0, 0, 621, 611, 1, 0, 0, 0, 621, 613, 1, 0, 0, 0, 621, 615, 1, 0, 0, 0, 621, 618, 1, 0, 0, 0, 622, 99, 1, 0, 0, 0, 623, 624, 3, 106, 53, 0, 624, 625, 5, 0, 0, 1, 625, 101, 1, 0, 0, 0, 626, 674, 3, 150, 75, 0, 627, 628, 3, 150, 75, 0, 628, 629, 5, 126, 0, 0, 629, 630, 3, 150, 75, 0, 630, 637, 3, 102, 51, 0, 631, 632, 5, 112, 0, 0, 632, 633, 3, 150, 75, 0, 633, 634, 3, 102, 51, 0, 634, 636, 1, 0, 0, 0, 635, 631, 1, 0, 0, 0, 636, 639, 1, 0, 0, 0, 637, 635, 1, 0, 0, 0, 637, 638, 1, 0, 0, 0, 638, 640, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 640, 641, 5, 144, 0, 0, 641, 674, 1, 0, 0, 0, 642, 643, 3, 150, 75, 0, 643, 644, 5, 126, 0, 0, 644, 649, 3, 152, 76, 0, 645, 646, 5, 112, 0, 0, 646, 648, 3, 152, 76, 0, 647, 645, 1, 0, 0, 0, 648, 651, 1, 0, 0, 0, 649, 647, 1, 0, 0, 0, 649, 650, 1, 0, 0, 0, 650, 652, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 652, 653, 5, 144, 0, 0, 653, 674, 1, 0, 0, 0, 654, 655, 3, 150, 75, 0, 655, 656, 5, 126, 0, 0, 656, 661, 3, 102, 51, 0, 657, 658, 5, 112, 0, 0, 658, 660, 3, 102, 51, 0, 659, 657, 1, 0, 0, 0, 660, 663, 1, 0, 0, 0, 661, 659, 1, 0, 0, 0, 661, 662, 1, 0, 0, 0, 662, 664, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 664, 665, 5, 144, 0, 0, 665, 674, 1, 0, 0, 0, 666, 667, 3, 150, 75, 0, 667, 669, 5, 126, 0, 0, 668, 670, 3, 104, 52, 0, 669, 668, 1, 0, 0, 0, 669, 670, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 672, 5, 144, 0, 0, 672, 674, 1, 0, 0, 0, 673, 626, 1, 0, 0, 0, 673, 627, 1, 0, 0, 0, 673, 642, 1, 0, 0, 0, 673, 654, 1, 0, 0, 0, 673, 666, 1, 0, 0, 0, 674, 103, 1, 0, 0, 0, 675, 680, 3, 106, 53, 0, 676, 677, 5, 112, 0, 0, 677, 679, 3, 106, 53, 0, 678, 676, 1, 0, 0, 0, 679, 682, 1, 0, 0, 0, 680, 678, 1, 0, 0, 0, 680, 681, 1, 0, 0, 0, 681, 105, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 683, 684, 6, 53, -1, 0, 684, 686, 5, 12, 0, 0, 685, 687, 3, 106, 53, 0, 686, 685, 1, 0, 0, 0, 686, 687, 1, 0, 0, 0, 687, 693, 1, 0, 0, 0, 688, 689, 5, 94, 0, 0, 689, 690, 3, 106, 53, 0, 690, 691, 5, 81, 0, 0, 691, 692, 3, 106, 53, 0, 692, 694, 1, 0, 0, 0, 693, 688, 1, 0, 0, 0, 694, 695, 1, 0, 0, 0, 695, 693, 1, 0, 0, 0, 695, 696, 1, 0, 0, 0, 696, 699, 1, 0, 0, 0, 697, 698, 5, 24, 0, 0, 698, 700, 3, 106, 53, 0, 699, 697, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 702, 5, 25, 0, 0, 702, 833, 1, 0, 0, 0, 703, 704, 5, 13, 0, 0, 704, 705, 5, 126, 0, 0, 705, 706, 3, 106, 53, 0, 706, 707, 5, 6, 0, 0, 707, 708, 3, 102, 51, 0, 708, 709, 5, 144, 0, 0, 709, 833, 1, 0, 0, 0, 710, 711, 5, 19, 0, 0, 711, 833, 5, 106, 0, 0, 712, 713, 5, 43, 0, 0, 713, 714, 3, 106, 53, 0, 714, 715, 3, 142, 71, 0, 715, 833, 1, 0, 0, 0, 716, 717, 5, 80, 0, 0, 717, 718, 5, 126, 0, 0, 718, 719, 3, 106, 53, 0, 719, 720, 5, 32, 0, 0, 720, 723, 3, 106, 53, 0, 721, 722, 5, 31, 0, 0, 722, 724, 3, 106, 53, 0, 723, 721, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 726, 5, 144, 0, 0, 726, 833, 1, 0, 0, 0, 727, 728, 5, 83, 0, 0, 728, 833, 5, 106, 0, 0, 729, 730, 5, 88, 0, 0, 730, 731, 5, 126, 0, 0, 731, 732, 7, 9, 0, 0, 732, 733, 3, 156, 78, 0, 733, 734, 5, 32, 0, 0, 734, 735, 3, 106, 53, 0, 735, 736, 5, 144, 0, 0, 736, 833, 1, 0, 0, 0, 737, 738, 3, 150, 75, 0, 738, 740, 5, 126, 0, 0, 739, 741, 3, 104, 52, 0, 740, 739, 1, 0, 0, 0, 740, 741, 1, 0, 0, 0, 741, 742, 1, 0, 0, 0, 742, 743, 5, 144, 0, 0, 743, 752, 1, 0, 0, 0, 744, 746, 5, 126, 0, 0, 745, 747, 5, 23, 0, 0, 746, 745, 1, 0, 0, 0, 746, 747, 1, 0, 0, 0, 747, 749, 1, 0, 0, 0, 748, 750, 3, 108, 54, 0, 749, 748, 1, 0, 0, 0, 749, 750, 1, 0, 0, 0, 750, 751, 1, 0, 0, 0, 751, 753, 5, 144, 0, 0, 752, 744, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 754, 1, 0, 0, 0, 754, 755, 5, 64, 0, 0, 755, 756, 5, 126, 0, 0, 756, 757, 3, 88, 44, 0, 757, 758, 5, 144, 0, 0, 758, 833, 1, 0, 0, 0, 759, 760, 3, 150, 75, 0, 760, 762, 5, 126, 0, 0, 761, 763, 3, 104, 52, 0, 762, 761, 1, 0, 0, 0, 762, 763, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 765, 5, 144, 0, 0, 765, 774, 1, 0, 0, 0, 766, 768, 5, 126, 0, 0, 767, 769, 5, 23, 0, 0, 768, 767, 1, 0, 0, 0, 768, 769, 1, 0, 0, 0, 769, 771, 1, 0, 0, 0, 770, 772, 3, 108, 54, 0, 771, 770, 1, 0, 0, 0, 771, 772, 1, 0, 0, 0, 772, 773, 1, 0, 0, 0, 773, 775, 5, 144, 0, 0, 774, 766, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 776, 1, 0, 0, 0, 776, 777, 5, 64, 0, 0, 777, 778, 3, 150, 75, 0, 778, 833, 1, 0, 0, 0, 779, 785, 3, 150, 75, 0, 780, 782, 5, 126, 0, 0, 781, 783, 3, 104, 52, 0, 782, 781, 1, 0, 0, 0, 782, 783, 1, 0, 0, 0, 783, 784, 1, 0, 0, 0, 784, 786, 5, 144, 0, 0, 785, 780, 1, 0, 0, 0, 785, 786, 1, 0, 0, 0, 786, 787, 1, 0, 0, 0, 787, 789, 5, 126, 0, 0, 788, 790, 5, 23, 0, 0, 789, 788, 1, 0, 0, 0, 789, 790, 1, 0, 0, 0, 790, 792, 1, 0, 0, 0, 791, 793, 3, 108, 54, 0, 792, 791, 1, 0, 0, 0, 792, 793, 1, 0, 0, 0, 793, 794, 1, 0, 0, 0, 794, 795, 5, 144, 0, 0, 795, 833, 1, 0, 0, 0, 796, 833, 3, 114, 57, 0, 797, 833, 3, 158, 79, 0, 798, 833, 3, 140, 70, 0, 799, 800, 5, 114, 0, 0, 800, 833, 3, 106, 53, 19, 801, 802, 5, 56, 0, 0, 802, 833, 3, 106, 53, 13, 803, 804, 3, 130, 65, 0, 804, 805, 5, 116, 0, 0, 805, 807, 1, 0, 0, 0, 806, 803, 1, 0, 0, 0, 806, 807, 1, 0, 0, 0, 807, 808, 1, 0, 0, 0, 808, 833, 5, 108, 0, 0, 809, 810, 5, 126, 0, 0, 810, 811, 3, 34, 17, 0, 811, 812, 5, 144, 0, 0, 812, 833, 1, 0, 0, 0, 813, 814, 5, 126, 0, 0, 814, 815, 3, 106, 53, 0, 815, 816, 5, 144, 0, 0, 816, 833, 1, 0, 0, 0, 817, 818, 5, 126, 0, 0, 818, 819, 3, 104, 52, 0, 819, 820, 5, 144, 0, 0, 820, 833, 1, 0, 0, 0, 821, 823, 5, 125, 0, 0, 822, 824, 3, 104, 52, 0, 823, 822, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 825, 1, 0, 0, 0, 825, 833, 5, 143, 0, 0, 826, 828, 5, 124, 0, 0, 827, 829, 3, 30, 15, 0, 828, 827, 1, 0, 0, 0, 828, 829, 1, 0, 0, 0, 829, 830, 1, 0, 0, 0, 830, 833, 5, 142, 0, 0, 831, 833, 3, 122, 61, 0, 832, 683, 1, 0, 0, 0, 832, 703, 1, 0, 0, 0, 832, 710, 1, 0, 0, 0, 832, 712, 1, 0, 0, 0, 832, 716, 1, 0, 0, 0, 832, 727, 1, 0, 0, 0, 832, 729, 1, 0, 0, 0, 832, 737, 1, 0, 0, 0, 832, 759, 1, 0, 0, 0, 832, 779, 1, 0, 0, 0, 832, 796, 1, 0, 0, 0, 832, 797, 1, 0, 0, 0, 832, 798, 1, 0, 0, 0, 832, 799, 1, 0, 0, 0, 832, 801, 1, 0, 0, 0, 832, 806, 1, 0, 0, 0, 832, 809, 1, 0, 0, 0, 832, 813, 1, 0, 0, 0, 832, 817, 1, 0, 0, 0, 832, 821, 1, 0, 0, 0, 832, 826, 1, 0, 0, 0, 832, 831, 1, 0, 0, 0, 833, 927, 1, 0, 0, 0, 834, 838, 10, 18, 0, 0, 835, 839, 5, 108, 0, 0, 836, 839, 5, 146, 0, 0, 837, 839, 5, 133, 0, 0, 838, 835, 1, 0, 0, 0, 838, 836, 1, 0, 0, 0, 838, 837, 1, 0, 0, 0, 839, 840, 1, 0, 0, 0, 840, 926, 3, 106, 53, 19, 841, 845, 10, 17, 0, 0, 842, 846, 5, 134, 0, 0, 843, 846, 5, 114, 0, 0, 844, 846, 5, 113, 0, 0, 845, 842, 1, 0, 0, 0, 845, 843, 1, 0, 0, 0, 845, 844, 1, 0, 0, 0, 846, 847, 1, 0, 0, 0, 847, 926, 3, 106, 53, 18, 848, 873, 10, 16, 0, 0, 849, 874, 5, 117, 0, 0, 850, 874, 5, 118, 0, 0, 851, 874, 5, 129, 0, 0, 852, 874, 5, 127, 0, 0, 853, 874, 5, 128, 0, 0, 854, 874, 5, 119, 0, 0, 855, 874, 5, 120, 0, 0, 856, 858, 5, 56, 0, 0, 857, 856, 1, 0, 0, 0, 857, 858, 1, 0, 0, 0, 858, 859, 1, 0, 0, 0, 859, 861, 5, 40, 0, 0, 860, 862, 5, 14, 0, 0, 861, 860, 1, 0, 0, 0, 861, 862, 1, 0, 0, 0, 862, 874, 1, 0, 0, 0, 863, 865, 5, 56, 0, 0, 864, 863, 1, 0, 0, 0, 864, 865, 1, 0, 0, 0, 865, 866, 1, 0, 0, 0, 866, 874, 7, 10, 0, 0, 867, 874, 5, 140, 0, 0, 868, 874, 5, 141, 0, 0, 869, 874, 5, 131, 0, 0, 870, 874, 5, 122, 0, 0, 871, 874, 5, 123, 0, 0, 872, 874, 5, 130, 0, 0, 873, 849, 1, 0, 0, 0, 873, 850, 1, 0, 0, 0, 873, 851, 1, 0, 0, 0, 873, 852, 1, 0, 0, 0, 873, 853, 1, 0, 0, 0, 873, 854, 1, 0, 0, 0, 873, 855, 1, 0, 0, 0, 873, 857, 1, 0, 0, 0, 873, 864, 1, 0, 0, 0, 873, 867, 1, 0, 0, 0, 873, 868, 1, 0, 0, 0, 873, 869, 1, 0, 0, 0, 873, 870, 1, 0, 0, 0, 873, 871, 1, 0, 0, 0, 873, 872, 1, 0, 0, 0, 874, 875, 1, 0, 0, 0, 875, 926, 3, 106, 53, 17, 876, 877, 10, 14, 0, 0, 877, 878, 5, 132, 0, 0, 878, 926, 3, 106, 53, 15, 879, 880, 10, 12, 0, 0, 880, 881, 5, 2, 0, 0, 881, 926, 3, 106, 53, 13, 882, 883, 10, 11, 0, 0, 883, 884, 5, 61, 0, 0, 884, 926, 3, 106, 53, 12, 885, 887, 10, 10, 0, 0, 886, 888, 5, 56, 0, 0, 887, 886, 1, 0, 0, 0, 887, 888, 1, 0, 0, 0, 888, 889, 1, 0, 0, 0, 889, 890, 5, 9, 0, 0, 890, 891, 3, 106, 53, 0, 891, 892, 5, 2, 0, 0, 892, 893, 3, 106, 53, 11, 893, 926, 1, 0, 0, 0, 894, 895, 10, 9, 0, 0, 895, 896, 5, 135, 0, 0, 896, 897, 3, 106, 53, 0, 897, 898, 5, 111, 0, 0, 898, 899, 3, 106, 53, 9, 899, 926, 1, 0, 0, 0, 900, 901, 10, 22, 0, 0, 901, 902, 5, 125, 0, 0, 902, 903, 3, 106, 53, 0, 903, 904, 5, 143, 0, 0, 904, 926, 1, 0, 0, 0, 905, 906, 10, 21, 0, 0, 906, 907, 5, 116, 0, 0, 907, 926, 5, 104, 0, 0, 908, 909, 10, 20, 0, 0, 909, 910, 5, 116, 0, 0, 910, 926, 3, 150, 75, 0, 911, 912, 10, 15, 0, 0, 912, 914, 5, 44, 0, 0, 913, 915, 5, 56, 0, 0, 914, 913, 1, 0, 0, 0, 914, 915, 1, 0, 0, 0, 915, 916, 1, 0, 0, 0, 916, 926, 5, 57, 0, 0, 917, 923, 10, 8, 0, 0, 918, 924, 3, 148, 74, 0, 919, 920, 5, 6, 0, 0, 920, 924, 3, 150, 75, 0, 921, 922, 5, 6, 0, 0, 922, 924, 5, 106, 0, 0, 923, 918, 1, 0, 0, 0, 923, 919, 1, 0, 0, 0, 923, 921, 1, 0, 0, 0, 924, 926, 1, 0, 0, 0, 925, 834, 1, 0, 0, 0, 925, 841, 1, 0, 0, 0, 925, 848, 1, 0, 0, 0, 925, 876, 1, 0, 0, 0, 925, 879, 1, 0, 0, 0, 925, 882, 1, 0, 0, 0, 925, 885, 1, 0, 0, 0, 925, 894, 1, 0, 0, 0, 925, 900, 1, 0, 0, 0, 925, 905, 1, 0, 0, 0, 925, 908, 1, 0, 0, 0, 925, 911, 1, 0, 0, 0, 925, 917, 1, 0, 0, 0, 926, 929, 1, 0, 0, 0, 927, 925, 1, 0, 0, 0, 927, 928, 1, 0, 0, 0, 928, 107, 1, 0, 0, 0, 929, 927, 1, 0, 0, 0, 930, 935, 3, 110, 55, 0, 931, 932, 5, 112, 0, 0, 932, 934, 3, 110, 55, 0, 933, 931, 1, 0, 0, 0, 934, 937, 1, 0, 0, 0, 935, 933, 1, 0, 0, 0, 935, 936, 1, 0, 0, 0, 936, 109, 1, 0, 0, 0, 937, 935, 1, 0, 0, 0, 938, 941, 3, 112, 56, 0, 939, 941, 3, 106, 53, 0, 940, 938, 1, 0, 0, 0, 940, 939, 1, 0, 0, 0, 941, 111, 1, 0, 0, 0, 942, 943, 5, 126, 0, 0, 943, 948, 3, 150, 75, 0, 944, 945, 5, 112, 0, 0, 945, 947, 3, 150, 75, 0, 946, 944, 1, 0, 0, 0, 947, 950, 1, 0, 0, 0, 948, 946, 1, 0, 0, 0, 948, 949, 1, 0, 0, 0, 949, 951, 1, 0, 0, 0, 950, 948, 1, 0, 0, 0, 951, 952, 5, 144, 0, 0, 952, 962, 1, 0, 0, 0, 953, 958, 3, 150, 75, 0, 954, 955, 5, 112, 0, 0, 955, 957, 3, 150, 75, 0, 956, 954, 1, 0, 0, 0, 957, 960, 1, 0, 0, 0, 958, 956, 1, 0, 0, 0, 958, 959, 1, 0, 0, 0, 959, 962, 1, 0, 0, 0, 960, 958, 1, 0, 0, 0, 961, 942, 1, 0, 0, 0, 961, 953, 1, 0, 0, 0, 962, 963, 1, 0, 0, 0, 963, 964, 5, 107, 0, 0, 964, 965, 3, 106, 53, 0, 965, 113, 1, 0, 0, 0, 966, 967, 5, 128, 0, 0, 967, 971, 3, 150, 75, 0, 968, 970, 3, 116, 58, 0, 969, 968, 1, 0, 0, 0, 970, 973, 1, 0, 0, 0, 971, 969, 1, 0, 0, 0, 971, 972, 1, 0, 0, 0, 972, 974, 1, 0, 0, 0, 973, 971, 1, 0, 0, 0, 974, 975, 5, 146, 0, 0, 975, 976, 5, 120, 0, 0, 976, 995, 1, 0, 0, 0, 977, 978, 5, 128, 0, 0, 978, 982, 3, 150, 75, 0, 979, 981, 3, 116, 58, 0, 980, 979, 1, 0, 0, 0, 981, 984, 1, 0, 0, 0, 982, 980, 1, 0, 0, 0, 982, 983, 1, 0, 0, 0, 983, 985, 1, 0, 0, 0, 984, 982, 1, 0, 0, 0, 985, 987, 5, 120, 0, 0, 986, 988, 3, 114, 57, 0, 987, 986, 1, 0, 0, 0, 987, 988, 1, 0, 0, 0, 988, 989, 1, 0, 0, 0, 989, 990, 5, 128, 0, 0, 990, 991, 5, 146, 0, 0, 991, 992, 3, 150, 75, 0, 992, 993, 5, 120, 0, 0, 993, 995, 1, 0, 0, 0, 994, 966, 1, 0, 0, 0, 994, 977, 1, 0, 0, 0, 995, 115, 1, 0, 0, 0, 996, 997, 3, 150, 75, 0, 997, 998, 5, 118, 0, 0, 998, 999, 3, 156, 78, 0, 999, 1008, 1, 0, 0, 0, 1000, 1001, 3, 150, 75, 0, 1001, 1002, 5, 118, 0, 0, 1002, 1003, 5, 124, 0, 0, 1003, 1004, 3, 106, 53, 0, 1004, 1005, 5, 142, 0, 0, 1005, 1008, 1, 0, 0, 0, 1006, 1008, 3, 150, 75, 0, 1007, 996, 1, 0, 0, 0, 1007, 1000, 1, 0, 0, 0, 1007, 1006, 1, 0, 0, 0, 1008, 117, 1, 0, 0, 0, 1009, 1014, 3, 120, 60, 0, 1010, 1011, 5, 112, 0, 0, 1011, 1013, 3, 120, 60, 0, 1012, 1010, 1, 0, 0, 0, 1013, 1016, 1, 0, 0, 0, 1014, 1012, 1, 0, 0, 0, 1014, 1015, 1, 0, 0, 0, 1015, 119, 1, 0, 0, 0, 1016, 1014, 1, 0, 0, 0, 1017, 1018, 3, 150, 75, 0, 1018, 1019, 5, 6, 0, 0, 1019, 1020, 5, 126, 0, 0, 1020, 1021, 3, 34, 17, 0, 1021, 1022, 5, 144, 0, 0, 1022, 1028, 1, 0, 0, 0, 1023, 1024, 3, 106, 53, 0, 1024, 1025, 5, 6, 0, 0, 1025, 1026, 3, 150, 75, 0, 1026, 1028, 1, 0, 0, 0, 1027, 1017, 1, 0, 0, 0, 1027, 1023, 1, 0, 0, 0, 1028, 121, 1, 0, 0, 0, 1029, 1037, 3, 154, 77, 0, 1030, 1031, 3, 130, 65, 0, 1031, 1032, 5, 116, 0, 0, 1032, 1034, 1, 0, 0, 0, 1033, 1030, 1, 0, 0, 0, 1033, 1034, 1, 0, 0, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1037, 3, 124, 62, 0, 1036, 1029, 1, 0, 0, 0, 1036, 1033, 1, 0, 0, 0, 1037, 123, 1, 0, 0, 0, 1038, 1043, 3, 150, 75, 0, 1039, 1040, 5, 116, 0, 0, 1040, 1042, 3, 150, 75, 0, 1041, 1039, 1, 0, 0, 0, 1042, 1045, 1, 0, 0, 0, 1043, 1041, 1, 0, 0, 0, 1043, 1044, 1, 0, 0, 0, 1044, 125, 1, 0, 0, 0, 1045, 1043, 1, 0, 0, 0, 1046, 1047, 6, 63, -1, 0, 1047, 1056, 3, 130, 65, 0, 1048, 1056, 3, 128, 64, 0, 1049, 1050, 5, 126, 0, 0, 1050, 1051, 3, 34, 17, 0, 1051, 1052, 5, 144, 0, 0, 1052, 1056, 1, 0, 0, 0, 1053, 1056, 3, 114, 57, 0, 1054, 1056, 3, 154, 77, 0, 1055, 1046, 1, 0, 0, 0, 1055, 1048, 1, 0, 0, 0, 1055, 1049, 1, 0, 0, 0, 1055, 1053, 1, 0, 0, 0, 1055, 1054, 1, 0, 0, 0, 1056, 1065, 1, 0, 0, 0, 1057, 1061, 10, 3, 0, 0, 1058, 1062, 3, 148, 74, 0, 1059, 1060, 5, 6, 0, 0, 1060, 1062, 3, 150, 75, 0, 1061, 1058, 1, 0, 0, 0, 1061, 1059, 1, 0, 0, 0, 1062, 1064, 1, 0, 0, 0, 1063, 1057, 1, 0, 0, 0, 1064, 1067, 1, 0, 0, 0, 1065, 1063, 1, 0, 0, 0, 1065, 1066, 1, 0, 0, 0, 1066, 127, 1, 0, 0, 0, 1067, 1065, 1, 0, 0, 0, 1068, 1069, 3, 150, 75, 0, 1069, 1071, 5, 126, 0, 0, 1070, 1072, 3, 132, 66, 0, 1071, 1070, 1, 0, 0, 0, 1071, 1072, 1, 0, 0, 0, 1072, 1073, 1, 0, 0, 0, 1073, 1074, 5, 144, 0, 0, 1074, 129, 1, 0, 0, 0, 1075, 1076, 3, 134, 67, 0, 1076, 1077, 5, 116, 0, 0, 1077, 1079, 1, 0, 0, 0, 1078, 1075, 1, 0, 0, 0, 1078, 1079, 1, 0, 0, 0, 1079, 1080, 1, 0, 0, 0, 1080, 1081, 3, 150, 75, 0, 1081, 131, 1, 0, 0, 0, 1082, 1087, 3, 106, 53, 0, 1083, 1084, 5, 112, 0, 0, 1084, 1086, 3, 106, 53, 0, 1085, 1083, 1, 0, 0, 0, 1086, 1089, 1, 0, 0, 0, 1087, 1085, 1, 0, 0, 0, 1087, 1088, 1, 0, 0, 0, 1088, 133, 1, 0, 0, 0, 1089, 1087, 1, 0, 0, 0, 1090, 1091, 3, 150, 75, 0, 1091, 135, 1, 0, 0, 0, 1092, 1101, 5, 102, 0, 0, 1093, 1094, 5, 116, 0, 0, 1094, 1101, 7, 11, 0, 0, 1095, 1096, 5, 104, 0, 0, 1096, 1098, 5, 116, 0, 0, 1097, 1099, 7, 11, 0, 0, 1098, 1097, 1, 0, 0, 0, 1098, 1099, 1, 0, 0, 0, 1099, 1101, 1, 0, 0, 0, 1100, 1092, 1, 0, 0, 0, 1100, 1093, 1, 0, 0, 0, 1100, 1095, 1, 0, 0, 0, 1101, 137, 1, 0, 0, 0, 1102, 1104, 7, 12, 0, 0, 1103, 1102, 1, 0, 0, 0, 1103, 1104, 1, 0, 0, 0, 1104, 1111, 1, 0, 0, 0, 1105, 1112, 3, 136, 68, 0, 1106, 1112, 5, 103, 0, 0, 1107, 1112, 5, 104, 0, 0, 1108, 1112, 5, 105, 0, 0, 1109, 1112, 5, 41, 0, 0, 1110, 1112, 5, 55, 0, 0, 1111, 1105, 1, 0, 0, 0, 1111, 1106, 1, 0, 0, 0, 1111, 1107, 1, 0, 0, 0, 1111, 1108, 1, 0, 0, 0, 1111, 1109, 1, 0, 0, 0, 1111, 1110, 1, 0, 0, 0, 1112, 139, 1, 0, 0, 0, 1113, 1117, 3, 138, 69, 0, 1114, 1117, 5, 106, 0, 0, 1115, 1117, 5, 57, 0, 0, 1116, 1113, 1, 0, 0, 0, 1116, 1114, 1, 0, 0, 0, 1116, 1115, 1, 0, 0, 0, 1117, 141, 1, 0, 0, 0, 1118, 1119, 7, 13, 0, 0, 1119, 143, 1, 0, 0, 0, 1120, 1121, 7, 14, 0, 0, 1121, 145, 1, 0, 0, 0, 1122, 1123, 7, 15, 0, 0, 1123, 147, 1, 0, 0, 0, 1124, 1127, 5, 101, 0, 0, 1125, 1127, 3, 146, 73, 0, 1126, 1124, 1, 0, 0, 0, 1126, 1125, 1, 0, 0, 0, 1127, 149, 1, 0, 0, 0, 1128, 1132, 5, 101, 0, 0, 1129, 1132, 3, 142, 71, 0, 1130, 1132, 3, 144, 72, 0, 1131, 1128, 1, 0, 0, 0, 1131, 1129, 1, 0, 0, 0, 1131, 1130, 1, 0, 0, 0, 1132, 151, 1, 0, 0, 0, 1133, 1134, 3, 156, 78, 0, 1134, 1135, 5, 118, 0, 0, 1135, 1136, 3, 138, 69, 0, 1136, 153, 1, 0, 0, 0, 1137, 1138, 5, 124, 0, 0, 1138, 1139, 3, 150, 75, 0, 1139, 1140, 5, 142, 0, 0, 1140, 155, 1, 0, 0, 0, 1141, 1144, 5, 106, 0, 0, 1142, 1144, 3, 158, 79, 0, 1143, 1141, 1, 0, 0, 0, 1143, 1142, 1, 0, 0, 0, 1144, 157, 1, 0, 0, 0, 1145, 1149, 5, 137, 0, 0, 1146, 1148, 3, 160, 80, 0, 1147, 1146, 1, 0, 0, 0, 1148, 1151, 1, 0, 0, 0, 1149, 1147, 1, 0, 0, 0, 1149, 1150, 1, 0, 0, 0, 1150, 1152, 1, 0, 0, 0, 1151, 1149, 1, 0, 0, 0, 1152, 1153, 5, 139, 0, 0, 1153, 159, 1, 0, 0, 0, 1154, 1155, 5, 152, 0, 0, 1155, 1156, 3, 106, 53, 0, 1156, 1157, 5, 142, 0, 0, 1157, 1160, 1, 0, 0, 0, 1158, 1160, 5, 151, 0, 0, 1159, 1154, 1, 0, 0, 0, 1159, 1158, 1, 0, 0, 0, 1160, 161, 1, 0, 0, 0, 1161, 1165, 5, 138, 0, 0, 1162, 1164, 3, 164, 82, 0, 1163, 1162, 1, 0, 0, 0, 1164, 1167, 1, 0, 0, 0, 1165, 1163, 1, 0, 0, 0, 1165, 1166, 1, 0, 0, 0, 1166, 1168, 1, 0, 0, 0, 1167, 1165, 1, 0, 0, 0, 1168, 1169, 5, 0, 0, 1, 1169, 163, 1, 0, 0, 0, 1170, 1171, 5, 154, 0, 0, 1171, 1172, 3, 106, 53, 0, 1172, 1173, 5, 142, 0, 0, 1173, 1176, 1, 0, 0, 0, 1174, 1176, 5, 153, 0, 0, 1175, 1170, 1, 0, 0, 0, 1175, 1174, 1, 0, 0, 0, 1176, 165, 1, 0, 0, 0, 141, 169, 176, 185, 200, 212, 224, 240, 251, 265, 271, 281, 290, 293, 297, 300, 304, 307, 310, 313, 316, 320, 324, 327, 330, 333, 337, 340, 349, 355, 376, 393, 410, 416, 422, 433, 435, 446, 449, 455, 463, 469, 471, 475, 480, 483, 486, 490, 494, 497, 499, 502, 506, 510, 513, 515, 517, 522, 533, 539, 546, 551, 555, 559, 565, 567, 574, 582, 585, 588, 607, 621, 637, 649, 661, 669, 673, 680, 686, 695, 699, 723, 740, 746, 749, 752, 762, 768, 771, 774, 782, 785, 789, 792, 806, 823, 828, 832, 838, 845, 857, 861, 864, 873, 887, 914, 923, 925, 927, 935, 940, 948, 958, 961, 971, 982, 987, 994, 1007, 1014, 1027, 1033, 1036, 1043, 1055, 1061, 1065, 1071, 1078, 1087, 1098, 1100, 1103, 1111, 1116, 1126, 1131, 1143, 1149, 1159, 1165, 1175] \ No newline at end of file +[4, 1, 154, 1179, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 1, 0, 5, 0, 168, 8, 0, 10, 0, 12, 0, 171, 9, 0, 1, 0, 1, 0, 1, 1, 1, 1, 3, 1, 177, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 186, 8, 3, 1, 4, 1, 4, 1, 4, 5, 4, 191, 8, 4, 10, 4, 12, 4, 194, 9, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 204, 8, 5, 1, 6, 1, 6, 3, 6, 208, 8, 6, 1, 6, 3, 6, 211, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 3, 7, 220, 8, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 3, 8, 228, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 3, 9, 234, 8, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 3, 11, 246, 8, 11, 1, 12, 1, 12, 1, 13, 1, 13, 5, 13, 252, 8, 13, 10, 13, 12, 13, 255, 9, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 5, 15, 266, 8, 15, 10, 15, 12, 15, 269, 9, 15, 1, 16, 1, 16, 1, 16, 3, 16, 274, 8, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 282, 8, 17, 10, 17, 12, 17, 285, 9, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 293, 8, 18, 1, 19, 3, 19, 296, 8, 19, 1, 19, 1, 19, 3, 19, 300, 8, 19, 1, 19, 3, 19, 303, 8, 19, 1, 19, 1, 19, 3, 19, 307, 8, 19, 1, 19, 3, 19, 310, 8, 19, 1, 19, 3, 19, 313, 8, 19, 1, 19, 3, 19, 316, 8, 19, 1, 19, 3, 19, 319, 8, 19, 1, 19, 1, 19, 3, 19, 323, 8, 19, 1, 19, 1, 19, 3, 19, 327, 8, 19, 1, 19, 3, 19, 330, 8, 19, 1, 19, 3, 19, 333, 8, 19, 1, 19, 3, 19, 336, 8, 19, 1, 19, 1, 19, 3, 19, 340, 8, 19, 1, 19, 3, 19, 343, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 352, 8, 21, 1, 22, 1, 22, 1, 22, 1, 23, 3, 23, 358, 8, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 5, 24, 377, 8, 24, 10, 24, 12, 24, 380, 9, 24, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 396, 8, 27, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 413, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 419, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 425, 8, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 3, 31, 436, 8, 31, 3, 31, 438, 8, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 3, 34, 449, 8, 34, 1, 34, 3, 34, 452, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 458, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 466, 8, 34, 1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 472, 8, 34, 10, 34, 12, 34, 475, 9, 34, 1, 35, 3, 35, 478, 8, 35, 1, 35, 1, 35, 1, 35, 3, 35, 483, 8, 35, 1, 35, 3, 35, 486, 8, 35, 1, 35, 3, 35, 489, 8, 35, 1, 35, 1, 35, 3, 35, 493, 8, 35, 1, 35, 1, 35, 3, 35, 497, 8, 35, 1, 35, 3, 35, 500, 8, 35, 3, 35, 502, 8, 35, 1, 35, 3, 35, 505, 8, 35, 1, 35, 1, 35, 3, 35, 509, 8, 35, 1, 35, 1, 35, 3, 35, 513, 8, 35, 1, 35, 3, 35, 516, 8, 35, 3, 35, 518, 8, 35, 3, 35, 520, 8, 35, 1, 36, 1, 36, 1, 36, 3, 36, 525, 8, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 536, 8, 37, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 542, 8, 38, 1, 39, 1, 39, 1, 39, 5, 39, 547, 8, 39, 10, 39, 12, 39, 550, 9, 39, 1, 40, 1, 40, 3, 40, 554, 8, 40, 1, 40, 1, 40, 3, 40, 558, 8, 40, 1, 40, 1, 40, 3, 40, 562, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 3, 41, 568, 8, 41, 3, 41, 570, 8, 41, 1, 42, 1, 42, 1, 42, 5, 42, 575, 8, 42, 10, 42, 12, 42, 578, 9, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 3, 44, 585, 8, 44, 1, 44, 3, 44, 588, 8, 44, 1, 44, 3, 44, 591, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 3, 48, 610, 8, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 3, 49, 624, 8, 49, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 638, 8, 51, 10, 51, 12, 51, 641, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 650, 8, 51, 10, 51, 12, 51, 653, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 662, 8, 51, 10, 51, 12, 51, 665, 9, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 672, 8, 51, 1, 51, 1, 51, 3, 51, 676, 8, 51, 1, 52, 1, 52, 1, 52, 5, 52, 681, 8, 52, 10, 52, 12, 52, 684, 9, 52, 1, 53, 1, 53, 1, 53, 3, 53, 689, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 4, 53, 696, 8, 53, 11, 53, 12, 53, 697, 1, 53, 1, 53, 3, 53, 702, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 726, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 743, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 749, 8, 53, 1, 53, 3, 53, 752, 8, 53, 1, 53, 3, 53, 755, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 765, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 771, 8, 53, 1, 53, 3, 53, 774, 8, 53, 1, 53, 3, 53, 777, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 785, 8, 53, 1, 53, 3, 53, 788, 8, 53, 1, 53, 1, 53, 3, 53, 792, 8, 53, 1, 53, 3, 53, 795, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 809, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 826, 8, 53, 1, 53, 1, 53, 1, 53, 3, 53, 831, 8, 53, 1, 53, 1, 53, 3, 53, 835, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 841, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 848, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 860, 8, 53, 1, 53, 1, 53, 3, 53, 864, 8, 53, 1, 53, 3, 53, 867, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 876, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 890, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 917, 8, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 3, 53, 925, 8, 53, 5, 53, 927, 8, 53, 10, 53, 12, 53, 930, 9, 53, 1, 54, 1, 54, 1, 54, 5, 54, 935, 8, 54, 10, 54, 12, 54, 938, 9, 54, 1, 55, 1, 55, 3, 55, 942, 8, 55, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 948, 8, 56, 10, 56, 12, 56, 951, 9, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 5, 56, 958, 8, 56, 10, 56, 12, 56, 961, 9, 56, 3, 56, 963, 8, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 5, 57, 971, 8, 57, 10, 57, 12, 57, 974, 9, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 5, 57, 982, 8, 57, 10, 57, 12, 57, 985, 9, 57, 1, 57, 1, 57, 3, 57, 989, 8, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 3, 57, 996, 8, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 3, 58, 1009, 8, 58, 1, 59, 1, 59, 1, 59, 5, 59, 1014, 8, 59, 10, 59, 12, 59, 1017, 9, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 1029, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 3, 61, 1035, 8, 61, 1, 61, 3, 61, 1038, 8, 61, 1, 62, 1, 62, 1, 62, 5, 62, 1043, 8, 62, 10, 62, 12, 62, 1046, 9, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1057, 8, 63, 1, 63, 1, 63, 1, 63, 1, 63, 3, 63, 1063, 8, 63, 5, 63, 1065, 8, 63, 10, 63, 12, 63, 1068, 9, 63, 1, 64, 1, 64, 1, 64, 3, 64, 1073, 8, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 3, 65, 1080, 8, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 5, 66, 1087, 8, 66, 10, 66, 12, 66, 1090, 9, 66, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 3, 68, 1100, 8, 68, 3, 68, 1102, 8, 68, 1, 69, 3, 69, 1105, 8, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 3, 69, 1113, 8, 69, 1, 70, 1, 70, 1, 70, 3, 70, 1118, 8, 70, 1, 71, 1, 71, 1, 72, 1, 72, 1, 73, 1, 73, 1, 74, 1, 74, 3, 74, 1128, 8, 74, 1, 75, 1, 75, 1, 75, 3, 75, 1133, 8, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 3, 78, 1145, 8, 78, 1, 79, 1, 79, 5, 79, 1149, 8, 79, 10, 79, 12, 79, 1152, 9, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 3, 80, 1161, 8, 80, 1, 81, 1, 81, 5, 81, 1165, 8, 81, 10, 81, 12, 81, 1168, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 1177, 8, 82, 1, 82, 0, 3, 68, 106, 126, 83, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 0, 16, 2, 0, 17, 17, 72, 72, 2, 0, 42, 42, 49, 49, 3, 0, 1, 1, 4, 4, 8, 8, 4, 0, 1, 1, 3, 4, 8, 8, 78, 78, 2, 0, 49, 49, 71, 71, 2, 0, 1, 1, 4, 4, 2, 0, 7, 7, 21, 22, 2, 0, 28, 28, 47, 47, 2, 0, 69, 69, 74, 74, 3, 0, 10, 10, 48, 48, 87, 87, 2, 0, 39, 39, 51, 51, 1, 0, 103, 104, 2, 0, 114, 114, 134, 134, 7, 0, 20, 20, 36, 36, 53, 54, 68, 68, 76, 76, 93, 93, 99, 99, 12, 0, 1, 19, 21, 28, 30, 35, 37, 40, 42, 49, 51, 52, 56, 56, 58, 67, 69, 75, 77, 92, 94, 95, 97, 98, 4, 0, 19, 19, 28, 28, 37, 37, 46, 46, 1317, 0, 169, 1, 0, 0, 0, 2, 176, 1, 0, 0, 0, 4, 178, 1, 0, 0, 0, 6, 180, 1, 0, 0, 0, 8, 187, 1, 0, 0, 0, 10, 203, 1, 0, 0, 0, 12, 205, 1, 0, 0, 0, 14, 212, 1, 0, 0, 0, 16, 221, 1, 0, 0, 0, 18, 229, 1, 0, 0, 0, 20, 238, 1, 0, 0, 0, 22, 243, 1, 0, 0, 0, 24, 247, 1, 0, 0, 0, 26, 249, 1, 0, 0, 0, 28, 258, 1, 0, 0, 0, 30, 262, 1, 0, 0, 0, 32, 273, 1, 0, 0, 0, 34, 277, 1, 0, 0, 0, 36, 292, 1, 0, 0, 0, 38, 295, 1, 0, 0, 0, 40, 344, 1, 0, 0, 0, 42, 347, 1, 0, 0, 0, 44, 353, 1, 0, 0, 0, 46, 357, 1, 0, 0, 0, 48, 363, 1, 0, 0, 0, 50, 381, 1, 0, 0, 0, 52, 384, 1, 0, 0, 0, 54, 387, 1, 0, 0, 0, 56, 397, 1, 0, 0, 0, 58, 400, 1, 0, 0, 0, 60, 404, 1, 0, 0, 0, 62, 437, 1, 0, 0, 0, 64, 439, 1, 0, 0, 0, 66, 442, 1, 0, 0, 0, 68, 457, 1, 0, 0, 0, 70, 519, 1, 0, 0, 0, 72, 524, 1, 0, 0, 0, 74, 535, 1, 0, 0, 0, 76, 537, 1, 0, 0, 0, 78, 543, 1, 0, 0, 0, 80, 551, 1, 0, 0, 0, 82, 569, 1, 0, 0, 0, 84, 571, 1, 0, 0, 0, 86, 579, 1, 0, 0, 0, 88, 584, 1, 0, 0, 0, 90, 592, 1, 0, 0, 0, 92, 596, 1, 0, 0, 0, 94, 600, 1, 0, 0, 0, 96, 609, 1, 0, 0, 0, 98, 623, 1, 0, 0, 0, 100, 625, 1, 0, 0, 0, 102, 675, 1, 0, 0, 0, 104, 677, 1, 0, 0, 0, 106, 834, 1, 0, 0, 0, 108, 931, 1, 0, 0, 0, 110, 941, 1, 0, 0, 0, 112, 962, 1, 0, 0, 0, 114, 995, 1, 0, 0, 0, 116, 1008, 1, 0, 0, 0, 118, 1010, 1, 0, 0, 0, 120, 1028, 1, 0, 0, 0, 122, 1037, 1, 0, 0, 0, 124, 1039, 1, 0, 0, 0, 126, 1056, 1, 0, 0, 0, 128, 1069, 1, 0, 0, 0, 130, 1079, 1, 0, 0, 0, 132, 1083, 1, 0, 0, 0, 134, 1091, 1, 0, 0, 0, 136, 1101, 1, 0, 0, 0, 138, 1104, 1, 0, 0, 0, 140, 1117, 1, 0, 0, 0, 142, 1119, 1, 0, 0, 0, 144, 1121, 1, 0, 0, 0, 146, 1123, 1, 0, 0, 0, 148, 1127, 1, 0, 0, 0, 150, 1132, 1, 0, 0, 0, 152, 1134, 1, 0, 0, 0, 154, 1138, 1, 0, 0, 0, 156, 1144, 1, 0, 0, 0, 158, 1146, 1, 0, 0, 0, 160, 1160, 1, 0, 0, 0, 162, 1162, 1, 0, 0, 0, 164, 1176, 1, 0, 0, 0, 166, 168, 3, 2, 1, 0, 167, 166, 1, 0, 0, 0, 168, 171, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 172, 1, 0, 0, 0, 171, 169, 1, 0, 0, 0, 172, 173, 5, 0, 0, 1, 173, 1, 1, 0, 0, 0, 174, 177, 3, 6, 3, 0, 175, 177, 3, 10, 5, 0, 176, 174, 1, 0, 0, 0, 176, 175, 1, 0, 0, 0, 177, 3, 1, 0, 0, 0, 178, 179, 3, 106, 53, 0, 179, 5, 1, 0, 0, 0, 180, 181, 5, 50, 0, 0, 181, 185, 3, 150, 75, 0, 182, 183, 5, 111, 0, 0, 183, 184, 5, 118, 0, 0, 184, 186, 3, 4, 2, 0, 185, 182, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 7, 1, 0, 0, 0, 187, 192, 3, 150, 75, 0, 188, 189, 5, 112, 0, 0, 189, 191, 3, 150, 75, 0, 190, 188, 1, 0, 0, 0, 191, 194, 1, 0, 0, 0, 192, 190, 1, 0, 0, 0, 192, 193, 1, 0, 0, 0, 193, 9, 1, 0, 0, 0, 194, 192, 1, 0, 0, 0, 195, 204, 3, 12, 6, 0, 196, 204, 3, 14, 7, 0, 197, 204, 3, 16, 8, 0, 198, 204, 3, 18, 9, 0, 199, 204, 3, 20, 10, 0, 200, 204, 3, 22, 11, 0, 201, 204, 3, 24, 12, 0, 202, 204, 3, 26, 13, 0, 203, 195, 1, 0, 0, 0, 203, 196, 1, 0, 0, 0, 203, 197, 1, 0, 0, 0, 203, 198, 1, 0, 0, 0, 203, 199, 1, 0, 0, 0, 203, 200, 1, 0, 0, 0, 203, 201, 1, 0, 0, 0, 203, 202, 1, 0, 0, 0, 204, 11, 1, 0, 0, 0, 205, 207, 5, 70, 0, 0, 206, 208, 3, 4, 2, 0, 207, 206, 1, 0, 0, 0, 207, 208, 1, 0, 0, 0, 208, 210, 1, 0, 0, 0, 209, 211, 5, 145, 0, 0, 210, 209, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 13, 1, 0, 0, 0, 212, 213, 5, 38, 0, 0, 213, 214, 5, 126, 0, 0, 214, 215, 3, 4, 2, 0, 215, 216, 5, 144, 0, 0, 216, 219, 3, 10, 5, 0, 217, 218, 5, 24, 0, 0, 218, 220, 3, 10, 5, 0, 219, 217, 1, 0, 0, 0, 219, 220, 1, 0, 0, 0, 220, 15, 1, 0, 0, 0, 221, 222, 5, 96, 0, 0, 222, 223, 5, 126, 0, 0, 223, 224, 3, 4, 2, 0, 224, 225, 5, 144, 0, 0, 225, 227, 3, 10, 5, 0, 226, 228, 5, 145, 0, 0, 227, 226, 1, 0, 0, 0, 227, 228, 1, 0, 0, 0, 228, 17, 1, 0, 0, 0, 229, 230, 5, 29, 0, 0, 230, 231, 3, 150, 75, 0, 231, 233, 5, 126, 0, 0, 232, 234, 3, 8, 4, 0, 233, 232, 1, 0, 0, 0, 233, 234, 1, 0, 0, 0, 234, 235, 1, 0, 0, 0, 235, 236, 5, 144, 0, 0, 236, 237, 3, 26, 13, 0, 237, 19, 1, 0, 0, 0, 238, 239, 3, 4, 2, 0, 239, 240, 5, 111, 0, 0, 240, 241, 5, 118, 0, 0, 241, 242, 3, 4, 2, 0, 242, 21, 1, 0, 0, 0, 243, 245, 3, 4, 2, 0, 244, 246, 5, 145, 0, 0, 245, 244, 1, 0, 0, 0, 245, 246, 1, 0, 0, 0, 246, 23, 1, 0, 0, 0, 247, 248, 5, 145, 0, 0, 248, 25, 1, 0, 0, 0, 249, 253, 5, 124, 0, 0, 250, 252, 3, 2, 1, 0, 251, 250, 1, 0, 0, 0, 252, 255, 1, 0, 0, 0, 253, 251, 1, 0, 0, 0, 253, 254, 1, 0, 0, 0, 254, 256, 1, 0, 0, 0, 255, 253, 1, 0, 0, 0, 256, 257, 5, 142, 0, 0, 257, 27, 1, 0, 0, 0, 258, 259, 3, 4, 2, 0, 259, 260, 5, 111, 0, 0, 260, 261, 3, 4, 2, 0, 261, 29, 1, 0, 0, 0, 262, 267, 3, 28, 14, 0, 263, 264, 5, 112, 0, 0, 264, 266, 3, 28, 14, 0, 265, 263, 1, 0, 0, 0, 266, 269, 1, 0, 0, 0, 267, 265, 1, 0, 0, 0, 267, 268, 1, 0, 0, 0, 268, 31, 1, 0, 0, 0, 269, 267, 1, 0, 0, 0, 270, 274, 3, 34, 17, 0, 271, 274, 3, 38, 19, 0, 272, 274, 3, 114, 57, 0, 273, 270, 1, 0, 0, 0, 273, 271, 1, 0, 0, 0, 273, 272, 1, 0, 0, 0, 274, 275, 1, 0, 0, 0, 275, 276, 5, 0, 0, 1, 276, 33, 1, 0, 0, 0, 277, 283, 3, 36, 18, 0, 278, 279, 5, 91, 0, 0, 279, 280, 5, 1, 0, 0, 280, 282, 3, 36, 18, 0, 281, 278, 1, 0, 0, 0, 282, 285, 1, 0, 0, 0, 283, 281, 1, 0, 0, 0, 283, 284, 1, 0, 0, 0, 284, 35, 1, 0, 0, 0, 285, 283, 1, 0, 0, 0, 286, 293, 3, 38, 19, 0, 287, 288, 5, 126, 0, 0, 288, 289, 3, 34, 17, 0, 289, 290, 5, 144, 0, 0, 290, 293, 1, 0, 0, 0, 291, 293, 3, 154, 77, 0, 292, 286, 1, 0, 0, 0, 292, 287, 1, 0, 0, 0, 292, 291, 1, 0, 0, 0, 293, 37, 1, 0, 0, 0, 294, 296, 3, 40, 20, 0, 295, 294, 1, 0, 0, 0, 295, 296, 1, 0, 0, 0, 296, 297, 1, 0, 0, 0, 297, 299, 5, 77, 0, 0, 298, 300, 5, 23, 0, 0, 299, 298, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 302, 1, 0, 0, 0, 301, 303, 3, 42, 21, 0, 302, 301, 1, 0, 0, 0, 302, 303, 1, 0, 0, 0, 303, 304, 1, 0, 0, 0, 304, 306, 3, 104, 52, 0, 305, 307, 3, 44, 22, 0, 306, 305, 1, 0, 0, 0, 306, 307, 1, 0, 0, 0, 307, 309, 1, 0, 0, 0, 308, 310, 3, 46, 23, 0, 309, 308, 1, 0, 0, 0, 309, 310, 1, 0, 0, 0, 310, 312, 1, 0, 0, 0, 311, 313, 3, 50, 25, 0, 312, 311, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0, 313, 315, 1, 0, 0, 0, 314, 316, 3, 52, 26, 0, 315, 314, 1, 0, 0, 0, 315, 316, 1, 0, 0, 0, 316, 318, 1, 0, 0, 0, 317, 319, 3, 54, 27, 0, 318, 317, 1, 0, 0, 0, 318, 319, 1, 0, 0, 0, 319, 322, 1, 0, 0, 0, 320, 321, 5, 98, 0, 0, 321, 323, 7, 0, 0, 0, 322, 320, 1, 0, 0, 0, 322, 323, 1, 0, 0, 0, 323, 326, 1, 0, 0, 0, 324, 325, 5, 98, 0, 0, 325, 327, 5, 86, 0, 0, 326, 324, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 329, 1, 0, 0, 0, 328, 330, 3, 56, 28, 0, 329, 328, 1, 0, 0, 0, 329, 330, 1, 0, 0, 0, 330, 332, 1, 0, 0, 0, 331, 333, 3, 48, 24, 0, 332, 331, 1, 0, 0, 0, 332, 333, 1, 0, 0, 0, 333, 335, 1, 0, 0, 0, 334, 336, 3, 58, 29, 0, 335, 334, 1, 0, 0, 0, 335, 336, 1, 0, 0, 0, 336, 339, 1, 0, 0, 0, 337, 340, 3, 62, 31, 0, 338, 340, 3, 64, 32, 0, 339, 337, 1, 0, 0, 0, 339, 338, 1, 0, 0, 0, 339, 340, 1, 0, 0, 0, 340, 342, 1, 0, 0, 0, 341, 343, 3, 66, 33, 0, 342, 341, 1, 0, 0, 0, 342, 343, 1, 0, 0, 0, 343, 39, 1, 0, 0, 0, 344, 345, 5, 98, 0, 0, 345, 346, 3, 118, 59, 0, 346, 41, 1, 0, 0, 0, 347, 348, 5, 85, 0, 0, 348, 351, 5, 104, 0, 0, 349, 350, 5, 98, 0, 0, 350, 352, 5, 82, 0, 0, 351, 349, 1, 0, 0, 0, 351, 352, 1, 0, 0, 0, 352, 43, 1, 0, 0, 0, 353, 354, 5, 32, 0, 0, 354, 355, 3, 68, 34, 0, 355, 45, 1, 0, 0, 0, 356, 358, 7, 1, 0, 0, 357, 356, 1, 0, 0, 0, 357, 358, 1, 0, 0, 0, 358, 359, 1, 0, 0, 0, 359, 360, 5, 5, 0, 0, 360, 361, 5, 45, 0, 0, 361, 362, 3, 104, 52, 0, 362, 47, 1, 0, 0, 0, 363, 364, 5, 97, 0, 0, 364, 365, 3, 150, 75, 0, 365, 366, 5, 6, 0, 0, 366, 367, 5, 126, 0, 0, 367, 368, 3, 88, 44, 0, 368, 378, 5, 144, 0, 0, 369, 370, 5, 112, 0, 0, 370, 371, 3, 150, 75, 0, 371, 372, 5, 6, 0, 0, 372, 373, 5, 126, 0, 0, 373, 374, 3, 88, 44, 0, 374, 375, 5, 144, 0, 0, 375, 377, 1, 0, 0, 0, 376, 369, 1, 0, 0, 0, 377, 380, 1, 0, 0, 0, 378, 376, 1, 0, 0, 0, 378, 379, 1, 0, 0, 0, 379, 49, 1, 0, 0, 0, 380, 378, 1, 0, 0, 0, 381, 382, 5, 67, 0, 0, 382, 383, 3, 106, 53, 0, 383, 51, 1, 0, 0, 0, 384, 385, 5, 95, 0, 0, 385, 386, 3, 106, 53, 0, 386, 53, 1, 0, 0, 0, 387, 388, 5, 34, 0, 0, 388, 395, 5, 11, 0, 0, 389, 390, 7, 0, 0, 0, 390, 391, 5, 126, 0, 0, 391, 392, 3, 104, 52, 0, 392, 393, 5, 144, 0, 0, 393, 396, 1, 0, 0, 0, 394, 396, 3, 104, 52, 0, 395, 389, 1, 0, 0, 0, 395, 394, 1, 0, 0, 0, 396, 55, 1, 0, 0, 0, 397, 398, 5, 35, 0, 0, 398, 399, 3, 106, 53, 0, 399, 57, 1, 0, 0, 0, 400, 401, 5, 62, 0, 0, 401, 402, 5, 11, 0, 0, 402, 403, 3, 78, 39, 0, 403, 59, 1, 0, 0, 0, 404, 405, 5, 62, 0, 0, 405, 406, 5, 11, 0, 0, 406, 407, 3, 104, 52, 0, 407, 61, 1, 0, 0, 0, 408, 409, 5, 52, 0, 0, 409, 412, 3, 106, 53, 0, 410, 411, 5, 112, 0, 0, 411, 413, 3, 106, 53, 0, 412, 410, 1, 0, 0, 0, 412, 413, 1, 0, 0, 0, 413, 418, 1, 0, 0, 0, 414, 415, 5, 98, 0, 0, 415, 419, 5, 82, 0, 0, 416, 417, 5, 11, 0, 0, 417, 419, 3, 104, 52, 0, 418, 414, 1, 0, 0, 0, 418, 416, 1, 0, 0, 0, 418, 419, 1, 0, 0, 0, 419, 438, 1, 0, 0, 0, 420, 421, 5, 52, 0, 0, 421, 424, 3, 106, 53, 0, 422, 423, 5, 98, 0, 0, 423, 425, 5, 82, 0, 0, 424, 422, 1, 0, 0, 0, 424, 425, 1, 0, 0, 0, 425, 426, 1, 0, 0, 0, 426, 427, 5, 59, 0, 0, 427, 428, 3, 106, 53, 0, 428, 438, 1, 0, 0, 0, 429, 430, 5, 52, 0, 0, 430, 431, 3, 106, 53, 0, 431, 432, 5, 59, 0, 0, 432, 435, 3, 106, 53, 0, 433, 434, 5, 11, 0, 0, 434, 436, 3, 104, 52, 0, 435, 433, 1, 0, 0, 0, 435, 436, 1, 0, 0, 0, 436, 438, 1, 0, 0, 0, 437, 408, 1, 0, 0, 0, 437, 420, 1, 0, 0, 0, 437, 429, 1, 0, 0, 0, 438, 63, 1, 0, 0, 0, 439, 440, 5, 59, 0, 0, 440, 441, 3, 106, 53, 0, 441, 65, 1, 0, 0, 0, 442, 443, 5, 79, 0, 0, 443, 444, 3, 84, 42, 0, 444, 67, 1, 0, 0, 0, 445, 446, 6, 34, -1, 0, 446, 448, 3, 126, 63, 0, 447, 449, 5, 27, 0, 0, 448, 447, 1, 0, 0, 0, 448, 449, 1, 0, 0, 0, 449, 451, 1, 0, 0, 0, 450, 452, 3, 76, 38, 0, 451, 450, 1, 0, 0, 0, 451, 452, 1, 0, 0, 0, 452, 458, 1, 0, 0, 0, 453, 454, 5, 126, 0, 0, 454, 455, 3, 68, 34, 0, 455, 456, 5, 144, 0, 0, 456, 458, 1, 0, 0, 0, 457, 445, 1, 0, 0, 0, 457, 453, 1, 0, 0, 0, 458, 473, 1, 0, 0, 0, 459, 460, 10, 3, 0, 0, 460, 461, 3, 72, 36, 0, 461, 462, 3, 68, 34, 4, 462, 472, 1, 0, 0, 0, 463, 465, 10, 4, 0, 0, 464, 466, 3, 70, 35, 0, 465, 464, 1, 0, 0, 0, 465, 466, 1, 0, 0, 0, 466, 467, 1, 0, 0, 0, 467, 468, 5, 45, 0, 0, 468, 469, 3, 68, 34, 0, 469, 470, 3, 74, 37, 0, 470, 472, 1, 0, 0, 0, 471, 459, 1, 0, 0, 0, 471, 463, 1, 0, 0, 0, 472, 475, 1, 0, 0, 0, 473, 471, 1, 0, 0, 0, 473, 474, 1, 0, 0, 0, 474, 69, 1, 0, 0, 0, 475, 473, 1, 0, 0, 0, 476, 478, 7, 2, 0, 0, 477, 476, 1, 0, 0, 0, 477, 478, 1, 0, 0, 0, 478, 479, 1, 0, 0, 0, 479, 486, 5, 42, 0, 0, 480, 482, 5, 42, 0, 0, 481, 483, 7, 2, 0, 0, 482, 481, 1, 0, 0, 0, 482, 483, 1, 0, 0, 0, 483, 486, 1, 0, 0, 0, 484, 486, 7, 2, 0, 0, 485, 477, 1, 0, 0, 0, 485, 480, 1, 0, 0, 0, 485, 484, 1, 0, 0, 0, 486, 520, 1, 0, 0, 0, 487, 489, 7, 3, 0, 0, 488, 487, 1, 0, 0, 0, 488, 489, 1, 0, 0, 0, 489, 490, 1, 0, 0, 0, 490, 492, 7, 4, 0, 0, 491, 493, 5, 63, 0, 0, 492, 491, 1, 0, 0, 0, 492, 493, 1, 0, 0, 0, 493, 502, 1, 0, 0, 0, 494, 496, 7, 4, 0, 0, 495, 497, 5, 63, 0, 0, 496, 495, 1, 0, 0, 0, 496, 497, 1, 0, 0, 0, 497, 499, 1, 0, 0, 0, 498, 500, 7, 3, 0, 0, 499, 498, 1, 0, 0, 0, 499, 500, 1, 0, 0, 0, 500, 502, 1, 0, 0, 0, 501, 488, 1, 0, 0, 0, 501, 494, 1, 0, 0, 0, 502, 520, 1, 0, 0, 0, 503, 505, 7, 5, 0, 0, 504, 503, 1, 0, 0, 0, 504, 505, 1, 0, 0, 0, 505, 506, 1, 0, 0, 0, 506, 508, 5, 33, 0, 0, 507, 509, 5, 63, 0, 0, 508, 507, 1, 0, 0, 0, 508, 509, 1, 0, 0, 0, 509, 518, 1, 0, 0, 0, 510, 512, 5, 33, 0, 0, 511, 513, 5, 63, 0, 0, 512, 511, 1, 0, 0, 0, 512, 513, 1, 0, 0, 0, 513, 515, 1, 0, 0, 0, 514, 516, 7, 5, 0, 0, 515, 514, 1, 0, 0, 0, 515, 516, 1, 0, 0, 0, 516, 518, 1, 0, 0, 0, 517, 504, 1, 0, 0, 0, 517, 510, 1, 0, 0, 0, 518, 520, 1, 0, 0, 0, 519, 485, 1, 0, 0, 0, 519, 501, 1, 0, 0, 0, 519, 517, 1, 0, 0, 0, 520, 71, 1, 0, 0, 0, 521, 522, 5, 16, 0, 0, 522, 525, 5, 45, 0, 0, 523, 525, 5, 112, 0, 0, 524, 521, 1, 0, 0, 0, 524, 523, 1, 0, 0, 0, 525, 73, 1, 0, 0, 0, 526, 527, 5, 60, 0, 0, 527, 536, 3, 104, 52, 0, 528, 529, 5, 92, 0, 0, 529, 530, 5, 126, 0, 0, 530, 531, 3, 104, 52, 0, 531, 532, 5, 144, 0, 0, 532, 536, 1, 0, 0, 0, 533, 534, 5, 92, 0, 0, 534, 536, 3, 104, 52, 0, 535, 526, 1, 0, 0, 0, 535, 528, 1, 0, 0, 0, 535, 533, 1, 0, 0, 0, 536, 75, 1, 0, 0, 0, 537, 538, 5, 75, 0, 0, 538, 541, 3, 82, 41, 0, 539, 540, 5, 59, 0, 0, 540, 542, 3, 82, 41, 0, 541, 539, 1, 0, 0, 0, 541, 542, 1, 0, 0, 0, 542, 77, 1, 0, 0, 0, 543, 548, 3, 80, 40, 0, 544, 545, 5, 112, 0, 0, 545, 547, 3, 80, 40, 0, 546, 544, 1, 0, 0, 0, 547, 550, 1, 0, 0, 0, 548, 546, 1, 0, 0, 0, 548, 549, 1, 0, 0, 0, 549, 79, 1, 0, 0, 0, 550, 548, 1, 0, 0, 0, 551, 553, 3, 106, 53, 0, 552, 554, 7, 6, 0, 0, 553, 552, 1, 0, 0, 0, 553, 554, 1, 0, 0, 0, 554, 557, 1, 0, 0, 0, 555, 556, 5, 58, 0, 0, 556, 558, 7, 7, 0, 0, 557, 555, 1, 0, 0, 0, 557, 558, 1, 0, 0, 0, 558, 561, 1, 0, 0, 0, 559, 560, 5, 15, 0, 0, 560, 562, 5, 106, 0, 0, 561, 559, 1, 0, 0, 0, 561, 562, 1, 0, 0, 0, 562, 81, 1, 0, 0, 0, 563, 570, 3, 154, 77, 0, 564, 567, 3, 138, 69, 0, 565, 566, 5, 146, 0, 0, 566, 568, 3, 138, 69, 0, 567, 565, 1, 0, 0, 0, 567, 568, 1, 0, 0, 0, 568, 570, 1, 0, 0, 0, 569, 563, 1, 0, 0, 0, 569, 564, 1, 0, 0, 0, 570, 83, 1, 0, 0, 0, 571, 576, 3, 86, 43, 0, 572, 573, 5, 112, 0, 0, 573, 575, 3, 86, 43, 0, 574, 572, 1, 0, 0, 0, 575, 578, 1, 0, 0, 0, 576, 574, 1, 0, 0, 0, 576, 577, 1, 0, 0, 0, 577, 85, 1, 0, 0, 0, 578, 576, 1, 0, 0, 0, 579, 580, 3, 150, 75, 0, 580, 581, 5, 118, 0, 0, 581, 582, 3, 140, 70, 0, 582, 87, 1, 0, 0, 0, 583, 585, 3, 90, 45, 0, 584, 583, 1, 0, 0, 0, 584, 585, 1, 0, 0, 0, 585, 587, 1, 0, 0, 0, 586, 588, 3, 92, 46, 0, 587, 586, 1, 0, 0, 0, 587, 588, 1, 0, 0, 0, 588, 590, 1, 0, 0, 0, 589, 591, 3, 94, 47, 0, 590, 589, 1, 0, 0, 0, 590, 591, 1, 0, 0, 0, 591, 89, 1, 0, 0, 0, 592, 593, 5, 65, 0, 0, 593, 594, 5, 11, 0, 0, 594, 595, 3, 104, 52, 0, 595, 91, 1, 0, 0, 0, 596, 597, 5, 62, 0, 0, 597, 598, 5, 11, 0, 0, 598, 599, 3, 78, 39, 0, 599, 93, 1, 0, 0, 0, 600, 601, 7, 8, 0, 0, 601, 602, 3, 96, 48, 0, 602, 95, 1, 0, 0, 0, 603, 610, 3, 98, 49, 0, 604, 605, 5, 9, 0, 0, 605, 606, 3, 98, 49, 0, 606, 607, 5, 2, 0, 0, 607, 608, 3, 98, 49, 0, 608, 610, 1, 0, 0, 0, 609, 603, 1, 0, 0, 0, 609, 604, 1, 0, 0, 0, 610, 97, 1, 0, 0, 0, 611, 612, 5, 18, 0, 0, 612, 624, 5, 73, 0, 0, 613, 614, 5, 90, 0, 0, 614, 624, 5, 66, 0, 0, 615, 616, 5, 90, 0, 0, 616, 624, 5, 30, 0, 0, 617, 618, 3, 138, 69, 0, 618, 619, 5, 66, 0, 0, 619, 624, 1, 0, 0, 0, 620, 621, 3, 138, 69, 0, 621, 622, 5, 30, 0, 0, 622, 624, 1, 0, 0, 0, 623, 611, 1, 0, 0, 0, 623, 613, 1, 0, 0, 0, 623, 615, 1, 0, 0, 0, 623, 617, 1, 0, 0, 0, 623, 620, 1, 0, 0, 0, 624, 99, 1, 0, 0, 0, 625, 626, 3, 106, 53, 0, 626, 627, 5, 0, 0, 1, 627, 101, 1, 0, 0, 0, 628, 676, 3, 150, 75, 0, 629, 630, 3, 150, 75, 0, 630, 631, 5, 126, 0, 0, 631, 632, 3, 150, 75, 0, 632, 639, 3, 102, 51, 0, 633, 634, 5, 112, 0, 0, 634, 635, 3, 150, 75, 0, 635, 636, 3, 102, 51, 0, 636, 638, 1, 0, 0, 0, 637, 633, 1, 0, 0, 0, 638, 641, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 639, 640, 1, 0, 0, 0, 640, 642, 1, 0, 0, 0, 641, 639, 1, 0, 0, 0, 642, 643, 5, 144, 0, 0, 643, 676, 1, 0, 0, 0, 644, 645, 3, 150, 75, 0, 645, 646, 5, 126, 0, 0, 646, 651, 3, 152, 76, 0, 647, 648, 5, 112, 0, 0, 648, 650, 3, 152, 76, 0, 649, 647, 1, 0, 0, 0, 650, 653, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 651, 652, 1, 0, 0, 0, 652, 654, 1, 0, 0, 0, 653, 651, 1, 0, 0, 0, 654, 655, 5, 144, 0, 0, 655, 676, 1, 0, 0, 0, 656, 657, 3, 150, 75, 0, 657, 658, 5, 126, 0, 0, 658, 663, 3, 102, 51, 0, 659, 660, 5, 112, 0, 0, 660, 662, 3, 102, 51, 0, 661, 659, 1, 0, 0, 0, 662, 665, 1, 0, 0, 0, 663, 661, 1, 0, 0, 0, 663, 664, 1, 0, 0, 0, 664, 666, 1, 0, 0, 0, 665, 663, 1, 0, 0, 0, 666, 667, 5, 144, 0, 0, 667, 676, 1, 0, 0, 0, 668, 669, 3, 150, 75, 0, 669, 671, 5, 126, 0, 0, 670, 672, 3, 104, 52, 0, 671, 670, 1, 0, 0, 0, 671, 672, 1, 0, 0, 0, 672, 673, 1, 0, 0, 0, 673, 674, 5, 144, 0, 0, 674, 676, 1, 0, 0, 0, 675, 628, 1, 0, 0, 0, 675, 629, 1, 0, 0, 0, 675, 644, 1, 0, 0, 0, 675, 656, 1, 0, 0, 0, 675, 668, 1, 0, 0, 0, 676, 103, 1, 0, 0, 0, 677, 682, 3, 106, 53, 0, 678, 679, 5, 112, 0, 0, 679, 681, 3, 106, 53, 0, 680, 678, 1, 0, 0, 0, 681, 684, 1, 0, 0, 0, 682, 680, 1, 0, 0, 0, 682, 683, 1, 0, 0, 0, 683, 105, 1, 0, 0, 0, 684, 682, 1, 0, 0, 0, 685, 686, 6, 53, -1, 0, 686, 688, 5, 12, 0, 0, 687, 689, 3, 106, 53, 0, 688, 687, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 695, 1, 0, 0, 0, 690, 691, 5, 94, 0, 0, 691, 692, 3, 106, 53, 0, 692, 693, 5, 81, 0, 0, 693, 694, 3, 106, 53, 0, 694, 696, 1, 0, 0, 0, 695, 690, 1, 0, 0, 0, 696, 697, 1, 0, 0, 0, 697, 695, 1, 0, 0, 0, 697, 698, 1, 0, 0, 0, 698, 701, 1, 0, 0, 0, 699, 700, 5, 24, 0, 0, 700, 702, 3, 106, 53, 0, 701, 699, 1, 0, 0, 0, 701, 702, 1, 0, 0, 0, 702, 703, 1, 0, 0, 0, 703, 704, 5, 25, 0, 0, 704, 835, 1, 0, 0, 0, 705, 706, 5, 13, 0, 0, 706, 707, 5, 126, 0, 0, 707, 708, 3, 106, 53, 0, 708, 709, 5, 6, 0, 0, 709, 710, 3, 102, 51, 0, 710, 711, 5, 144, 0, 0, 711, 835, 1, 0, 0, 0, 712, 713, 5, 19, 0, 0, 713, 835, 5, 106, 0, 0, 714, 715, 5, 43, 0, 0, 715, 716, 3, 106, 53, 0, 716, 717, 3, 142, 71, 0, 717, 835, 1, 0, 0, 0, 718, 719, 5, 80, 0, 0, 719, 720, 5, 126, 0, 0, 720, 721, 3, 106, 53, 0, 721, 722, 5, 32, 0, 0, 722, 725, 3, 106, 53, 0, 723, 724, 5, 31, 0, 0, 724, 726, 3, 106, 53, 0, 725, 723, 1, 0, 0, 0, 725, 726, 1, 0, 0, 0, 726, 727, 1, 0, 0, 0, 727, 728, 5, 144, 0, 0, 728, 835, 1, 0, 0, 0, 729, 730, 5, 83, 0, 0, 730, 835, 5, 106, 0, 0, 731, 732, 5, 88, 0, 0, 732, 733, 5, 126, 0, 0, 733, 734, 7, 9, 0, 0, 734, 735, 3, 156, 78, 0, 735, 736, 5, 32, 0, 0, 736, 737, 3, 106, 53, 0, 737, 738, 5, 144, 0, 0, 738, 835, 1, 0, 0, 0, 739, 740, 3, 150, 75, 0, 740, 742, 5, 126, 0, 0, 741, 743, 3, 104, 52, 0, 742, 741, 1, 0, 0, 0, 742, 743, 1, 0, 0, 0, 743, 744, 1, 0, 0, 0, 744, 745, 5, 144, 0, 0, 745, 754, 1, 0, 0, 0, 746, 748, 5, 126, 0, 0, 747, 749, 5, 23, 0, 0, 748, 747, 1, 0, 0, 0, 748, 749, 1, 0, 0, 0, 749, 751, 1, 0, 0, 0, 750, 752, 3, 108, 54, 0, 751, 750, 1, 0, 0, 0, 751, 752, 1, 0, 0, 0, 752, 753, 1, 0, 0, 0, 753, 755, 5, 144, 0, 0, 754, 746, 1, 0, 0, 0, 754, 755, 1, 0, 0, 0, 755, 756, 1, 0, 0, 0, 756, 757, 5, 64, 0, 0, 757, 758, 5, 126, 0, 0, 758, 759, 3, 88, 44, 0, 759, 760, 5, 144, 0, 0, 760, 835, 1, 0, 0, 0, 761, 762, 3, 150, 75, 0, 762, 764, 5, 126, 0, 0, 763, 765, 3, 104, 52, 0, 764, 763, 1, 0, 0, 0, 764, 765, 1, 0, 0, 0, 765, 766, 1, 0, 0, 0, 766, 767, 5, 144, 0, 0, 767, 776, 1, 0, 0, 0, 768, 770, 5, 126, 0, 0, 769, 771, 5, 23, 0, 0, 770, 769, 1, 0, 0, 0, 770, 771, 1, 0, 0, 0, 771, 773, 1, 0, 0, 0, 772, 774, 3, 108, 54, 0, 773, 772, 1, 0, 0, 0, 773, 774, 1, 0, 0, 0, 774, 775, 1, 0, 0, 0, 775, 777, 5, 144, 0, 0, 776, 768, 1, 0, 0, 0, 776, 777, 1, 0, 0, 0, 777, 778, 1, 0, 0, 0, 778, 779, 5, 64, 0, 0, 779, 780, 3, 150, 75, 0, 780, 835, 1, 0, 0, 0, 781, 787, 3, 150, 75, 0, 782, 784, 5, 126, 0, 0, 783, 785, 3, 104, 52, 0, 784, 783, 1, 0, 0, 0, 784, 785, 1, 0, 0, 0, 785, 786, 1, 0, 0, 0, 786, 788, 5, 144, 0, 0, 787, 782, 1, 0, 0, 0, 787, 788, 1, 0, 0, 0, 788, 789, 1, 0, 0, 0, 789, 791, 5, 126, 0, 0, 790, 792, 5, 23, 0, 0, 791, 790, 1, 0, 0, 0, 791, 792, 1, 0, 0, 0, 792, 794, 1, 0, 0, 0, 793, 795, 3, 108, 54, 0, 794, 793, 1, 0, 0, 0, 794, 795, 1, 0, 0, 0, 795, 796, 1, 0, 0, 0, 796, 797, 5, 144, 0, 0, 797, 835, 1, 0, 0, 0, 798, 835, 3, 114, 57, 0, 799, 835, 3, 158, 79, 0, 800, 835, 3, 140, 70, 0, 801, 802, 5, 114, 0, 0, 802, 835, 3, 106, 53, 19, 803, 804, 5, 56, 0, 0, 804, 835, 3, 106, 53, 13, 805, 806, 3, 130, 65, 0, 806, 807, 5, 116, 0, 0, 807, 809, 1, 0, 0, 0, 808, 805, 1, 0, 0, 0, 808, 809, 1, 0, 0, 0, 809, 810, 1, 0, 0, 0, 810, 835, 5, 108, 0, 0, 811, 812, 5, 126, 0, 0, 812, 813, 3, 34, 17, 0, 813, 814, 5, 144, 0, 0, 814, 835, 1, 0, 0, 0, 815, 816, 5, 126, 0, 0, 816, 817, 3, 106, 53, 0, 817, 818, 5, 144, 0, 0, 818, 835, 1, 0, 0, 0, 819, 820, 5, 126, 0, 0, 820, 821, 3, 104, 52, 0, 821, 822, 5, 144, 0, 0, 822, 835, 1, 0, 0, 0, 823, 825, 5, 125, 0, 0, 824, 826, 3, 104, 52, 0, 825, 824, 1, 0, 0, 0, 825, 826, 1, 0, 0, 0, 826, 827, 1, 0, 0, 0, 827, 835, 5, 143, 0, 0, 828, 830, 5, 124, 0, 0, 829, 831, 3, 30, 15, 0, 830, 829, 1, 0, 0, 0, 830, 831, 1, 0, 0, 0, 831, 832, 1, 0, 0, 0, 832, 835, 5, 142, 0, 0, 833, 835, 3, 122, 61, 0, 834, 685, 1, 0, 0, 0, 834, 705, 1, 0, 0, 0, 834, 712, 1, 0, 0, 0, 834, 714, 1, 0, 0, 0, 834, 718, 1, 0, 0, 0, 834, 729, 1, 0, 0, 0, 834, 731, 1, 0, 0, 0, 834, 739, 1, 0, 0, 0, 834, 761, 1, 0, 0, 0, 834, 781, 1, 0, 0, 0, 834, 798, 1, 0, 0, 0, 834, 799, 1, 0, 0, 0, 834, 800, 1, 0, 0, 0, 834, 801, 1, 0, 0, 0, 834, 803, 1, 0, 0, 0, 834, 808, 1, 0, 0, 0, 834, 811, 1, 0, 0, 0, 834, 815, 1, 0, 0, 0, 834, 819, 1, 0, 0, 0, 834, 823, 1, 0, 0, 0, 834, 828, 1, 0, 0, 0, 834, 833, 1, 0, 0, 0, 835, 928, 1, 0, 0, 0, 836, 840, 10, 18, 0, 0, 837, 841, 5, 108, 0, 0, 838, 841, 5, 146, 0, 0, 839, 841, 5, 133, 0, 0, 840, 837, 1, 0, 0, 0, 840, 838, 1, 0, 0, 0, 840, 839, 1, 0, 0, 0, 841, 842, 1, 0, 0, 0, 842, 927, 3, 106, 53, 19, 843, 847, 10, 17, 0, 0, 844, 848, 5, 134, 0, 0, 845, 848, 5, 114, 0, 0, 846, 848, 5, 113, 0, 0, 847, 844, 1, 0, 0, 0, 847, 845, 1, 0, 0, 0, 847, 846, 1, 0, 0, 0, 848, 849, 1, 0, 0, 0, 849, 927, 3, 106, 53, 18, 850, 875, 10, 16, 0, 0, 851, 876, 5, 117, 0, 0, 852, 876, 5, 118, 0, 0, 853, 876, 5, 129, 0, 0, 854, 876, 5, 127, 0, 0, 855, 876, 5, 128, 0, 0, 856, 876, 5, 119, 0, 0, 857, 876, 5, 120, 0, 0, 858, 860, 5, 56, 0, 0, 859, 858, 1, 0, 0, 0, 859, 860, 1, 0, 0, 0, 860, 861, 1, 0, 0, 0, 861, 863, 5, 40, 0, 0, 862, 864, 5, 14, 0, 0, 863, 862, 1, 0, 0, 0, 863, 864, 1, 0, 0, 0, 864, 876, 1, 0, 0, 0, 865, 867, 5, 56, 0, 0, 866, 865, 1, 0, 0, 0, 866, 867, 1, 0, 0, 0, 867, 868, 1, 0, 0, 0, 868, 876, 7, 10, 0, 0, 869, 876, 5, 140, 0, 0, 870, 876, 5, 141, 0, 0, 871, 876, 5, 131, 0, 0, 872, 876, 5, 122, 0, 0, 873, 876, 5, 123, 0, 0, 874, 876, 5, 130, 0, 0, 875, 851, 1, 0, 0, 0, 875, 852, 1, 0, 0, 0, 875, 853, 1, 0, 0, 0, 875, 854, 1, 0, 0, 0, 875, 855, 1, 0, 0, 0, 875, 856, 1, 0, 0, 0, 875, 857, 1, 0, 0, 0, 875, 859, 1, 0, 0, 0, 875, 866, 1, 0, 0, 0, 875, 869, 1, 0, 0, 0, 875, 870, 1, 0, 0, 0, 875, 871, 1, 0, 0, 0, 875, 872, 1, 0, 0, 0, 875, 873, 1, 0, 0, 0, 875, 874, 1, 0, 0, 0, 876, 877, 1, 0, 0, 0, 877, 927, 3, 106, 53, 17, 878, 879, 10, 14, 0, 0, 879, 880, 5, 132, 0, 0, 880, 927, 3, 106, 53, 15, 881, 882, 10, 12, 0, 0, 882, 883, 5, 2, 0, 0, 883, 927, 3, 106, 53, 13, 884, 885, 10, 11, 0, 0, 885, 886, 5, 61, 0, 0, 886, 927, 3, 106, 53, 12, 887, 889, 10, 10, 0, 0, 888, 890, 5, 56, 0, 0, 889, 888, 1, 0, 0, 0, 889, 890, 1, 0, 0, 0, 890, 891, 1, 0, 0, 0, 891, 892, 5, 9, 0, 0, 892, 893, 3, 106, 53, 0, 893, 894, 5, 2, 0, 0, 894, 895, 3, 106, 53, 11, 895, 927, 1, 0, 0, 0, 896, 897, 10, 9, 0, 0, 897, 898, 5, 135, 0, 0, 898, 899, 3, 106, 53, 0, 899, 900, 5, 111, 0, 0, 900, 901, 3, 106, 53, 9, 901, 927, 1, 0, 0, 0, 902, 903, 10, 22, 0, 0, 903, 904, 5, 125, 0, 0, 904, 905, 3, 106, 53, 0, 905, 906, 5, 143, 0, 0, 906, 927, 1, 0, 0, 0, 907, 908, 10, 21, 0, 0, 908, 909, 5, 116, 0, 0, 909, 927, 5, 104, 0, 0, 910, 911, 10, 20, 0, 0, 911, 912, 5, 116, 0, 0, 912, 927, 3, 150, 75, 0, 913, 914, 10, 15, 0, 0, 914, 916, 5, 44, 0, 0, 915, 917, 5, 56, 0, 0, 916, 915, 1, 0, 0, 0, 916, 917, 1, 0, 0, 0, 917, 918, 1, 0, 0, 0, 918, 927, 5, 57, 0, 0, 919, 924, 10, 8, 0, 0, 920, 921, 5, 6, 0, 0, 921, 925, 3, 150, 75, 0, 922, 923, 5, 6, 0, 0, 923, 925, 5, 106, 0, 0, 924, 920, 1, 0, 0, 0, 924, 922, 1, 0, 0, 0, 925, 927, 1, 0, 0, 0, 926, 836, 1, 0, 0, 0, 926, 843, 1, 0, 0, 0, 926, 850, 1, 0, 0, 0, 926, 878, 1, 0, 0, 0, 926, 881, 1, 0, 0, 0, 926, 884, 1, 0, 0, 0, 926, 887, 1, 0, 0, 0, 926, 896, 1, 0, 0, 0, 926, 902, 1, 0, 0, 0, 926, 907, 1, 0, 0, 0, 926, 910, 1, 0, 0, 0, 926, 913, 1, 0, 0, 0, 926, 919, 1, 0, 0, 0, 927, 930, 1, 0, 0, 0, 928, 926, 1, 0, 0, 0, 928, 929, 1, 0, 0, 0, 929, 107, 1, 0, 0, 0, 930, 928, 1, 0, 0, 0, 931, 936, 3, 110, 55, 0, 932, 933, 5, 112, 0, 0, 933, 935, 3, 110, 55, 0, 934, 932, 1, 0, 0, 0, 935, 938, 1, 0, 0, 0, 936, 934, 1, 0, 0, 0, 936, 937, 1, 0, 0, 0, 937, 109, 1, 0, 0, 0, 938, 936, 1, 0, 0, 0, 939, 942, 3, 112, 56, 0, 940, 942, 3, 106, 53, 0, 941, 939, 1, 0, 0, 0, 941, 940, 1, 0, 0, 0, 942, 111, 1, 0, 0, 0, 943, 944, 5, 126, 0, 0, 944, 949, 3, 150, 75, 0, 945, 946, 5, 112, 0, 0, 946, 948, 3, 150, 75, 0, 947, 945, 1, 0, 0, 0, 948, 951, 1, 0, 0, 0, 949, 947, 1, 0, 0, 0, 949, 950, 1, 0, 0, 0, 950, 952, 1, 0, 0, 0, 951, 949, 1, 0, 0, 0, 952, 953, 5, 144, 0, 0, 953, 963, 1, 0, 0, 0, 954, 959, 3, 150, 75, 0, 955, 956, 5, 112, 0, 0, 956, 958, 3, 150, 75, 0, 957, 955, 1, 0, 0, 0, 958, 961, 1, 0, 0, 0, 959, 957, 1, 0, 0, 0, 959, 960, 1, 0, 0, 0, 960, 963, 1, 0, 0, 0, 961, 959, 1, 0, 0, 0, 962, 943, 1, 0, 0, 0, 962, 954, 1, 0, 0, 0, 963, 964, 1, 0, 0, 0, 964, 965, 5, 107, 0, 0, 965, 966, 3, 106, 53, 0, 966, 113, 1, 0, 0, 0, 967, 968, 5, 128, 0, 0, 968, 972, 3, 150, 75, 0, 969, 971, 3, 116, 58, 0, 970, 969, 1, 0, 0, 0, 971, 974, 1, 0, 0, 0, 972, 970, 1, 0, 0, 0, 972, 973, 1, 0, 0, 0, 973, 975, 1, 0, 0, 0, 974, 972, 1, 0, 0, 0, 975, 976, 5, 146, 0, 0, 976, 977, 5, 120, 0, 0, 977, 996, 1, 0, 0, 0, 978, 979, 5, 128, 0, 0, 979, 983, 3, 150, 75, 0, 980, 982, 3, 116, 58, 0, 981, 980, 1, 0, 0, 0, 982, 985, 1, 0, 0, 0, 983, 981, 1, 0, 0, 0, 983, 984, 1, 0, 0, 0, 984, 986, 1, 0, 0, 0, 985, 983, 1, 0, 0, 0, 986, 988, 5, 120, 0, 0, 987, 989, 3, 114, 57, 0, 988, 987, 1, 0, 0, 0, 988, 989, 1, 0, 0, 0, 989, 990, 1, 0, 0, 0, 990, 991, 5, 128, 0, 0, 991, 992, 5, 146, 0, 0, 992, 993, 3, 150, 75, 0, 993, 994, 5, 120, 0, 0, 994, 996, 1, 0, 0, 0, 995, 967, 1, 0, 0, 0, 995, 978, 1, 0, 0, 0, 996, 115, 1, 0, 0, 0, 997, 998, 3, 150, 75, 0, 998, 999, 5, 118, 0, 0, 999, 1000, 3, 156, 78, 0, 1000, 1009, 1, 0, 0, 0, 1001, 1002, 3, 150, 75, 0, 1002, 1003, 5, 118, 0, 0, 1003, 1004, 5, 124, 0, 0, 1004, 1005, 3, 106, 53, 0, 1005, 1006, 5, 142, 0, 0, 1006, 1009, 1, 0, 0, 0, 1007, 1009, 3, 150, 75, 0, 1008, 997, 1, 0, 0, 0, 1008, 1001, 1, 0, 0, 0, 1008, 1007, 1, 0, 0, 0, 1009, 117, 1, 0, 0, 0, 1010, 1015, 3, 120, 60, 0, 1011, 1012, 5, 112, 0, 0, 1012, 1014, 3, 120, 60, 0, 1013, 1011, 1, 0, 0, 0, 1014, 1017, 1, 0, 0, 0, 1015, 1013, 1, 0, 0, 0, 1015, 1016, 1, 0, 0, 0, 1016, 119, 1, 0, 0, 0, 1017, 1015, 1, 0, 0, 0, 1018, 1019, 3, 150, 75, 0, 1019, 1020, 5, 6, 0, 0, 1020, 1021, 5, 126, 0, 0, 1021, 1022, 3, 34, 17, 0, 1022, 1023, 5, 144, 0, 0, 1023, 1029, 1, 0, 0, 0, 1024, 1025, 3, 106, 53, 0, 1025, 1026, 5, 6, 0, 0, 1026, 1027, 3, 150, 75, 0, 1027, 1029, 1, 0, 0, 0, 1028, 1018, 1, 0, 0, 0, 1028, 1024, 1, 0, 0, 0, 1029, 121, 1, 0, 0, 0, 1030, 1038, 3, 154, 77, 0, 1031, 1032, 3, 130, 65, 0, 1032, 1033, 5, 116, 0, 0, 1033, 1035, 1, 0, 0, 0, 1034, 1031, 1, 0, 0, 0, 1034, 1035, 1, 0, 0, 0, 1035, 1036, 1, 0, 0, 0, 1036, 1038, 3, 124, 62, 0, 1037, 1030, 1, 0, 0, 0, 1037, 1034, 1, 0, 0, 0, 1038, 123, 1, 0, 0, 0, 1039, 1044, 3, 150, 75, 0, 1040, 1041, 5, 116, 0, 0, 1041, 1043, 3, 150, 75, 0, 1042, 1040, 1, 0, 0, 0, 1043, 1046, 1, 0, 0, 0, 1044, 1042, 1, 0, 0, 0, 1044, 1045, 1, 0, 0, 0, 1045, 125, 1, 0, 0, 0, 1046, 1044, 1, 0, 0, 0, 1047, 1048, 6, 63, -1, 0, 1048, 1057, 3, 130, 65, 0, 1049, 1057, 3, 128, 64, 0, 1050, 1051, 5, 126, 0, 0, 1051, 1052, 3, 34, 17, 0, 1052, 1053, 5, 144, 0, 0, 1053, 1057, 1, 0, 0, 0, 1054, 1057, 3, 114, 57, 0, 1055, 1057, 3, 154, 77, 0, 1056, 1047, 1, 0, 0, 0, 1056, 1049, 1, 0, 0, 0, 1056, 1050, 1, 0, 0, 0, 1056, 1054, 1, 0, 0, 0, 1056, 1055, 1, 0, 0, 0, 1057, 1066, 1, 0, 0, 0, 1058, 1062, 10, 3, 0, 0, 1059, 1063, 3, 148, 74, 0, 1060, 1061, 5, 6, 0, 0, 1061, 1063, 3, 150, 75, 0, 1062, 1059, 1, 0, 0, 0, 1062, 1060, 1, 0, 0, 0, 1063, 1065, 1, 0, 0, 0, 1064, 1058, 1, 0, 0, 0, 1065, 1068, 1, 0, 0, 0, 1066, 1064, 1, 0, 0, 0, 1066, 1067, 1, 0, 0, 0, 1067, 127, 1, 0, 0, 0, 1068, 1066, 1, 0, 0, 0, 1069, 1070, 3, 150, 75, 0, 1070, 1072, 5, 126, 0, 0, 1071, 1073, 3, 132, 66, 0, 1072, 1071, 1, 0, 0, 0, 1072, 1073, 1, 0, 0, 0, 1073, 1074, 1, 0, 0, 0, 1074, 1075, 5, 144, 0, 0, 1075, 129, 1, 0, 0, 0, 1076, 1077, 3, 134, 67, 0, 1077, 1078, 5, 116, 0, 0, 1078, 1080, 1, 0, 0, 0, 1079, 1076, 1, 0, 0, 0, 1079, 1080, 1, 0, 0, 0, 1080, 1081, 1, 0, 0, 0, 1081, 1082, 3, 150, 75, 0, 1082, 131, 1, 0, 0, 0, 1083, 1088, 3, 106, 53, 0, 1084, 1085, 5, 112, 0, 0, 1085, 1087, 3, 106, 53, 0, 1086, 1084, 1, 0, 0, 0, 1087, 1090, 1, 0, 0, 0, 1088, 1086, 1, 0, 0, 0, 1088, 1089, 1, 0, 0, 0, 1089, 133, 1, 0, 0, 0, 1090, 1088, 1, 0, 0, 0, 1091, 1092, 3, 150, 75, 0, 1092, 135, 1, 0, 0, 0, 1093, 1102, 5, 102, 0, 0, 1094, 1095, 5, 116, 0, 0, 1095, 1102, 7, 11, 0, 0, 1096, 1097, 5, 104, 0, 0, 1097, 1099, 5, 116, 0, 0, 1098, 1100, 7, 11, 0, 0, 1099, 1098, 1, 0, 0, 0, 1099, 1100, 1, 0, 0, 0, 1100, 1102, 1, 0, 0, 0, 1101, 1093, 1, 0, 0, 0, 1101, 1094, 1, 0, 0, 0, 1101, 1096, 1, 0, 0, 0, 1102, 137, 1, 0, 0, 0, 1103, 1105, 7, 12, 0, 0, 1104, 1103, 1, 0, 0, 0, 1104, 1105, 1, 0, 0, 0, 1105, 1112, 1, 0, 0, 0, 1106, 1113, 3, 136, 68, 0, 1107, 1113, 5, 103, 0, 0, 1108, 1113, 5, 104, 0, 0, 1109, 1113, 5, 105, 0, 0, 1110, 1113, 5, 41, 0, 0, 1111, 1113, 5, 55, 0, 0, 1112, 1106, 1, 0, 0, 0, 1112, 1107, 1, 0, 0, 0, 1112, 1108, 1, 0, 0, 0, 1112, 1109, 1, 0, 0, 0, 1112, 1110, 1, 0, 0, 0, 1112, 1111, 1, 0, 0, 0, 1113, 139, 1, 0, 0, 0, 1114, 1118, 3, 138, 69, 0, 1115, 1118, 5, 106, 0, 0, 1116, 1118, 5, 57, 0, 0, 1117, 1114, 1, 0, 0, 0, 1117, 1115, 1, 0, 0, 0, 1117, 1116, 1, 0, 0, 0, 1118, 141, 1, 0, 0, 0, 1119, 1120, 7, 13, 0, 0, 1120, 143, 1, 0, 0, 0, 1121, 1122, 7, 14, 0, 0, 1122, 145, 1, 0, 0, 0, 1123, 1124, 7, 15, 0, 0, 1124, 147, 1, 0, 0, 0, 1125, 1128, 5, 101, 0, 0, 1126, 1128, 3, 146, 73, 0, 1127, 1125, 1, 0, 0, 0, 1127, 1126, 1, 0, 0, 0, 1128, 149, 1, 0, 0, 0, 1129, 1133, 5, 101, 0, 0, 1130, 1133, 3, 142, 71, 0, 1131, 1133, 3, 144, 72, 0, 1132, 1129, 1, 0, 0, 0, 1132, 1130, 1, 0, 0, 0, 1132, 1131, 1, 0, 0, 0, 1133, 151, 1, 0, 0, 0, 1134, 1135, 3, 156, 78, 0, 1135, 1136, 5, 118, 0, 0, 1136, 1137, 3, 138, 69, 0, 1137, 153, 1, 0, 0, 0, 1138, 1139, 5, 124, 0, 0, 1139, 1140, 3, 150, 75, 0, 1140, 1141, 5, 142, 0, 0, 1141, 155, 1, 0, 0, 0, 1142, 1145, 5, 106, 0, 0, 1143, 1145, 3, 158, 79, 0, 1144, 1142, 1, 0, 0, 0, 1144, 1143, 1, 0, 0, 0, 1145, 157, 1, 0, 0, 0, 1146, 1150, 5, 137, 0, 0, 1147, 1149, 3, 160, 80, 0, 1148, 1147, 1, 0, 0, 0, 1149, 1152, 1, 0, 0, 0, 1150, 1148, 1, 0, 0, 0, 1150, 1151, 1, 0, 0, 0, 1151, 1153, 1, 0, 0, 0, 1152, 1150, 1, 0, 0, 0, 1153, 1154, 5, 139, 0, 0, 1154, 159, 1, 0, 0, 0, 1155, 1156, 5, 152, 0, 0, 1156, 1157, 3, 106, 53, 0, 1157, 1158, 5, 142, 0, 0, 1158, 1161, 1, 0, 0, 0, 1159, 1161, 5, 151, 0, 0, 1160, 1155, 1, 0, 0, 0, 1160, 1159, 1, 0, 0, 0, 1161, 161, 1, 0, 0, 0, 1162, 1166, 5, 138, 0, 0, 1163, 1165, 3, 164, 82, 0, 1164, 1163, 1, 0, 0, 0, 1165, 1168, 1, 0, 0, 0, 1166, 1164, 1, 0, 0, 0, 1166, 1167, 1, 0, 0, 0, 1167, 1169, 1, 0, 0, 0, 1168, 1166, 1, 0, 0, 0, 1169, 1170, 5, 0, 0, 1, 1170, 163, 1, 0, 0, 0, 1171, 1172, 5, 154, 0, 0, 1172, 1173, 3, 106, 53, 0, 1173, 1174, 5, 142, 0, 0, 1174, 1177, 1, 0, 0, 0, 1175, 1177, 5, 153, 0, 0, 1176, 1171, 1, 0, 0, 0, 1176, 1175, 1, 0, 0, 0, 1177, 165, 1, 0, 0, 0, 145, 169, 176, 185, 192, 203, 207, 210, 219, 227, 233, 245, 253, 267, 273, 283, 292, 295, 299, 302, 306, 309, 312, 315, 318, 322, 326, 329, 332, 335, 339, 342, 351, 357, 378, 395, 412, 418, 424, 435, 437, 448, 451, 457, 465, 471, 473, 477, 482, 485, 488, 492, 496, 499, 501, 504, 508, 512, 515, 517, 519, 524, 535, 541, 548, 553, 557, 561, 567, 569, 576, 584, 587, 590, 609, 623, 639, 651, 663, 671, 675, 682, 688, 697, 701, 725, 742, 748, 751, 754, 764, 770, 773, 776, 784, 787, 791, 794, 808, 825, 830, 834, 840, 847, 859, 863, 866, 875, 889, 916, 924, 926, 928, 936, 941, 949, 959, 962, 972, 983, 988, 995, 1008, 1015, 1028, 1034, 1037, 1044, 1056, 1062, 1066, 1072, 1079, 1088, 1099, 1101, 1104, 1112, 1117, 1127, 1132, 1144, 1150, 1160, 1166, 1176] \ No newline at end of file diff --git a/posthog/hogql/grammar/HogQLParser.py b/posthog/hogql/grammar/HogQLParser.py index 9c0c50c0835da..cf2b82f619d91 100644 --- a/posthog/hogql/grammar/HogQLParser.py +++ b/posthog/hogql/grammar/HogQLParser.py @@ -10,7 +10,7 @@ def serializedATN(): return [ - 4,1,154,1178,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, + 4,1,154,1179,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, 7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7, 13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2, 20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7, @@ -24,458 +24,459 @@ def serializedATN(): 72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77,2,78,7, 78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,1,0,5,0,168,8,0,10,0, 12,0,171,9,0,1,0,1,0,1,1,1,1,3,1,177,8,1,1,2,1,2,1,3,1,3,1,3,1,3, - 1,3,3,3,186,8,3,1,3,1,3,1,4,1,4,1,4,1,4,1,4,1,4,1,5,1,5,1,5,5,5, - 199,8,5,10,5,12,5,202,9,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,3, - 6,213,8,6,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,3,8,225,8,8,1, - 9,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,3, - 11,241,8,11,1,11,1,11,1,11,1,12,1,12,1,13,1,13,5,13,250,8,13,10, - 13,12,13,253,9,13,1,13,1,13,1,14,1,14,1,14,1,14,1,15,1,15,1,15,5, - 15,264,8,15,10,15,12,15,267,9,15,1,16,1,16,1,16,3,16,272,8,16,1, - 16,1,16,1,17,1,17,1,17,1,17,5,17,280,8,17,10,17,12,17,283,9,17,1, - 18,1,18,1,18,1,18,1,18,1,18,3,18,291,8,18,1,19,3,19,294,8,19,1,19, - 1,19,3,19,298,8,19,1,19,3,19,301,8,19,1,19,1,19,3,19,305,8,19,1, - 19,3,19,308,8,19,1,19,3,19,311,8,19,1,19,3,19,314,8,19,1,19,3,19, - 317,8,19,1,19,1,19,3,19,321,8,19,1,19,1,19,3,19,325,8,19,1,19,3, - 19,328,8,19,1,19,3,19,331,8,19,1,19,3,19,334,8,19,1,19,1,19,3,19, - 338,8,19,1,19,3,19,341,8,19,1,20,1,20,1,20,1,21,1,21,1,21,1,21,3, - 21,350,8,21,1,22,1,22,1,22,1,23,3,23,356,8,23,1,23,1,23,1,23,1,23, - 1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24, - 5,24,375,8,24,10,24,12,24,378,9,24,1,25,1,25,1,25,1,26,1,26,1,26, - 1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,394,8,27,1,28,1,28, - 1,28,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31, - 3,31,411,8,31,1,31,1,31,1,31,1,31,3,31,417,8,31,1,31,1,31,1,31,1, - 31,3,31,423,8,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,3, - 31,434,8,31,3,31,436,8,31,1,32,1,32,1,32,1,33,1,33,1,33,1,34,1,34, - 1,34,3,34,447,8,34,1,34,3,34,450,8,34,1,34,1,34,1,34,1,34,3,34,456, - 8,34,1,34,1,34,1,34,1,34,1,34,1,34,3,34,464,8,34,1,34,1,34,1,34, - 1,34,5,34,470,8,34,10,34,12,34,473,9,34,1,35,3,35,476,8,35,1,35, - 1,35,1,35,3,35,481,8,35,1,35,3,35,484,8,35,1,35,3,35,487,8,35,1, - 35,1,35,3,35,491,8,35,1,35,1,35,3,35,495,8,35,1,35,3,35,498,8,35, - 3,35,500,8,35,1,35,3,35,503,8,35,1,35,1,35,3,35,507,8,35,1,35,1, - 35,3,35,511,8,35,1,35,3,35,514,8,35,3,35,516,8,35,3,35,518,8,35, - 1,36,1,36,1,36,3,36,523,8,36,1,37,1,37,1,37,1,37,1,37,1,37,1,37, - 1,37,1,37,3,37,534,8,37,1,38,1,38,1,38,1,38,3,38,540,8,38,1,39,1, - 39,1,39,5,39,545,8,39,10,39,12,39,548,9,39,1,40,1,40,3,40,552,8, - 40,1,40,1,40,3,40,556,8,40,1,40,1,40,3,40,560,8,40,1,41,1,41,1,41, - 1,41,3,41,566,8,41,3,41,568,8,41,1,42,1,42,1,42,5,42,573,8,42,10, - 42,12,42,576,9,42,1,43,1,43,1,43,1,43,1,44,3,44,583,8,44,1,44,3, - 44,586,8,44,1,44,3,44,589,8,44,1,45,1,45,1,45,1,45,1,46,1,46,1,46, - 1,46,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1,48,1,48,3,48,608,8,48, - 1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,3,49, - 622,8,49,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51, - 1,51,5,51,636,8,51,10,51,12,51,639,9,51,1,51,1,51,1,51,1,51,1,51, - 1,51,1,51,5,51,648,8,51,10,51,12,51,651,9,51,1,51,1,51,1,51,1,51, - 1,51,1,51,1,51,5,51,660,8,51,10,51,12,51,663,9,51,1,51,1,51,1,51, - 1,51,1,51,3,51,670,8,51,1,51,1,51,3,51,674,8,51,1,52,1,52,1,52,5, - 52,679,8,52,10,52,12,52,682,9,52,1,53,1,53,1,53,3,53,687,8,53,1, - 53,1,53,1,53,1,53,1,53,4,53,694,8,53,11,53,12,53,695,1,53,1,53,3, - 53,700,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, - 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,724, - 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,3,53,741,8,53,1,53,1,53,1,53,1,53,3,53,747,8,53,1, - 53,3,53,750,8,53,1,53,3,53,753,8,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,3,53,763,8,53,1,53,1,53,1,53,1,53,3,53,769,8,53,1,53,3, - 53,772,8,53,1,53,3,53,775,8,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53, - 783,8,53,1,53,3,53,786,8,53,1,53,1,53,3,53,790,8,53,1,53,3,53,793, - 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 3,53,807,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,3,53,824,8,53,1,53,1,53,1,53,3,53,829,8, - 53,1,53,1,53,3,53,833,8,53,1,53,1,53,1,53,1,53,3,53,839,8,53,1,53, - 1,53,1,53,1,53,1,53,3,53,846,8,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,3,53,858,8,53,1,53,1,53,3,53,862,8,53,1,53,3, - 53,865,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,874,8,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,888, - 8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,3,3,3,186,8,3,1,4,1,4,1,4,5,4,191,8,4,10,4,12,4,194,9,4,1,5,1, + 5,1,5,1,5,1,5,1,5,1,5,1,5,3,5,204,8,5,1,6,1,6,3,6,208,8,6,1,6,3, + 6,211,8,6,1,7,1,7,1,7,1,7,1,7,1,7,1,7,3,7,220,8,7,1,8,1,8,1,8,1, + 8,1,8,1,8,3,8,228,8,8,1,9,1,9,1,9,1,9,3,9,234,8,9,1,9,1,9,1,9,1, + 10,1,10,1,10,1,10,1,10,1,11,1,11,3,11,246,8,11,1,12,1,12,1,13,1, + 13,5,13,252,8,13,10,13,12,13,255,9,13,1,13,1,13,1,14,1,14,1,14,1, + 14,1,15,1,15,1,15,5,15,266,8,15,10,15,12,15,269,9,15,1,16,1,16,1, + 16,3,16,274,8,16,1,16,1,16,1,17,1,17,1,17,1,17,5,17,282,8,17,10, + 17,12,17,285,9,17,1,18,1,18,1,18,1,18,1,18,1,18,3,18,293,8,18,1, + 19,3,19,296,8,19,1,19,1,19,3,19,300,8,19,1,19,3,19,303,8,19,1,19, + 1,19,3,19,307,8,19,1,19,3,19,310,8,19,1,19,3,19,313,8,19,1,19,3, + 19,316,8,19,1,19,3,19,319,8,19,1,19,1,19,3,19,323,8,19,1,19,1,19, + 3,19,327,8,19,1,19,3,19,330,8,19,1,19,3,19,333,8,19,1,19,3,19,336, + 8,19,1,19,1,19,3,19,340,8,19,1,19,3,19,343,8,19,1,20,1,20,1,20,1, + 21,1,21,1,21,1,21,3,21,352,8,21,1,22,1,22,1,22,1,23,3,23,358,8,23, + 1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24, + 1,24,1,24,1,24,1,24,5,24,377,8,24,10,24,12,24,380,9,24,1,25,1,25, + 1,25,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27, + 396,8,27,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30, + 1,31,1,31,1,31,1,31,3,31,413,8,31,1,31,1,31,1,31,1,31,3,31,419,8, + 31,1,31,1,31,1,31,1,31,3,31,425,8,31,1,31,1,31,1,31,1,31,1,31,1, + 31,1,31,1,31,1,31,3,31,436,8,31,3,31,438,8,31,1,32,1,32,1,32,1,33, + 1,33,1,33,1,34,1,34,1,34,3,34,449,8,34,1,34,3,34,452,8,34,1,34,1, + 34,1,34,1,34,3,34,458,8,34,1,34,1,34,1,34,1,34,1,34,1,34,3,34,466, + 8,34,1,34,1,34,1,34,1,34,5,34,472,8,34,10,34,12,34,475,9,34,1,35, + 3,35,478,8,35,1,35,1,35,1,35,3,35,483,8,35,1,35,3,35,486,8,35,1, + 35,3,35,489,8,35,1,35,1,35,3,35,493,8,35,1,35,1,35,3,35,497,8,35, + 1,35,3,35,500,8,35,3,35,502,8,35,1,35,3,35,505,8,35,1,35,1,35,3, + 35,509,8,35,1,35,1,35,3,35,513,8,35,1,35,3,35,516,8,35,3,35,518, + 8,35,3,35,520,8,35,1,36,1,36,1,36,3,36,525,8,36,1,37,1,37,1,37,1, + 37,1,37,1,37,1,37,1,37,1,37,3,37,536,8,37,1,38,1,38,1,38,1,38,3, + 38,542,8,38,1,39,1,39,1,39,5,39,547,8,39,10,39,12,39,550,9,39,1, + 40,1,40,3,40,554,8,40,1,40,1,40,3,40,558,8,40,1,40,1,40,3,40,562, + 8,40,1,41,1,41,1,41,1,41,3,41,568,8,41,3,41,570,8,41,1,42,1,42,1, + 42,5,42,575,8,42,10,42,12,42,578,9,42,1,43,1,43,1,43,1,43,1,44,3, + 44,585,8,44,1,44,3,44,588,8,44,1,44,3,44,591,8,44,1,45,1,45,1,45, + 1,45,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1,48, + 1,48,3,48,610,8,48,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49, + 1,49,1,49,1,49,3,49,624,8,49,1,50,1,50,1,50,1,51,1,51,1,51,1,51, + 1,51,1,51,1,51,1,51,1,51,5,51,638,8,51,10,51,12,51,641,9,51,1,51, + 1,51,1,51,1,51,1,51,1,51,1,51,5,51,650,8,51,10,51,12,51,653,9,51, + 1,51,1,51,1,51,1,51,1,51,1,51,1,51,5,51,662,8,51,10,51,12,51,665, + 9,51,1,51,1,51,1,51,1,51,1,51,3,51,672,8,51,1,51,1,51,3,51,676,8, + 51,1,52,1,52,1,52,5,52,681,8,52,10,52,12,52,684,9,52,1,53,1,53,1, + 53,3,53,689,8,53,1,53,1,53,1,53,1,53,1,53,4,53,696,8,53,11,53,12, + 53,697,1,53,1,53,3,53,702,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 3,53,915,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,924,8,53,5, - 53,926,8,53,10,53,12,53,929,9,53,1,54,1,54,1,54,5,54,934,8,54,10, - 54,12,54,937,9,54,1,55,1,55,3,55,941,8,55,1,56,1,56,1,56,1,56,5, - 56,947,8,56,10,56,12,56,950,9,56,1,56,1,56,1,56,1,56,1,56,5,56,957, - 8,56,10,56,12,56,960,9,56,3,56,962,8,56,1,56,1,56,1,56,1,57,1,57, - 1,57,5,57,970,8,57,10,57,12,57,973,9,57,1,57,1,57,1,57,1,57,1,57, - 1,57,5,57,981,8,57,10,57,12,57,984,9,57,1,57,1,57,3,57,988,8,57, - 1,57,1,57,1,57,1,57,1,57,3,57,995,8,57,1,58,1,58,1,58,1,58,1,58, - 1,58,1,58,1,58,1,58,1,58,1,58,3,58,1008,8,58,1,59,1,59,1,59,5,59, - 1013,8,59,10,59,12,59,1016,9,59,1,60,1,60,1,60,1,60,1,60,1,60,1, - 60,1,60,1,60,1,60,3,60,1028,8,60,1,61,1,61,1,61,1,61,3,61,1034,8, - 61,1,61,3,61,1037,8,61,1,62,1,62,1,62,5,62,1042,8,62,10,62,12,62, - 1045,9,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,3,63,1056, - 8,63,1,63,1,63,1,63,1,63,3,63,1062,8,63,5,63,1064,8,63,10,63,12, - 63,1067,9,63,1,64,1,64,1,64,3,64,1072,8,64,1,64,1,64,1,65,1,65,1, - 65,3,65,1079,8,65,1,65,1,65,1,66,1,66,1,66,5,66,1086,8,66,10,66, - 12,66,1089,9,66,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68,3,68,1099, - 8,68,3,68,1101,8,68,1,69,3,69,1104,8,69,1,69,1,69,1,69,1,69,1,69, - 1,69,3,69,1112,8,69,1,70,1,70,1,70,3,70,1117,8,70,1,71,1,71,1,72, - 1,72,1,73,1,73,1,74,1,74,3,74,1127,8,74,1,75,1,75,1,75,3,75,1132, - 8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77,1,78,1,78,3,78,1144, - 8,78,1,79,1,79,5,79,1148,8,79,10,79,12,79,1151,9,79,1,79,1,79,1, - 80,1,80,1,80,1,80,1,80,3,80,1160,8,80,1,81,1,81,5,81,1164,8,81,10, - 81,12,81,1167,9,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,3,82,1176, - 8,82,1,82,0,3,68,106,126,83,0,2,4,6,8,10,12,14,16,18,20,22,24,26, - 28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70, - 72,74,76,78,80,82,84,86,88,90,92,94,96,98,100,102,104,106,108,110, - 112,114,116,118,120,122,124,126,128,130,132,134,136,138,140,142, - 144,146,148,150,152,154,156,158,160,162,164,0,16,2,0,17,17,72,72, - 2,0,42,42,49,49,3,0,1,1,4,4,8,8,4,0,1,1,3,4,8,8,78,78,2,0,49,49, - 71,71,2,0,1,1,4,4,2,0,7,7,21,22,2,0,28,28,47,47,2,0,69,69,74,74, - 3,0,10,10,48,48,87,87,2,0,39,39,51,51,1,0,103,104,2,0,114,114,134, - 134,7,0,20,20,36,36,53,54,68,68,76,76,93,93,99,99,12,0,1,19,21,28, - 30,35,37,40,42,49,51,52,56,56,58,67,69,75,77,92,94,95,97,98,4,0, - 19,19,28,28,37,37,46,46,1314,0,169,1,0,0,0,2,176,1,0,0,0,4,178,1, - 0,0,0,6,180,1,0,0,0,8,189,1,0,0,0,10,195,1,0,0,0,12,212,1,0,0,0, - 14,214,1,0,0,0,16,217,1,0,0,0,18,226,1,0,0,0,20,232,1,0,0,0,22,236, - 1,0,0,0,24,245,1,0,0,0,26,247,1,0,0,0,28,256,1,0,0,0,30,260,1,0, - 0,0,32,271,1,0,0,0,34,275,1,0,0,0,36,290,1,0,0,0,38,293,1,0,0,0, - 40,342,1,0,0,0,42,345,1,0,0,0,44,351,1,0,0,0,46,355,1,0,0,0,48,361, - 1,0,0,0,50,379,1,0,0,0,52,382,1,0,0,0,54,385,1,0,0,0,56,395,1,0, - 0,0,58,398,1,0,0,0,60,402,1,0,0,0,62,435,1,0,0,0,64,437,1,0,0,0, - 66,440,1,0,0,0,68,455,1,0,0,0,70,517,1,0,0,0,72,522,1,0,0,0,74,533, - 1,0,0,0,76,535,1,0,0,0,78,541,1,0,0,0,80,549,1,0,0,0,82,567,1,0, - 0,0,84,569,1,0,0,0,86,577,1,0,0,0,88,582,1,0,0,0,90,590,1,0,0,0, - 92,594,1,0,0,0,94,598,1,0,0,0,96,607,1,0,0,0,98,621,1,0,0,0,100, - 623,1,0,0,0,102,673,1,0,0,0,104,675,1,0,0,0,106,832,1,0,0,0,108, - 930,1,0,0,0,110,940,1,0,0,0,112,961,1,0,0,0,114,994,1,0,0,0,116, - 1007,1,0,0,0,118,1009,1,0,0,0,120,1027,1,0,0,0,122,1036,1,0,0,0, - 124,1038,1,0,0,0,126,1055,1,0,0,0,128,1068,1,0,0,0,130,1078,1,0, - 0,0,132,1082,1,0,0,0,134,1090,1,0,0,0,136,1100,1,0,0,0,138,1103, - 1,0,0,0,140,1116,1,0,0,0,142,1118,1,0,0,0,144,1120,1,0,0,0,146,1122, - 1,0,0,0,148,1126,1,0,0,0,150,1131,1,0,0,0,152,1133,1,0,0,0,154,1137, - 1,0,0,0,156,1143,1,0,0,0,158,1145,1,0,0,0,160,1159,1,0,0,0,162,1161, - 1,0,0,0,164,1175,1,0,0,0,166,168,3,2,1,0,167,166,1,0,0,0,168,171, - 1,0,0,0,169,167,1,0,0,0,169,170,1,0,0,0,170,172,1,0,0,0,171,169, - 1,0,0,0,172,173,5,0,0,1,173,1,1,0,0,0,174,177,3,6,3,0,175,177,3, - 12,6,0,176,174,1,0,0,0,176,175,1,0,0,0,177,3,1,0,0,0,178,179,3,106, - 53,0,179,5,1,0,0,0,180,181,5,50,0,0,181,185,3,150,75,0,182,183,5, - 111,0,0,183,184,5,118,0,0,184,186,3,4,2,0,185,182,1,0,0,0,185,186, - 1,0,0,0,186,187,1,0,0,0,187,188,5,145,0,0,188,7,1,0,0,0,189,190, - 3,4,2,0,190,191,5,111,0,0,191,192,5,118,0,0,192,193,3,4,2,0,193, - 194,5,145,0,0,194,9,1,0,0,0,195,200,3,150,75,0,196,197,5,112,0,0, - 197,199,3,150,75,0,198,196,1,0,0,0,199,202,1,0,0,0,200,198,1,0,0, - 0,200,201,1,0,0,0,201,11,1,0,0,0,202,200,1,0,0,0,203,213,3,20,10, - 0,204,213,3,24,12,0,205,213,3,14,7,0,206,213,3,16,8,0,207,213,3, - 18,9,0,208,213,3,22,11,0,209,213,3,8,4,0,210,213,3,20,10,0,211,213, - 3,26,13,0,212,203,1,0,0,0,212,204,1,0,0,0,212,205,1,0,0,0,212,206, - 1,0,0,0,212,207,1,0,0,0,212,208,1,0,0,0,212,209,1,0,0,0,212,210, - 1,0,0,0,212,211,1,0,0,0,213,13,1,0,0,0,214,215,3,4,2,0,215,216,5, - 145,0,0,216,15,1,0,0,0,217,218,5,38,0,0,218,219,5,126,0,0,219,220, - 3,4,2,0,220,221,5,144,0,0,221,224,3,12,6,0,222,223,5,24,0,0,223, - 225,3,12,6,0,224,222,1,0,0,0,224,225,1,0,0,0,225,17,1,0,0,0,226, - 227,5,96,0,0,227,228,5,126,0,0,228,229,3,4,2,0,229,230,5,144,0,0, - 230,231,3,12,6,0,231,19,1,0,0,0,232,233,5,70,0,0,233,234,3,4,2,0, - 234,235,5,145,0,0,235,21,1,0,0,0,236,237,5,29,0,0,237,238,3,150, - 75,0,238,240,5,126,0,0,239,241,3,10,5,0,240,239,1,0,0,0,240,241, - 1,0,0,0,241,242,1,0,0,0,242,243,5,144,0,0,243,244,3,26,13,0,244, - 23,1,0,0,0,245,246,5,145,0,0,246,25,1,0,0,0,247,251,5,124,0,0,248, - 250,3,2,1,0,249,248,1,0,0,0,250,253,1,0,0,0,251,249,1,0,0,0,251, - 252,1,0,0,0,252,254,1,0,0,0,253,251,1,0,0,0,254,255,5,142,0,0,255, - 27,1,0,0,0,256,257,3,4,2,0,257,258,5,111,0,0,258,259,3,4,2,0,259, - 29,1,0,0,0,260,265,3,28,14,0,261,262,5,112,0,0,262,264,3,28,14,0, - 263,261,1,0,0,0,264,267,1,0,0,0,265,263,1,0,0,0,265,266,1,0,0,0, - 266,31,1,0,0,0,267,265,1,0,0,0,268,272,3,34,17,0,269,272,3,38,19, - 0,270,272,3,114,57,0,271,268,1,0,0,0,271,269,1,0,0,0,271,270,1,0, - 0,0,272,273,1,0,0,0,273,274,5,0,0,1,274,33,1,0,0,0,275,281,3,36, - 18,0,276,277,5,91,0,0,277,278,5,1,0,0,278,280,3,36,18,0,279,276, - 1,0,0,0,280,283,1,0,0,0,281,279,1,0,0,0,281,282,1,0,0,0,282,35,1, - 0,0,0,283,281,1,0,0,0,284,291,3,38,19,0,285,286,5,126,0,0,286,287, - 3,34,17,0,287,288,5,144,0,0,288,291,1,0,0,0,289,291,3,154,77,0,290, - 284,1,0,0,0,290,285,1,0,0,0,290,289,1,0,0,0,291,37,1,0,0,0,292,294, - 3,40,20,0,293,292,1,0,0,0,293,294,1,0,0,0,294,295,1,0,0,0,295,297, - 5,77,0,0,296,298,5,23,0,0,297,296,1,0,0,0,297,298,1,0,0,0,298,300, - 1,0,0,0,299,301,3,42,21,0,300,299,1,0,0,0,300,301,1,0,0,0,301,302, - 1,0,0,0,302,304,3,104,52,0,303,305,3,44,22,0,304,303,1,0,0,0,304, - 305,1,0,0,0,305,307,1,0,0,0,306,308,3,46,23,0,307,306,1,0,0,0,307, - 308,1,0,0,0,308,310,1,0,0,0,309,311,3,50,25,0,310,309,1,0,0,0,310, - 311,1,0,0,0,311,313,1,0,0,0,312,314,3,52,26,0,313,312,1,0,0,0,313, - 314,1,0,0,0,314,316,1,0,0,0,315,317,3,54,27,0,316,315,1,0,0,0,316, - 317,1,0,0,0,317,320,1,0,0,0,318,319,5,98,0,0,319,321,7,0,0,0,320, - 318,1,0,0,0,320,321,1,0,0,0,321,324,1,0,0,0,322,323,5,98,0,0,323, - 325,5,86,0,0,324,322,1,0,0,0,324,325,1,0,0,0,325,327,1,0,0,0,326, - 328,3,56,28,0,327,326,1,0,0,0,327,328,1,0,0,0,328,330,1,0,0,0,329, - 331,3,48,24,0,330,329,1,0,0,0,330,331,1,0,0,0,331,333,1,0,0,0,332, - 334,3,58,29,0,333,332,1,0,0,0,333,334,1,0,0,0,334,337,1,0,0,0,335, - 338,3,62,31,0,336,338,3,64,32,0,337,335,1,0,0,0,337,336,1,0,0,0, - 337,338,1,0,0,0,338,340,1,0,0,0,339,341,3,66,33,0,340,339,1,0,0, - 0,340,341,1,0,0,0,341,39,1,0,0,0,342,343,5,98,0,0,343,344,3,118, - 59,0,344,41,1,0,0,0,345,346,5,85,0,0,346,349,5,104,0,0,347,348,5, - 98,0,0,348,350,5,82,0,0,349,347,1,0,0,0,349,350,1,0,0,0,350,43,1, - 0,0,0,351,352,5,32,0,0,352,353,3,68,34,0,353,45,1,0,0,0,354,356, - 7,1,0,0,355,354,1,0,0,0,355,356,1,0,0,0,356,357,1,0,0,0,357,358, - 5,5,0,0,358,359,5,45,0,0,359,360,3,104,52,0,360,47,1,0,0,0,361,362, - 5,97,0,0,362,363,3,150,75,0,363,364,5,6,0,0,364,365,5,126,0,0,365, - 366,3,88,44,0,366,376,5,144,0,0,367,368,5,112,0,0,368,369,3,150, - 75,0,369,370,5,6,0,0,370,371,5,126,0,0,371,372,3,88,44,0,372,373, - 5,144,0,0,373,375,1,0,0,0,374,367,1,0,0,0,375,378,1,0,0,0,376,374, - 1,0,0,0,376,377,1,0,0,0,377,49,1,0,0,0,378,376,1,0,0,0,379,380,5, - 67,0,0,380,381,3,106,53,0,381,51,1,0,0,0,382,383,5,95,0,0,383,384, - 3,106,53,0,384,53,1,0,0,0,385,386,5,34,0,0,386,393,5,11,0,0,387, - 388,7,0,0,0,388,389,5,126,0,0,389,390,3,104,52,0,390,391,5,144,0, - 0,391,394,1,0,0,0,392,394,3,104,52,0,393,387,1,0,0,0,393,392,1,0, - 0,0,394,55,1,0,0,0,395,396,5,35,0,0,396,397,3,106,53,0,397,57,1, - 0,0,0,398,399,5,62,0,0,399,400,5,11,0,0,400,401,3,78,39,0,401,59, - 1,0,0,0,402,403,5,62,0,0,403,404,5,11,0,0,404,405,3,104,52,0,405, - 61,1,0,0,0,406,407,5,52,0,0,407,410,3,106,53,0,408,409,5,112,0,0, - 409,411,3,106,53,0,410,408,1,0,0,0,410,411,1,0,0,0,411,416,1,0,0, - 0,412,413,5,98,0,0,413,417,5,82,0,0,414,415,5,11,0,0,415,417,3,104, - 52,0,416,412,1,0,0,0,416,414,1,0,0,0,416,417,1,0,0,0,417,436,1,0, - 0,0,418,419,5,52,0,0,419,422,3,106,53,0,420,421,5,98,0,0,421,423, - 5,82,0,0,422,420,1,0,0,0,422,423,1,0,0,0,423,424,1,0,0,0,424,425, - 5,59,0,0,425,426,3,106,53,0,426,436,1,0,0,0,427,428,5,52,0,0,428, - 429,3,106,53,0,429,430,5,59,0,0,430,433,3,106,53,0,431,432,5,11, - 0,0,432,434,3,104,52,0,433,431,1,0,0,0,433,434,1,0,0,0,434,436,1, - 0,0,0,435,406,1,0,0,0,435,418,1,0,0,0,435,427,1,0,0,0,436,63,1,0, - 0,0,437,438,5,59,0,0,438,439,3,106,53,0,439,65,1,0,0,0,440,441,5, - 79,0,0,441,442,3,84,42,0,442,67,1,0,0,0,443,444,6,34,-1,0,444,446, - 3,126,63,0,445,447,5,27,0,0,446,445,1,0,0,0,446,447,1,0,0,0,447, - 449,1,0,0,0,448,450,3,76,38,0,449,448,1,0,0,0,449,450,1,0,0,0,450, - 456,1,0,0,0,451,452,5,126,0,0,452,453,3,68,34,0,453,454,5,144,0, - 0,454,456,1,0,0,0,455,443,1,0,0,0,455,451,1,0,0,0,456,471,1,0,0, - 0,457,458,10,3,0,0,458,459,3,72,36,0,459,460,3,68,34,4,460,470,1, - 0,0,0,461,463,10,4,0,0,462,464,3,70,35,0,463,462,1,0,0,0,463,464, - 1,0,0,0,464,465,1,0,0,0,465,466,5,45,0,0,466,467,3,68,34,0,467,468, - 3,74,37,0,468,470,1,0,0,0,469,457,1,0,0,0,469,461,1,0,0,0,470,473, - 1,0,0,0,471,469,1,0,0,0,471,472,1,0,0,0,472,69,1,0,0,0,473,471,1, - 0,0,0,474,476,7,2,0,0,475,474,1,0,0,0,475,476,1,0,0,0,476,477,1, - 0,0,0,477,484,5,42,0,0,478,480,5,42,0,0,479,481,7,2,0,0,480,479, - 1,0,0,0,480,481,1,0,0,0,481,484,1,0,0,0,482,484,7,2,0,0,483,475, - 1,0,0,0,483,478,1,0,0,0,483,482,1,0,0,0,484,518,1,0,0,0,485,487, - 7,3,0,0,486,485,1,0,0,0,486,487,1,0,0,0,487,488,1,0,0,0,488,490, - 7,4,0,0,489,491,5,63,0,0,490,489,1,0,0,0,490,491,1,0,0,0,491,500, - 1,0,0,0,492,494,7,4,0,0,493,495,5,63,0,0,494,493,1,0,0,0,494,495, - 1,0,0,0,495,497,1,0,0,0,496,498,7,3,0,0,497,496,1,0,0,0,497,498, - 1,0,0,0,498,500,1,0,0,0,499,486,1,0,0,0,499,492,1,0,0,0,500,518, - 1,0,0,0,501,503,7,5,0,0,502,501,1,0,0,0,502,503,1,0,0,0,503,504, - 1,0,0,0,504,506,5,33,0,0,505,507,5,63,0,0,506,505,1,0,0,0,506,507, - 1,0,0,0,507,516,1,0,0,0,508,510,5,33,0,0,509,511,5,63,0,0,510,509, - 1,0,0,0,510,511,1,0,0,0,511,513,1,0,0,0,512,514,7,5,0,0,513,512, - 1,0,0,0,513,514,1,0,0,0,514,516,1,0,0,0,515,502,1,0,0,0,515,508, - 1,0,0,0,516,518,1,0,0,0,517,483,1,0,0,0,517,499,1,0,0,0,517,515, - 1,0,0,0,518,71,1,0,0,0,519,520,5,16,0,0,520,523,5,45,0,0,521,523, - 5,112,0,0,522,519,1,0,0,0,522,521,1,0,0,0,523,73,1,0,0,0,524,525, - 5,60,0,0,525,534,3,104,52,0,526,527,5,92,0,0,527,528,5,126,0,0,528, - 529,3,104,52,0,529,530,5,144,0,0,530,534,1,0,0,0,531,532,5,92,0, - 0,532,534,3,104,52,0,533,524,1,0,0,0,533,526,1,0,0,0,533,531,1,0, - 0,0,534,75,1,0,0,0,535,536,5,75,0,0,536,539,3,82,41,0,537,538,5, - 59,0,0,538,540,3,82,41,0,539,537,1,0,0,0,539,540,1,0,0,0,540,77, - 1,0,0,0,541,546,3,80,40,0,542,543,5,112,0,0,543,545,3,80,40,0,544, - 542,1,0,0,0,545,548,1,0,0,0,546,544,1,0,0,0,546,547,1,0,0,0,547, - 79,1,0,0,0,548,546,1,0,0,0,549,551,3,106,53,0,550,552,7,6,0,0,551, - 550,1,0,0,0,551,552,1,0,0,0,552,555,1,0,0,0,553,554,5,58,0,0,554, - 556,7,7,0,0,555,553,1,0,0,0,555,556,1,0,0,0,556,559,1,0,0,0,557, - 558,5,15,0,0,558,560,5,106,0,0,559,557,1,0,0,0,559,560,1,0,0,0,560, - 81,1,0,0,0,561,568,3,154,77,0,562,565,3,138,69,0,563,564,5,146,0, - 0,564,566,3,138,69,0,565,563,1,0,0,0,565,566,1,0,0,0,566,568,1,0, - 0,0,567,561,1,0,0,0,567,562,1,0,0,0,568,83,1,0,0,0,569,574,3,86, - 43,0,570,571,5,112,0,0,571,573,3,86,43,0,572,570,1,0,0,0,573,576, - 1,0,0,0,574,572,1,0,0,0,574,575,1,0,0,0,575,85,1,0,0,0,576,574,1, - 0,0,0,577,578,3,150,75,0,578,579,5,118,0,0,579,580,3,140,70,0,580, - 87,1,0,0,0,581,583,3,90,45,0,582,581,1,0,0,0,582,583,1,0,0,0,583, - 585,1,0,0,0,584,586,3,92,46,0,585,584,1,0,0,0,585,586,1,0,0,0,586, - 588,1,0,0,0,587,589,3,94,47,0,588,587,1,0,0,0,588,589,1,0,0,0,589, - 89,1,0,0,0,590,591,5,65,0,0,591,592,5,11,0,0,592,593,3,104,52,0, - 593,91,1,0,0,0,594,595,5,62,0,0,595,596,5,11,0,0,596,597,3,78,39, - 0,597,93,1,0,0,0,598,599,7,8,0,0,599,600,3,96,48,0,600,95,1,0,0, - 0,601,608,3,98,49,0,602,603,5,9,0,0,603,604,3,98,49,0,604,605,5, - 2,0,0,605,606,3,98,49,0,606,608,1,0,0,0,607,601,1,0,0,0,607,602, - 1,0,0,0,608,97,1,0,0,0,609,610,5,18,0,0,610,622,5,73,0,0,611,612, - 5,90,0,0,612,622,5,66,0,0,613,614,5,90,0,0,614,622,5,30,0,0,615, - 616,3,138,69,0,616,617,5,66,0,0,617,622,1,0,0,0,618,619,3,138,69, - 0,619,620,5,30,0,0,620,622,1,0,0,0,621,609,1,0,0,0,621,611,1,0,0, - 0,621,613,1,0,0,0,621,615,1,0,0,0,621,618,1,0,0,0,622,99,1,0,0,0, - 623,624,3,106,53,0,624,625,5,0,0,1,625,101,1,0,0,0,626,674,3,150, - 75,0,627,628,3,150,75,0,628,629,5,126,0,0,629,630,3,150,75,0,630, - 637,3,102,51,0,631,632,5,112,0,0,632,633,3,150,75,0,633,634,3,102, - 51,0,634,636,1,0,0,0,635,631,1,0,0,0,636,639,1,0,0,0,637,635,1,0, - 0,0,637,638,1,0,0,0,638,640,1,0,0,0,639,637,1,0,0,0,640,641,5,144, - 0,0,641,674,1,0,0,0,642,643,3,150,75,0,643,644,5,126,0,0,644,649, - 3,152,76,0,645,646,5,112,0,0,646,648,3,152,76,0,647,645,1,0,0,0, - 648,651,1,0,0,0,649,647,1,0,0,0,649,650,1,0,0,0,650,652,1,0,0,0, - 651,649,1,0,0,0,652,653,5,144,0,0,653,674,1,0,0,0,654,655,3,150, - 75,0,655,656,5,126,0,0,656,661,3,102,51,0,657,658,5,112,0,0,658, - 660,3,102,51,0,659,657,1,0,0,0,660,663,1,0,0,0,661,659,1,0,0,0,661, - 662,1,0,0,0,662,664,1,0,0,0,663,661,1,0,0,0,664,665,5,144,0,0,665, - 674,1,0,0,0,666,667,3,150,75,0,667,669,5,126,0,0,668,670,3,104,52, - 0,669,668,1,0,0,0,669,670,1,0,0,0,670,671,1,0,0,0,671,672,5,144, - 0,0,672,674,1,0,0,0,673,626,1,0,0,0,673,627,1,0,0,0,673,642,1,0, - 0,0,673,654,1,0,0,0,673,666,1,0,0,0,674,103,1,0,0,0,675,680,3,106, - 53,0,676,677,5,112,0,0,677,679,3,106,53,0,678,676,1,0,0,0,679,682, - 1,0,0,0,680,678,1,0,0,0,680,681,1,0,0,0,681,105,1,0,0,0,682,680, - 1,0,0,0,683,684,6,53,-1,0,684,686,5,12,0,0,685,687,3,106,53,0,686, - 685,1,0,0,0,686,687,1,0,0,0,687,693,1,0,0,0,688,689,5,94,0,0,689, - 690,3,106,53,0,690,691,5,81,0,0,691,692,3,106,53,0,692,694,1,0,0, - 0,693,688,1,0,0,0,694,695,1,0,0,0,695,693,1,0,0,0,695,696,1,0,0, - 0,696,699,1,0,0,0,697,698,5,24,0,0,698,700,3,106,53,0,699,697,1, - 0,0,0,699,700,1,0,0,0,700,701,1,0,0,0,701,702,5,25,0,0,702,833,1, - 0,0,0,703,704,5,13,0,0,704,705,5,126,0,0,705,706,3,106,53,0,706, - 707,5,6,0,0,707,708,3,102,51,0,708,709,5,144,0,0,709,833,1,0,0,0, - 710,711,5,19,0,0,711,833,5,106,0,0,712,713,5,43,0,0,713,714,3,106, - 53,0,714,715,3,142,71,0,715,833,1,0,0,0,716,717,5,80,0,0,717,718, - 5,126,0,0,718,719,3,106,53,0,719,720,5,32,0,0,720,723,3,106,53,0, - 721,722,5,31,0,0,722,724,3,106,53,0,723,721,1,0,0,0,723,724,1,0, - 0,0,724,725,1,0,0,0,725,726,5,144,0,0,726,833,1,0,0,0,727,728,5, - 83,0,0,728,833,5,106,0,0,729,730,5,88,0,0,730,731,5,126,0,0,731, - 732,7,9,0,0,732,733,3,156,78,0,733,734,5,32,0,0,734,735,3,106,53, - 0,735,736,5,144,0,0,736,833,1,0,0,0,737,738,3,150,75,0,738,740,5, - 126,0,0,739,741,3,104,52,0,740,739,1,0,0,0,740,741,1,0,0,0,741,742, - 1,0,0,0,742,743,5,144,0,0,743,752,1,0,0,0,744,746,5,126,0,0,745, - 747,5,23,0,0,746,745,1,0,0,0,746,747,1,0,0,0,747,749,1,0,0,0,748, - 750,3,108,54,0,749,748,1,0,0,0,749,750,1,0,0,0,750,751,1,0,0,0,751, - 753,5,144,0,0,752,744,1,0,0,0,752,753,1,0,0,0,753,754,1,0,0,0,754, - 755,5,64,0,0,755,756,5,126,0,0,756,757,3,88,44,0,757,758,5,144,0, - 0,758,833,1,0,0,0,759,760,3,150,75,0,760,762,5,126,0,0,761,763,3, - 104,52,0,762,761,1,0,0,0,762,763,1,0,0,0,763,764,1,0,0,0,764,765, - 5,144,0,0,765,774,1,0,0,0,766,768,5,126,0,0,767,769,5,23,0,0,768, - 767,1,0,0,0,768,769,1,0,0,0,769,771,1,0,0,0,770,772,3,108,54,0,771, - 770,1,0,0,0,771,772,1,0,0,0,772,773,1,0,0,0,773,775,5,144,0,0,774, - 766,1,0,0,0,774,775,1,0,0,0,775,776,1,0,0,0,776,777,5,64,0,0,777, - 778,3,150,75,0,778,833,1,0,0,0,779,785,3,150,75,0,780,782,5,126, - 0,0,781,783,3,104,52,0,782,781,1,0,0,0,782,783,1,0,0,0,783,784,1, - 0,0,0,784,786,5,144,0,0,785,780,1,0,0,0,785,786,1,0,0,0,786,787, - 1,0,0,0,787,789,5,126,0,0,788,790,5,23,0,0,789,788,1,0,0,0,789,790, - 1,0,0,0,790,792,1,0,0,0,791,793,3,108,54,0,792,791,1,0,0,0,792,793, - 1,0,0,0,793,794,1,0,0,0,794,795,5,144,0,0,795,833,1,0,0,0,796,833, - 3,114,57,0,797,833,3,158,79,0,798,833,3,140,70,0,799,800,5,114,0, - 0,800,833,3,106,53,19,801,802,5,56,0,0,802,833,3,106,53,13,803,804, - 3,130,65,0,804,805,5,116,0,0,805,807,1,0,0,0,806,803,1,0,0,0,806, - 807,1,0,0,0,807,808,1,0,0,0,808,833,5,108,0,0,809,810,5,126,0,0, - 810,811,3,34,17,0,811,812,5,144,0,0,812,833,1,0,0,0,813,814,5,126, - 0,0,814,815,3,106,53,0,815,816,5,144,0,0,816,833,1,0,0,0,817,818, - 5,126,0,0,818,819,3,104,52,0,819,820,5,144,0,0,820,833,1,0,0,0,821, - 823,5,125,0,0,822,824,3,104,52,0,823,822,1,0,0,0,823,824,1,0,0,0, - 824,825,1,0,0,0,825,833,5,143,0,0,826,828,5,124,0,0,827,829,3,30, - 15,0,828,827,1,0,0,0,828,829,1,0,0,0,829,830,1,0,0,0,830,833,5,142, - 0,0,831,833,3,122,61,0,832,683,1,0,0,0,832,703,1,0,0,0,832,710,1, - 0,0,0,832,712,1,0,0,0,832,716,1,0,0,0,832,727,1,0,0,0,832,729,1, - 0,0,0,832,737,1,0,0,0,832,759,1,0,0,0,832,779,1,0,0,0,832,796,1, - 0,0,0,832,797,1,0,0,0,832,798,1,0,0,0,832,799,1,0,0,0,832,801,1, - 0,0,0,832,806,1,0,0,0,832,809,1,0,0,0,832,813,1,0,0,0,832,817,1, - 0,0,0,832,821,1,0,0,0,832,826,1,0,0,0,832,831,1,0,0,0,833,927,1, - 0,0,0,834,838,10,18,0,0,835,839,5,108,0,0,836,839,5,146,0,0,837, - 839,5,133,0,0,838,835,1,0,0,0,838,836,1,0,0,0,838,837,1,0,0,0,839, - 840,1,0,0,0,840,926,3,106,53,19,841,845,10,17,0,0,842,846,5,134, - 0,0,843,846,5,114,0,0,844,846,5,113,0,0,845,842,1,0,0,0,845,843, - 1,0,0,0,845,844,1,0,0,0,846,847,1,0,0,0,847,926,3,106,53,18,848, - 873,10,16,0,0,849,874,5,117,0,0,850,874,5,118,0,0,851,874,5,129, - 0,0,852,874,5,127,0,0,853,874,5,128,0,0,854,874,5,119,0,0,855,874, - 5,120,0,0,856,858,5,56,0,0,857,856,1,0,0,0,857,858,1,0,0,0,858,859, - 1,0,0,0,859,861,5,40,0,0,860,862,5,14,0,0,861,860,1,0,0,0,861,862, - 1,0,0,0,862,874,1,0,0,0,863,865,5,56,0,0,864,863,1,0,0,0,864,865, - 1,0,0,0,865,866,1,0,0,0,866,874,7,10,0,0,867,874,5,140,0,0,868,874, - 5,141,0,0,869,874,5,131,0,0,870,874,5,122,0,0,871,874,5,123,0,0, - 872,874,5,130,0,0,873,849,1,0,0,0,873,850,1,0,0,0,873,851,1,0,0, - 0,873,852,1,0,0,0,873,853,1,0,0,0,873,854,1,0,0,0,873,855,1,0,0, - 0,873,857,1,0,0,0,873,864,1,0,0,0,873,867,1,0,0,0,873,868,1,0,0, - 0,873,869,1,0,0,0,873,870,1,0,0,0,873,871,1,0,0,0,873,872,1,0,0, - 0,874,875,1,0,0,0,875,926,3,106,53,17,876,877,10,14,0,0,877,878, - 5,132,0,0,878,926,3,106,53,15,879,880,10,12,0,0,880,881,5,2,0,0, - 881,926,3,106,53,13,882,883,10,11,0,0,883,884,5,61,0,0,884,926,3, - 106,53,12,885,887,10,10,0,0,886,888,5,56,0,0,887,886,1,0,0,0,887, - 888,1,0,0,0,888,889,1,0,0,0,889,890,5,9,0,0,890,891,3,106,53,0,891, - 892,5,2,0,0,892,893,3,106,53,11,893,926,1,0,0,0,894,895,10,9,0,0, - 895,896,5,135,0,0,896,897,3,106,53,0,897,898,5,111,0,0,898,899,3, - 106,53,9,899,926,1,0,0,0,900,901,10,22,0,0,901,902,5,125,0,0,902, - 903,3,106,53,0,903,904,5,143,0,0,904,926,1,0,0,0,905,906,10,21,0, - 0,906,907,5,116,0,0,907,926,5,104,0,0,908,909,10,20,0,0,909,910, - 5,116,0,0,910,926,3,150,75,0,911,912,10,15,0,0,912,914,5,44,0,0, - 913,915,5,56,0,0,914,913,1,0,0,0,914,915,1,0,0,0,915,916,1,0,0,0, - 916,926,5,57,0,0,917,923,10,8,0,0,918,924,3,148,74,0,919,920,5,6, - 0,0,920,924,3,150,75,0,921,922,5,6,0,0,922,924,5,106,0,0,923,918, - 1,0,0,0,923,919,1,0,0,0,923,921,1,0,0,0,924,926,1,0,0,0,925,834, - 1,0,0,0,925,841,1,0,0,0,925,848,1,0,0,0,925,876,1,0,0,0,925,879, - 1,0,0,0,925,882,1,0,0,0,925,885,1,0,0,0,925,894,1,0,0,0,925,900, - 1,0,0,0,925,905,1,0,0,0,925,908,1,0,0,0,925,911,1,0,0,0,925,917, - 1,0,0,0,926,929,1,0,0,0,927,925,1,0,0,0,927,928,1,0,0,0,928,107, - 1,0,0,0,929,927,1,0,0,0,930,935,3,110,55,0,931,932,5,112,0,0,932, - 934,3,110,55,0,933,931,1,0,0,0,934,937,1,0,0,0,935,933,1,0,0,0,935, - 936,1,0,0,0,936,109,1,0,0,0,937,935,1,0,0,0,938,941,3,112,56,0,939, - 941,3,106,53,0,940,938,1,0,0,0,940,939,1,0,0,0,941,111,1,0,0,0,942, - 943,5,126,0,0,943,948,3,150,75,0,944,945,5,112,0,0,945,947,3,150, - 75,0,946,944,1,0,0,0,947,950,1,0,0,0,948,946,1,0,0,0,948,949,1,0, - 0,0,949,951,1,0,0,0,950,948,1,0,0,0,951,952,5,144,0,0,952,962,1, - 0,0,0,953,958,3,150,75,0,954,955,5,112,0,0,955,957,3,150,75,0,956, - 954,1,0,0,0,957,960,1,0,0,0,958,956,1,0,0,0,958,959,1,0,0,0,959, - 962,1,0,0,0,960,958,1,0,0,0,961,942,1,0,0,0,961,953,1,0,0,0,962, - 963,1,0,0,0,963,964,5,107,0,0,964,965,3,106,53,0,965,113,1,0,0,0, - 966,967,5,128,0,0,967,971,3,150,75,0,968,970,3,116,58,0,969,968, - 1,0,0,0,970,973,1,0,0,0,971,969,1,0,0,0,971,972,1,0,0,0,972,974, - 1,0,0,0,973,971,1,0,0,0,974,975,5,146,0,0,975,976,5,120,0,0,976, - 995,1,0,0,0,977,978,5,128,0,0,978,982,3,150,75,0,979,981,3,116,58, - 0,980,979,1,0,0,0,981,984,1,0,0,0,982,980,1,0,0,0,982,983,1,0,0, - 0,983,985,1,0,0,0,984,982,1,0,0,0,985,987,5,120,0,0,986,988,3,114, - 57,0,987,986,1,0,0,0,987,988,1,0,0,0,988,989,1,0,0,0,989,990,5,128, - 0,0,990,991,5,146,0,0,991,992,3,150,75,0,992,993,5,120,0,0,993,995, - 1,0,0,0,994,966,1,0,0,0,994,977,1,0,0,0,995,115,1,0,0,0,996,997, - 3,150,75,0,997,998,5,118,0,0,998,999,3,156,78,0,999,1008,1,0,0,0, - 1000,1001,3,150,75,0,1001,1002,5,118,0,0,1002,1003,5,124,0,0,1003, - 1004,3,106,53,0,1004,1005,5,142,0,0,1005,1008,1,0,0,0,1006,1008, - 3,150,75,0,1007,996,1,0,0,0,1007,1000,1,0,0,0,1007,1006,1,0,0,0, - 1008,117,1,0,0,0,1009,1014,3,120,60,0,1010,1011,5,112,0,0,1011,1013, - 3,120,60,0,1012,1010,1,0,0,0,1013,1016,1,0,0,0,1014,1012,1,0,0,0, - 1014,1015,1,0,0,0,1015,119,1,0,0,0,1016,1014,1,0,0,0,1017,1018,3, - 150,75,0,1018,1019,5,6,0,0,1019,1020,5,126,0,0,1020,1021,3,34,17, - 0,1021,1022,5,144,0,0,1022,1028,1,0,0,0,1023,1024,3,106,53,0,1024, - 1025,5,6,0,0,1025,1026,3,150,75,0,1026,1028,1,0,0,0,1027,1017,1, - 0,0,0,1027,1023,1,0,0,0,1028,121,1,0,0,0,1029,1037,3,154,77,0,1030, - 1031,3,130,65,0,1031,1032,5,116,0,0,1032,1034,1,0,0,0,1033,1030, - 1,0,0,0,1033,1034,1,0,0,0,1034,1035,1,0,0,0,1035,1037,3,124,62,0, - 1036,1029,1,0,0,0,1036,1033,1,0,0,0,1037,123,1,0,0,0,1038,1043,3, - 150,75,0,1039,1040,5,116,0,0,1040,1042,3,150,75,0,1041,1039,1,0, - 0,0,1042,1045,1,0,0,0,1043,1041,1,0,0,0,1043,1044,1,0,0,0,1044,125, - 1,0,0,0,1045,1043,1,0,0,0,1046,1047,6,63,-1,0,1047,1056,3,130,65, - 0,1048,1056,3,128,64,0,1049,1050,5,126,0,0,1050,1051,3,34,17,0,1051, - 1052,5,144,0,0,1052,1056,1,0,0,0,1053,1056,3,114,57,0,1054,1056, - 3,154,77,0,1055,1046,1,0,0,0,1055,1048,1,0,0,0,1055,1049,1,0,0,0, - 1055,1053,1,0,0,0,1055,1054,1,0,0,0,1056,1065,1,0,0,0,1057,1061, - 10,3,0,0,1058,1062,3,148,74,0,1059,1060,5,6,0,0,1060,1062,3,150, - 75,0,1061,1058,1,0,0,0,1061,1059,1,0,0,0,1062,1064,1,0,0,0,1063, - 1057,1,0,0,0,1064,1067,1,0,0,0,1065,1063,1,0,0,0,1065,1066,1,0,0, - 0,1066,127,1,0,0,0,1067,1065,1,0,0,0,1068,1069,3,150,75,0,1069,1071, - 5,126,0,0,1070,1072,3,132,66,0,1071,1070,1,0,0,0,1071,1072,1,0,0, - 0,1072,1073,1,0,0,0,1073,1074,5,144,0,0,1074,129,1,0,0,0,1075,1076, - 3,134,67,0,1076,1077,5,116,0,0,1077,1079,1,0,0,0,1078,1075,1,0,0, - 0,1078,1079,1,0,0,0,1079,1080,1,0,0,0,1080,1081,3,150,75,0,1081, - 131,1,0,0,0,1082,1087,3,106,53,0,1083,1084,5,112,0,0,1084,1086,3, - 106,53,0,1085,1083,1,0,0,0,1086,1089,1,0,0,0,1087,1085,1,0,0,0,1087, - 1088,1,0,0,0,1088,133,1,0,0,0,1089,1087,1,0,0,0,1090,1091,3,150, - 75,0,1091,135,1,0,0,0,1092,1101,5,102,0,0,1093,1094,5,116,0,0,1094, - 1101,7,11,0,0,1095,1096,5,104,0,0,1096,1098,5,116,0,0,1097,1099, - 7,11,0,0,1098,1097,1,0,0,0,1098,1099,1,0,0,0,1099,1101,1,0,0,0,1100, - 1092,1,0,0,0,1100,1093,1,0,0,0,1100,1095,1,0,0,0,1101,137,1,0,0, - 0,1102,1104,7,12,0,0,1103,1102,1,0,0,0,1103,1104,1,0,0,0,1104,1111, - 1,0,0,0,1105,1112,3,136,68,0,1106,1112,5,103,0,0,1107,1112,5,104, - 0,0,1108,1112,5,105,0,0,1109,1112,5,41,0,0,1110,1112,5,55,0,0,1111, - 1105,1,0,0,0,1111,1106,1,0,0,0,1111,1107,1,0,0,0,1111,1108,1,0,0, - 0,1111,1109,1,0,0,0,1111,1110,1,0,0,0,1112,139,1,0,0,0,1113,1117, - 3,138,69,0,1114,1117,5,106,0,0,1115,1117,5,57,0,0,1116,1113,1,0, - 0,0,1116,1114,1,0,0,0,1116,1115,1,0,0,0,1117,141,1,0,0,0,1118,1119, - 7,13,0,0,1119,143,1,0,0,0,1120,1121,7,14,0,0,1121,145,1,0,0,0,1122, - 1123,7,15,0,0,1123,147,1,0,0,0,1124,1127,5,101,0,0,1125,1127,3,146, - 73,0,1126,1124,1,0,0,0,1126,1125,1,0,0,0,1127,149,1,0,0,0,1128,1132, - 5,101,0,0,1129,1132,3,142,71,0,1130,1132,3,144,72,0,1131,1128,1, - 0,0,0,1131,1129,1,0,0,0,1131,1130,1,0,0,0,1132,151,1,0,0,0,1133, - 1134,3,156,78,0,1134,1135,5,118,0,0,1135,1136,3,138,69,0,1136,153, - 1,0,0,0,1137,1138,5,124,0,0,1138,1139,3,150,75,0,1139,1140,5,142, - 0,0,1140,155,1,0,0,0,1141,1144,5,106,0,0,1142,1144,3,158,79,0,1143, - 1141,1,0,0,0,1143,1142,1,0,0,0,1144,157,1,0,0,0,1145,1149,5,137, - 0,0,1146,1148,3,160,80,0,1147,1146,1,0,0,0,1148,1151,1,0,0,0,1149, - 1147,1,0,0,0,1149,1150,1,0,0,0,1150,1152,1,0,0,0,1151,1149,1,0,0, - 0,1152,1153,5,139,0,0,1153,159,1,0,0,0,1154,1155,5,152,0,0,1155, - 1156,3,106,53,0,1156,1157,5,142,0,0,1157,1160,1,0,0,0,1158,1160, - 5,151,0,0,1159,1154,1,0,0,0,1159,1158,1,0,0,0,1160,161,1,0,0,0,1161, - 1165,5,138,0,0,1162,1164,3,164,82,0,1163,1162,1,0,0,0,1164,1167, - 1,0,0,0,1165,1163,1,0,0,0,1165,1166,1,0,0,0,1166,1168,1,0,0,0,1167, - 1165,1,0,0,0,1168,1169,5,0,0,1,1169,163,1,0,0,0,1170,1171,5,154, - 0,0,1171,1172,3,106,53,0,1172,1173,5,142,0,0,1173,1176,1,0,0,0,1174, - 1176,5,153,0,0,1175,1170,1,0,0,0,1175,1174,1,0,0,0,1176,165,1,0, - 0,0,141,169,176,185,200,212,224,240,251,265,271,281,290,293,297, - 300,304,307,310,313,316,320,324,327,330,333,337,340,349,355,376, - 393,410,416,422,433,435,446,449,455,463,469,471,475,480,483,486, - 490,494,497,499,502,506,510,513,515,517,522,533,539,546,551,555, - 559,565,567,574,582,585,588,607,621,637,649,661,669,673,680,686, - 695,699,723,740,746,749,752,762,768,771,774,782,785,789,792,806, - 823,828,832,838,845,857,861,864,873,887,914,923,925,927,935,940, - 948,958,961,971,982,987,994,1007,1014,1027,1033,1036,1043,1055,1061, - 1065,1071,1078,1087,1098,1100,1103,1111,1116,1126,1131,1143,1149, - 1159,1165,1175 + 1,53,1,53,3,53,726,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,743,8,53,1,53,1,53,1,53, + 1,53,3,53,749,8,53,1,53,3,53,752,8,53,1,53,3,53,755,8,53,1,53,1, + 53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,765,8,53,1,53,1,53,1,53,1, + 53,3,53,771,8,53,1,53,3,53,774,8,53,1,53,3,53,777,8,53,1,53,1,53, + 1,53,1,53,1,53,1,53,3,53,785,8,53,1,53,3,53,788,8,53,1,53,1,53,3, + 53,792,8,53,1,53,3,53,795,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,3,53,809,8,53,1,53,1,53,1,53,1,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,826,8,53, + 1,53,1,53,1,53,3,53,831,8,53,1,53,1,53,3,53,835,8,53,1,53,1,53,1, + 53,1,53,3,53,841,8,53,1,53,1,53,1,53,1,53,1,53,3,53,848,8,53,1,53, + 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,3,53,860,8,53,1,53, + 1,53,3,53,864,8,53,1,53,3,53,867,8,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,3,53,876,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,3,53,890,8,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,1,53,1,53,3,53,917,8,53,1,53,1,53,1,53,1,53,1, + 53,1,53,3,53,925,8,53,5,53,927,8,53,10,53,12,53,930,9,53,1,54,1, + 54,1,54,5,54,935,8,54,10,54,12,54,938,9,54,1,55,1,55,3,55,942,8, + 55,1,56,1,56,1,56,1,56,5,56,948,8,56,10,56,12,56,951,9,56,1,56,1, + 56,1,56,1,56,1,56,5,56,958,8,56,10,56,12,56,961,9,56,3,56,963,8, + 56,1,56,1,56,1,56,1,57,1,57,1,57,5,57,971,8,57,10,57,12,57,974,9, + 57,1,57,1,57,1,57,1,57,1,57,1,57,5,57,982,8,57,10,57,12,57,985,9, + 57,1,57,1,57,3,57,989,8,57,1,57,1,57,1,57,1,57,1,57,3,57,996,8,57, + 1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,3,58,1009, + 8,58,1,59,1,59,1,59,5,59,1014,8,59,10,59,12,59,1017,9,59,1,60,1, + 60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,3,60,1029,8,60,1,61,1, + 61,1,61,1,61,3,61,1035,8,61,1,61,3,61,1038,8,61,1,62,1,62,1,62,5, + 62,1043,8,62,10,62,12,62,1046,9,62,1,63,1,63,1,63,1,63,1,63,1,63, + 1,63,1,63,1,63,3,63,1057,8,63,1,63,1,63,1,63,1,63,3,63,1063,8,63, + 5,63,1065,8,63,10,63,12,63,1068,9,63,1,64,1,64,1,64,3,64,1073,8, + 64,1,64,1,64,1,65,1,65,1,65,3,65,1080,8,65,1,65,1,65,1,66,1,66,1, + 66,5,66,1087,8,66,10,66,12,66,1090,9,66,1,67,1,67,1,68,1,68,1,68, + 1,68,1,68,1,68,3,68,1100,8,68,3,68,1102,8,68,1,69,3,69,1105,8,69, + 1,69,1,69,1,69,1,69,1,69,1,69,3,69,1113,8,69,1,70,1,70,1,70,3,70, + 1118,8,70,1,71,1,71,1,72,1,72,1,73,1,73,1,74,1,74,3,74,1128,8,74, + 1,75,1,75,1,75,3,75,1133,8,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77, + 1,77,1,78,1,78,3,78,1145,8,78,1,79,1,79,5,79,1149,8,79,10,79,12, + 79,1152,9,79,1,79,1,79,1,80,1,80,1,80,1,80,1,80,3,80,1161,8,80,1, + 81,1,81,5,81,1165,8,81,10,81,12,81,1168,9,81,1,81,1,81,1,82,1,82, + 1,82,1,82,1,82,3,82,1177,8,82,1,82,0,3,68,106,126,83,0,2,4,6,8,10, + 12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54, + 56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98, + 100,102,104,106,108,110,112,114,116,118,120,122,124,126,128,130, + 132,134,136,138,140,142,144,146,148,150,152,154,156,158,160,162, + 164,0,16,2,0,17,17,72,72,2,0,42,42,49,49,3,0,1,1,4,4,8,8,4,0,1,1, + 3,4,8,8,78,78,2,0,49,49,71,71,2,0,1,1,4,4,2,0,7,7,21,22,2,0,28,28, + 47,47,2,0,69,69,74,74,3,0,10,10,48,48,87,87,2,0,39,39,51,51,1,0, + 103,104,2,0,114,114,134,134,7,0,20,20,36,36,53,54,68,68,76,76,93, + 93,99,99,12,0,1,19,21,28,30,35,37,40,42,49,51,52,56,56,58,67,69, + 75,77,92,94,95,97,98,4,0,19,19,28,28,37,37,46,46,1317,0,169,1,0, + 0,0,2,176,1,0,0,0,4,178,1,0,0,0,6,180,1,0,0,0,8,187,1,0,0,0,10,203, + 1,0,0,0,12,205,1,0,0,0,14,212,1,0,0,0,16,221,1,0,0,0,18,229,1,0, + 0,0,20,238,1,0,0,0,22,243,1,0,0,0,24,247,1,0,0,0,26,249,1,0,0,0, + 28,258,1,0,0,0,30,262,1,0,0,0,32,273,1,0,0,0,34,277,1,0,0,0,36,292, + 1,0,0,0,38,295,1,0,0,0,40,344,1,0,0,0,42,347,1,0,0,0,44,353,1,0, + 0,0,46,357,1,0,0,0,48,363,1,0,0,0,50,381,1,0,0,0,52,384,1,0,0,0, + 54,387,1,0,0,0,56,397,1,0,0,0,58,400,1,0,0,0,60,404,1,0,0,0,62,437, + 1,0,0,0,64,439,1,0,0,0,66,442,1,0,0,0,68,457,1,0,0,0,70,519,1,0, + 0,0,72,524,1,0,0,0,74,535,1,0,0,0,76,537,1,0,0,0,78,543,1,0,0,0, + 80,551,1,0,0,0,82,569,1,0,0,0,84,571,1,0,0,0,86,579,1,0,0,0,88,584, + 1,0,0,0,90,592,1,0,0,0,92,596,1,0,0,0,94,600,1,0,0,0,96,609,1,0, + 0,0,98,623,1,0,0,0,100,625,1,0,0,0,102,675,1,0,0,0,104,677,1,0,0, + 0,106,834,1,0,0,0,108,931,1,0,0,0,110,941,1,0,0,0,112,962,1,0,0, + 0,114,995,1,0,0,0,116,1008,1,0,0,0,118,1010,1,0,0,0,120,1028,1,0, + 0,0,122,1037,1,0,0,0,124,1039,1,0,0,0,126,1056,1,0,0,0,128,1069, + 1,0,0,0,130,1079,1,0,0,0,132,1083,1,0,0,0,134,1091,1,0,0,0,136,1101, + 1,0,0,0,138,1104,1,0,0,0,140,1117,1,0,0,0,142,1119,1,0,0,0,144,1121, + 1,0,0,0,146,1123,1,0,0,0,148,1127,1,0,0,0,150,1132,1,0,0,0,152,1134, + 1,0,0,0,154,1138,1,0,0,0,156,1144,1,0,0,0,158,1146,1,0,0,0,160,1160, + 1,0,0,0,162,1162,1,0,0,0,164,1176,1,0,0,0,166,168,3,2,1,0,167,166, + 1,0,0,0,168,171,1,0,0,0,169,167,1,0,0,0,169,170,1,0,0,0,170,172, + 1,0,0,0,171,169,1,0,0,0,172,173,5,0,0,1,173,1,1,0,0,0,174,177,3, + 6,3,0,175,177,3,10,5,0,176,174,1,0,0,0,176,175,1,0,0,0,177,3,1,0, + 0,0,178,179,3,106,53,0,179,5,1,0,0,0,180,181,5,50,0,0,181,185,3, + 150,75,0,182,183,5,111,0,0,183,184,5,118,0,0,184,186,3,4,2,0,185, + 182,1,0,0,0,185,186,1,0,0,0,186,7,1,0,0,0,187,192,3,150,75,0,188, + 189,5,112,0,0,189,191,3,150,75,0,190,188,1,0,0,0,191,194,1,0,0,0, + 192,190,1,0,0,0,192,193,1,0,0,0,193,9,1,0,0,0,194,192,1,0,0,0,195, + 204,3,12,6,0,196,204,3,14,7,0,197,204,3,16,8,0,198,204,3,18,9,0, + 199,204,3,20,10,0,200,204,3,22,11,0,201,204,3,24,12,0,202,204,3, + 26,13,0,203,195,1,0,0,0,203,196,1,0,0,0,203,197,1,0,0,0,203,198, + 1,0,0,0,203,199,1,0,0,0,203,200,1,0,0,0,203,201,1,0,0,0,203,202, + 1,0,0,0,204,11,1,0,0,0,205,207,5,70,0,0,206,208,3,4,2,0,207,206, + 1,0,0,0,207,208,1,0,0,0,208,210,1,0,0,0,209,211,5,145,0,0,210,209, + 1,0,0,0,210,211,1,0,0,0,211,13,1,0,0,0,212,213,5,38,0,0,213,214, + 5,126,0,0,214,215,3,4,2,0,215,216,5,144,0,0,216,219,3,10,5,0,217, + 218,5,24,0,0,218,220,3,10,5,0,219,217,1,0,0,0,219,220,1,0,0,0,220, + 15,1,0,0,0,221,222,5,96,0,0,222,223,5,126,0,0,223,224,3,4,2,0,224, + 225,5,144,0,0,225,227,3,10,5,0,226,228,5,145,0,0,227,226,1,0,0,0, + 227,228,1,0,0,0,228,17,1,0,0,0,229,230,5,29,0,0,230,231,3,150,75, + 0,231,233,5,126,0,0,232,234,3,8,4,0,233,232,1,0,0,0,233,234,1,0, + 0,0,234,235,1,0,0,0,235,236,5,144,0,0,236,237,3,26,13,0,237,19,1, + 0,0,0,238,239,3,4,2,0,239,240,5,111,0,0,240,241,5,118,0,0,241,242, + 3,4,2,0,242,21,1,0,0,0,243,245,3,4,2,0,244,246,5,145,0,0,245,244, + 1,0,0,0,245,246,1,0,0,0,246,23,1,0,0,0,247,248,5,145,0,0,248,25, + 1,0,0,0,249,253,5,124,0,0,250,252,3,2,1,0,251,250,1,0,0,0,252,255, + 1,0,0,0,253,251,1,0,0,0,253,254,1,0,0,0,254,256,1,0,0,0,255,253, + 1,0,0,0,256,257,5,142,0,0,257,27,1,0,0,0,258,259,3,4,2,0,259,260, + 5,111,0,0,260,261,3,4,2,0,261,29,1,0,0,0,262,267,3,28,14,0,263,264, + 5,112,0,0,264,266,3,28,14,0,265,263,1,0,0,0,266,269,1,0,0,0,267, + 265,1,0,0,0,267,268,1,0,0,0,268,31,1,0,0,0,269,267,1,0,0,0,270,274, + 3,34,17,0,271,274,3,38,19,0,272,274,3,114,57,0,273,270,1,0,0,0,273, + 271,1,0,0,0,273,272,1,0,0,0,274,275,1,0,0,0,275,276,5,0,0,1,276, + 33,1,0,0,0,277,283,3,36,18,0,278,279,5,91,0,0,279,280,5,1,0,0,280, + 282,3,36,18,0,281,278,1,0,0,0,282,285,1,0,0,0,283,281,1,0,0,0,283, + 284,1,0,0,0,284,35,1,0,0,0,285,283,1,0,0,0,286,293,3,38,19,0,287, + 288,5,126,0,0,288,289,3,34,17,0,289,290,5,144,0,0,290,293,1,0,0, + 0,291,293,3,154,77,0,292,286,1,0,0,0,292,287,1,0,0,0,292,291,1,0, + 0,0,293,37,1,0,0,0,294,296,3,40,20,0,295,294,1,0,0,0,295,296,1,0, + 0,0,296,297,1,0,0,0,297,299,5,77,0,0,298,300,5,23,0,0,299,298,1, + 0,0,0,299,300,1,0,0,0,300,302,1,0,0,0,301,303,3,42,21,0,302,301, + 1,0,0,0,302,303,1,0,0,0,303,304,1,0,0,0,304,306,3,104,52,0,305,307, + 3,44,22,0,306,305,1,0,0,0,306,307,1,0,0,0,307,309,1,0,0,0,308,310, + 3,46,23,0,309,308,1,0,0,0,309,310,1,0,0,0,310,312,1,0,0,0,311,313, + 3,50,25,0,312,311,1,0,0,0,312,313,1,0,0,0,313,315,1,0,0,0,314,316, + 3,52,26,0,315,314,1,0,0,0,315,316,1,0,0,0,316,318,1,0,0,0,317,319, + 3,54,27,0,318,317,1,0,0,0,318,319,1,0,0,0,319,322,1,0,0,0,320,321, + 5,98,0,0,321,323,7,0,0,0,322,320,1,0,0,0,322,323,1,0,0,0,323,326, + 1,0,0,0,324,325,5,98,0,0,325,327,5,86,0,0,326,324,1,0,0,0,326,327, + 1,0,0,0,327,329,1,0,0,0,328,330,3,56,28,0,329,328,1,0,0,0,329,330, + 1,0,0,0,330,332,1,0,0,0,331,333,3,48,24,0,332,331,1,0,0,0,332,333, + 1,0,0,0,333,335,1,0,0,0,334,336,3,58,29,0,335,334,1,0,0,0,335,336, + 1,0,0,0,336,339,1,0,0,0,337,340,3,62,31,0,338,340,3,64,32,0,339, + 337,1,0,0,0,339,338,1,0,0,0,339,340,1,0,0,0,340,342,1,0,0,0,341, + 343,3,66,33,0,342,341,1,0,0,0,342,343,1,0,0,0,343,39,1,0,0,0,344, + 345,5,98,0,0,345,346,3,118,59,0,346,41,1,0,0,0,347,348,5,85,0,0, + 348,351,5,104,0,0,349,350,5,98,0,0,350,352,5,82,0,0,351,349,1,0, + 0,0,351,352,1,0,0,0,352,43,1,0,0,0,353,354,5,32,0,0,354,355,3,68, + 34,0,355,45,1,0,0,0,356,358,7,1,0,0,357,356,1,0,0,0,357,358,1,0, + 0,0,358,359,1,0,0,0,359,360,5,5,0,0,360,361,5,45,0,0,361,362,3,104, + 52,0,362,47,1,0,0,0,363,364,5,97,0,0,364,365,3,150,75,0,365,366, + 5,6,0,0,366,367,5,126,0,0,367,368,3,88,44,0,368,378,5,144,0,0,369, + 370,5,112,0,0,370,371,3,150,75,0,371,372,5,6,0,0,372,373,5,126,0, + 0,373,374,3,88,44,0,374,375,5,144,0,0,375,377,1,0,0,0,376,369,1, + 0,0,0,377,380,1,0,0,0,378,376,1,0,0,0,378,379,1,0,0,0,379,49,1,0, + 0,0,380,378,1,0,0,0,381,382,5,67,0,0,382,383,3,106,53,0,383,51,1, + 0,0,0,384,385,5,95,0,0,385,386,3,106,53,0,386,53,1,0,0,0,387,388, + 5,34,0,0,388,395,5,11,0,0,389,390,7,0,0,0,390,391,5,126,0,0,391, + 392,3,104,52,0,392,393,5,144,0,0,393,396,1,0,0,0,394,396,3,104,52, + 0,395,389,1,0,0,0,395,394,1,0,0,0,396,55,1,0,0,0,397,398,5,35,0, + 0,398,399,3,106,53,0,399,57,1,0,0,0,400,401,5,62,0,0,401,402,5,11, + 0,0,402,403,3,78,39,0,403,59,1,0,0,0,404,405,5,62,0,0,405,406,5, + 11,0,0,406,407,3,104,52,0,407,61,1,0,0,0,408,409,5,52,0,0,409,412, + 3,106,53,0,410,411,5,112,0,0,411,413,3,106,53,0,412,410,1,0,0,0, + 412,413,1,0,0,0,413,418,1,0,0,0,414,415,5,98,0,0,415,419,5,82,0, + 0,416,417,5,11,0,0,417,419,3,104,52,0,418,414,1,0,0,0,418,416,1, + 0,0,0,418,419,1,0,0,0,419,438,1,0,0,0,420,421,5,52,0,0,421,424,3, + 106,53,0,422,423,5,98,0,0,423,425,5,82,0,0,424,422,1,0,0,0,424,425, + 1,0,0,0,425,426,1,0,0,0,426,427,5,59,0,0,427,428,3,106,53,0,428, + 438,1,0,0,0,429,430,5,52,0,0,430,431,3,106,53,0,431,432,5,59,0,0, + 432,435,3,106,53,0,433,434,5,11,0,0,434,436,3,104,52,0,435,433,1, + 0,0,0,435,436,1,0,0,0,436,438,1,0,0,0,437,408,1,0,0,0,437,420,1, + 0,0,0,437,429,1,0,0,0,438,63,1,0,0,0,439,440,5,59,0,0,440,441,3, + 106,53,0,441,65,1,0,0,0,442,443,5,79,0,0,443,444,3,84,42,0,444,67, + 1,0,0,0,445,446,6,34,-1,0,446,448,3,126,63,0,447,449,5,27,0,0,448, + 447,1,0,0,0,448,449,1,0,0,0,449,451,1,0,0,0,450,452,3,76,38,0,451, + 450,1,0,0,0,451,452,1,0,0,0,452,458,1,0,0,0,453,454,5,126,0,0,454, + 455,3,68,34,0,455,456,5,144,0,0,456,458,1,0,0,0,457,445,1,0,0,0, + 457,453,1,0,0,0,458,473,1,0,0,0,459,460,10,3,0,0,460,461,3,72,36, + 0,461,462,3,68,34,4,462,472,1,0,0,0,463,465,10,4,0,0,464,466,3,70, + 35,0,465,464,1,0,0,0,465,466,1,0,0,0,466,467,1,0,0,0,467,468,5,45, + 0,0,468,469,3,68,34,0,469,470,3,74,37,0,470,472,1,0,0,0,471,459, + 1,0,0,0,471,463,1,0,0,0,472,475,1,0,0,0,473,471,1,0,0,0,473,474, + 1,0,0,0,474,69,1,0,0,0,475,473,1,0,0,0,476,478,7,2,0,0,477,476,1, + 0,0,0,477,478,1,0,0,0,478,479,1,0,0,0,479,486,5,42,0,0,480,482,5, + 42,0,0,481,483,7,2,0,0,482,481,1,0,0,0,482,483,1,0,0,0,483,486,1, + 0,0,0,484,486,7,2,0,0,485,477,1,0,0,0,485,480,1,0,0,0,485,484,1, + 0,0,0,486,520,1,0,0,0,487,489,7,3,0,0,488,487,1,0,0,0,488,489,1, + 0,0,0,489,490,1,0,0,0,490,492,7,4,0,0,491,493,5,63,0,0,492,491,1, + 0,0,0,492,493,1,0,0,0,493,502,1,0,0,0,494,496,7,4,0,0,495,497,5, + 63,0,0,496,495,1,0,0,0,496,497,1,0,0,0,497,499,1,0,0,0,498,500,7, + 3,0,0,499,498,1,0,0,0,499,500,1,0,0,0,500,502,1,0,0,0,501,488,1, + 0,0,0,501,494,1,0,0,0,502,520,1,0,0,0,503,505,7,5,0,0,504,503,1, + 0,0,0,504,505,1,0,0,0,505,506,1,0,0,0,506,508,5,33,0,0,507,509,5, + 63,0,0,508,507,1,0,0,0,508,509,1,0,0,0,509,518,1,0,0,0,510,512,5, + 33,0,0,511,513,5,63,0,0,512,511,1,0,0,0,512,513,1,0,0,0,513,515, + 1,0,0,0,514,516,7,5,0,0,515,514,1,0,0,0,515,516,1,0,0,0,516,518, + 1,0,0,0,517,504,1,0,0,0,517,510,1,0,0,0,518,520,1,0,0,0,519,485, + 1,0,0,0,519,501,1,0,0,0,519,517,1,0,0,0,520,71,1,0,0,0,521,522,5, + 16,0,0,522,525,5,45,0,0,523,525,5,112,0,0,524,521,1,0,0,0,524,523, + 1,0,0,0,525,73,1,0,0,0,526,527,5,60,0,0,527,536,3,104,52,0,528,529, + 5,92,0,0,529,530,5,126,0,0,530,531,3,104,52,0,531,532,5,144,0,0, + 532,536,1,0,0,0,533,534,5,92,0,0,534,536,3,104,52,0,535,526,1,0, + 0,0,535,528,1,0,0,0,535,533,1,0,0,0,536,75,1,0,0,0,537,538,5,75, + 0,0,538,541,3,82,41,0,539,540,5,59,0,0,540,542,3,82,41,0,541,539, + 1,0,0,0,541,542,1,0,0,0,542,77,1,0,0,0,543,548,3,80,40,0,544,545, + 5,112,0,0,545,547,3,80,40,0,546,544,1,0,0,0,547,550,1,0,0,0,548, + 546,1,0,0,0,548,549,1,0,0,0,549,79,1,0,0,0,550,548,1,0,0,0,551,553, + 3,106,53,0,552,554,7,6,0,0,553,552,1,0,0,0,553,554,1,0,0,0,554,557, + 1,0,0,0,555,556,5,58,0,0,556,558,7,7,0,0,557,555,1,0,0,0,557,558, + 1,0,0,0,558,561,1,0,0,0,559,560,5,15,0,0,560,562,5,106,0,0,561,559, + 1,0,0,0,561,562,1,0,0,0,562,81,1,0,0,0,563,570,3,154,77,0,564,567, + 3,138,69,0,565,566,5,146,0,0,566,568,3,138,69,0,567,565,1,0,0,0, + 567,568,1,0,0,0,568,570,1,0,0,0,569,563,1,0,0,0,569,564,1,0,0,0, + 570,83,1,0,0,0,571,576,3,86,43,0,572,573,5,112,0,0,573,575,3,86, + 43,0,574,572,1,0,0,0,575,578,1,0,0,0,576,574,1,0,0,0,576,577,1,0, + 0,0,577,85,1,0,0,0,578,576,1,0,0,0,579,580,3,150,75,0,580,581,5, + 118,0,0,581,582,3,140,70,0,582,87,1,0,0,0,583,585,3,90,45,0,584, + 583,1,0,0,0,584,585,1,0,0,0,585,587,1,0,0,0,586,588,3,92,46,0,587, + 586,1,0,0,0,587,588,1,0,0,0,588,590,1,0,0,0,589,591,3,94,47,0,590, + 589,1,0,0,0,590,591,1,0,0,0,591,89,1,0,0,0,592,593,5,65,0,0,593, + 594,5,11,0,0,594,595,3,104,52,0,595,91,1,0,0,0,596,597,5,62,0,0, + 597,598,5,11,0,0,598,599,3,78,39,0,599,93,1,0,0,0,600,601,7,8,0, + 0,601,602,3,96,48,0,602,95,1,0,0,0,603,610,3,98,49,0,604,605,5,9, + 0,0,605,606,3,98,49,0,606,607,5,2,0,0,607,608,3,98,49,0,608,610, + 1,0,0,0,609,603,1,0,0,0,609,604,1,0,0,0,610,97,1,0,0,0,611,612,5, + 18,0,0,612,624,5,73,0,0,613,614,5,90,0,0,614,624,5,66,0,0,615,616, + 5,90,0,0,616,624,5,30,0,0,617,618,3,138,69,0,618,619,5,66,0,0,619, + 624,1,0,0,0,620,621,3,138,69,0,621,622,5,30,0,0,622,624,1,0,0,0, + 623,611,1,0,0,0,623,613,1,0,0,0,623,615,1,0,0,0,623,617,1,0,0,0, + 623,620,1,0,0,0,624,99,1,0,0,0,625,626,3,106,53,0,626,627,5,0,0, + 1,627,101,1,0,0,0,628,676,3,150,75,0,629,630,3,150,75,0,630,631, + 5,126,0,0,631,632,3,150,75,0,632,639,3,102,51,0,633,634,5,112,0, + 0,634,635,3,150,75,0,635,636,3,102,51,0,636,638,1,0,0,0,637,633, + 1,0,0,0,638,641,1,0,0,0,639,637,1,0,0,0,639,640,1,0,0,0,640,642, + 1,0,0,0,641,639,1,0,0,0,642,643,5,144,0,0,643,676,1,0,0,0,644,645, + 3,150,75,0,645,646,5,126,0,0,646,651,3,152,76,0,647,648,5,112,0, + 0,648,650,3,152,76,0,649,647,1,0,0,0,650,653,1,0,0,0,651,649,1,0, + 0,0,651,652,1,0,0,0,652,654,1,0,0,0,653,651,1,0,0,0,654,655,5,144, + 0,0,655,676,1,0,0,0,656,657,3,150,75,0,657,658,5,126,0,0,658,663, + 3,102,51,0,659,660,5,112,0,0,660,662,3,102,51,0,661,659,1,0,0,0, + 662,665,1,0,0,0,663,661,1,0,0,0,663,664,1,0,0,0,664,666,1,0,0,0, + 665,663,1,0,0,0,666,667,5,144,0,0,667,676,1,0,0,0,668,669,3,150, + 75,0,669,671,5,126,0,0,670,672,3,104,52,0,671,670,1,0,0,0,671,672, + 1,0,0,0,672,673,1,0,0,0,673,674,5,144,0,0,674,676,1,0,0,0,675,628, + 1,0,0,0,675,629,1,0,0,0,675,644,1,0,0,0,675,656,1,0,0,0,675,668, + 1,0,0,0,676,103,1,0,0,0,677,682,3,106,53,0,678,679,5,112,0,0,679, + 681,3,106,53,0,680,678,1,0,0,0,681,684,1,0,0,0,682,680,1,0,0,0,682, + 683,1,0,0,0,683,105,1,0,0,0,684,682,1,0,0,0,685,686,6,53,-1,0,686, + 688,5,12,0,0,687,689,3,106,53,0,688,687,1,0,0,0,688,689,1,0,0,0, + 689,695,1,0,0,0,690,691,5,94,0,0,691,692,3,106,53,0,692,693,5,81, + 0,0,693,694,3,106,53,0,694,696,1,0,0,0,695,690,1,0,0,0,696,697,1, + 0,0,0,697,695,1,0,0,0,697,698,1,0,0,0,698,701,1,0,0,0,699,700,5, + 24,0,0,700,702,3,106,53,0,701,699,1,0,0,0,701,702,1,0,0,0,702,703, + 1,0,0,0,703,704,5,25,0,0,704,835,1,0,0,0,705,706,5,13,0,0,706,707, + 5,126,0,0,707,708,3,106,53,0,708,709,5,6,0,0,709,710,3,102,51,0, + 710,711,5,144,0,0,711,835,1,0,0,0,712,713,5,19,0,0,713,835,5,106, + 0,0,714,715,5,43,0,0,715,716,3,106,53,0,716,717,3,142,71,0,717,835, + 1,0,0,0,718,719,5,80,0,0,719,720,5,126,0,0,720,721,3,106,53,0,721, + 722,5,32,0,0,722,725,3,106,53,0,723,724,5,31,0,0,724,726,3,106,53, + 0,725,723,1,0,0,0,725,726,1,0,0,0,726,727,1,0,0,0,727,728,5,144, + 0,0,728,835,1,0,0,0,729,730,5,83,0,0,730,835,5,106,0,0,731,732,5, + 88,0,0,732,733,5,126,0,0,733,734,7,9,0,0,734,735,3,156,78,0,735, + 736,5,32,0,0,736,737,3,106,53,0,737,738,5,144,0,0,738,835,1,0,0, + 0,739,740,3,150,75,0,740,742,5,126,0,0,741,743,3,104,52,0,742,741, + 1,0,0,0,742,743,1,0,0,0,743,744,1,0,0,0,744,745,5,144,0,0,745,754, + 1,0,0,0,746,748,5,126,0,0,747,749,5,23,0,0,748,747,1,0,0,0,748,749, + 1,0,0,0,749,751,1,0,0,0,750,752,3,108,54,0,751,750,1,0,0,0,751,752, + 1,0,0,0,752,753,1,0,0,0,753,755,5,144,0,0,754,746,1,0,0,0,754,755, + 1,0,0,0,755,756,1,0,0,0,756,757,5,64,0,0,757,758,5,126,0,0,758,759, + 3,88,44,0,759,760,5,144,0,0,760,835,1,0,0,0,761,762,3,150,75,0,762, + 764,5,126,0,0,763,765,3,104,52,0,764,763,1,0,0,0,764,765,1,0,0,0, + 765,766,1,0,0,0,766,767,5,144,0,0,767,776,1,0,0,0,768,770,5,126, + 0,0,769,771,5,23,0,0,770,769,1,0,0,0,770,771,1,0,0,0,771,773,1,0, + 0,0,772,774,3,108,54,0,773,772,1,0,0,0,773,774,1,0,0,0,774,775,1, + 0,0,0,775,777,5,144,0,0,776,768,1,0,0,0,776,777,1,0,0,0,777,778, + 1,0,0,0,778,779,5,64,0,0,779,780,3,150,75,0,780,835,1,0,0,0,781, + 787,3,150,75,0,782,784,5,126,0,0,783,785,3,104,52,0,784,783,1,0, + 0,0,784,785,1,0,0,0,785,786,1,0,0,0,786,788,5,144,0,0,787,782,1, + 0,0,0,787,788,1,0,0,0,788,789,1,0,0,0,789,791,5,126,0,0,790,792, + 5,23,0,0,791,790,1,0,0,0,791,792,1,0,0,0,792,794,1,0,0,0,793,795, + 3,108,54,0,794,793,1,0,0,0,794,795,1,0,0,0,795,796,1,0,0,0,796,797, + 5,144,0,0,797,835,1,0,0,0,798,835,3,114,57,0,799,835,3,158,79,0, + 800,835,3,140,70,0,801,802,5,114,0,0,802,835,3,106,53,19,803,804, + 5,56,0,0,804,835,3,106,53,13,805,806,3,130,65,0,806,807,5,116,0, + 0,807,809,1,0,0,0,808,805,1,0,0,0,808,809,1,0,0,0,809,810,1,0,0, + 0,810,835,5,108,0,0,811,812,5,126,0,0,812,813,3,34,17,0,813,814, + 5,144,0,0,814,835,1,0,0,0,815,816,5,126,0,0,816,817,3,106,53,0,817, + 818,5,144,0,0,818,835,1,0,0,0,819,820,5,126,0,0,820,821,3,104,52, + 0,821,822,5,144,0,0,822,835,1,0,0,0,823,825,5,125,0,0,824,826,3, + 104,52,0,825,824,1,0,0,0,825,826,1,0,0,0,826,827,1,0,0,0,827,835, + 5,143,0,0,828,830,5,124,0,0,829,831,3,30,15,0,830,829,1,0,0,0,830, + 831,1,0,0,0,831,832,1,0,0,0,832,835,5,142,0,0,833,835,3,122,61,0, + 834,685,1,0,0,0,834,705,1,0,0,0,834,712,1,0,0,0,834,714,1,0,0,0, + 834,718,1,0,0,0,834,729,1,0,0,0,834,731,1,0,0,0,834,739,1,0,0,0, + 834,761,1,0,0,0,834,781,1,0,0,0,834,798,1,0,0,0,834,799,1,0,0,0, + 834,800,1,0,0,0,834,801,1,0,0,0,834,803,1,0,0,0,834,808,1,0,0,0, + 834,811,1,0,0,0,834,815,1,0,0,0,834,819,1,0,0,0,834,823,1,0,0,0, + 834,828,1,0,0,0,834,833,1,0,0,0,835,928,1,0,0,0,836,840,10,18,0, + 0,837,841,5,108,0,0,838,841,5,146,0,0,839,841,5,133,0,0,840,837, + 1,0,0,0,840,838,1,0,0,0,840,839,1,0,0,0,841,842,1,0,0,0,842,927, + 3,106,53,19,843,847,10,17,0,0,844,848,5,134,0,0,845,848,5,114,0, + 0,846,848,5,113,0,0,847,844,1,0,0,0,847,845,1,0,0,0,847,846,1,0, + 0,0,848,849,1,0,0,0,849,927,3,106,53,18,850,875,10,16,0,0,851,876, + 5,117,0,0,852,876,5,118,0,0,853,876,5,129,0,0,854,876,5,127,0,0, + 855,876,5,128,0,0,856,876,5,119,0,0,857,876,5,120,0,0,858,860,5, + 56,0,0,859,858,1,0,0,0,859,860,1,0,0,0,860,861,1,0,0,0,861,863,5, + 40,0,0,862,864,5,14,0,0,863,862,1,0,0,0,863,864,1,0,0,0,864,876, + 1,0,0,0,865,867,5,56,0,0,866,865,1,0,0,0,866,867,1,0,0,0,867,868, + 1,0,0,0,868,876,7,10,0,0,869,876,5,140,0,0,870,876,5,141,0,0,871, + 876,5,131,0,0,872,876,5,122,0,0,873,876,5,123,0,0,874,876,5,130, + 0,0,875,851,1,0,0,0,875,852,1,0,0,0,875,853,1,0,0,0,875,854,1,0, + 0,0,875,855,1,0,0,0,875,856,1,0,0,0,875,857,1,0,0,0,875,859,1,0, + 0,0,875,866,1,0,0,0,875,869,1,0,0,0,875,870,1,0,0,0,875,871,1,0, + 0,0,875,872,1,0,0,0,875,873,1,0,0,0,875,874,1,0,0,0,876,877,1,0, + 0,0,877,927,3,106,53,17,878,879,10,14,0,0,879,880,5,132,0,0,880, + 927,3,106,53,15,881,882,10,12,0,0,882,883,5,2,0,0,883,927,3,106, + 53,13,884,885,10,11,0,0,885,886,5,61,0,0,886,927,3,106,53,12,887, + 889,10,10,0,0,888,890,5,56,0,0,889,888,1,0,0,0,889,890,1,0,0,0,890, + 891,1,0,0,0,891,892,5,9,0,0,892,893,3,106,53,0,893,894,5,2,0,0,894, + 895,3,106,53,11,895,927,1,0,0,0,896,897,10,9,0,0,897,898,5,135,0, + 0,898,899,3,106,53,0,899,900,5,111,0,0,900,901,3,106,53,9,901,927, + 1,0,0,0,902,903,10,22,0,0,903,904,5,125,0,0,904,905,3,106,53,0,905, + 906,5,143,0,0,906,927,1,0,0,0,907,908,10,21,0,0,908,909,5,116,0, + 0,909,927,5,104,0,0,910,911,10,20,0,0,911,912,5,116,0,0,912,927, + 3,150,75,0,913,914,10,15,0,0,914,916,5,44,0,0,915,917,5,56,0,0,916, + 915,1,0,0,0,916,917,1,0,0,0,917,918,1,0,0,0,918,927,5,57,0,0,919, + 924,10,8,0,0,920,921,5,6,0,0,921,925,3,150,75,0,922,923,5,6,0,0, + 923,925,5,106,0,0,924,920,1,0,0,0,924,922,1,0,0,0,925,927,1,0,0, + 0,926,836,1,0,0,0,926,843,1,0,0,0,926,850,1,0,0,0,926,878,1,0,0, + 0,926,881,1,0,0,0,926,884,1,0,0,0,926,887,1,0,0,0,926,896,1,0,0, + 0,926,902,1,0,0,0,926,907,1,0,0,0,926,910,1,0,0,0,926,913,1,0,0, + 0,926,919,1,0,0,0,927,930,1,0,0,0,928,926,1,0,0,0,928,929,1,0,0, + 0,929,107,1,0,0,0,930,928,1,0,0,0,931,936,3,110,55,0,932,933,5,112, + 0,0,933,935,3,110,55,0,934,932,1,0,0,0,935,938,1,0,0,0,936,934,1, + 0,0,0,936,937,1,0,0,0,937,109,1,0,0,0,938,936,1,0,0,0,939,942,3, + 112,56,0,940,942,3,106,53,0,941,939,1,0,0,0,941,940,1,0,0,0,942, + 111,1,0,0,0,943,944,5,126,0,0,944,949,3,150,75,0,945,946,5,112,0, + 0,946,948,3,150,75,0,947,945,1,0,0,0,948,951,1,0,0,0,949,947,1,0, + 0,0,949,950,1,0,0,0,950,952,1,0,0,0,951,949,1,0,0,0,952,953,5,144, + 0,0,953,963,1,0,0,0,954,959,3,150,75,0,955,956,5,112,0,0,956,958, + 3,150,75,0,957,955,1,0,0,0,958,961,1,0,0,0,959,957,1,0,0,0,959,960, + 1,0,0,0,960,963,1,0,0,0,961,959,1,0,0,0,962,943,1,0,0,0,962,954, + 1,0,0,0,963,964,1,0,0,0,964,965,5,107,0,0,965,966,3,106,53,0,966, + 113,1,0,0,0,967,968,5,128,0,0,968,972,3,150,75,0,969,971,3,116,58, + 0,970,969,1,0,0,0,971,974,1,0,0,0,972,970,1,0,0,0,972,973,1,0,0, + 0,973,975,1,0,0,0,974,972,1,0,0,0,975,976,5,146,0,0,976,977,5,120, + 0,0,977,996,1,0,0,0,978,979,5,128,0,0,979,983,3,150,75,0,980,982, + 3,116,58,0,981,980,1,0,0,0,982,985,1,0,0,0,983,981,1,0,0,0,983,984, + 1,0,0,0,984,986,1,0,0,0,985,983,1,0,0,0,986,988,5,120,0,0,987,989, + 3,114,57,0,988,987,1,0,0,0,988,989,1,0,0,0,989,990,1,0,0,0,990,991, + 5,128,0,0,991,992,5,146,0,0,992,993,3,150,75,0,993,994,5,120,0,0, + 994,996,1,0,0,0,995,967,1,0,0,0,995,978,1,0,0,0,996,115,1,0,0,0, + 997,998,3,150,75,0,998,999,5,118,0,0,999,1000,3,156,78,0,1000,1009, + 1,0,0,0,1001,1002,3,150,75,0,1002,1003,5,118,0,0,1003,1004,5,124, + 0,0,1004,1005,3,106,53,0,1005,1006,5,142,0,0,1006,1009,1,0,0,0,1007, + 1009,3,150,75,0,1008,997,1,0,0,0,1008,1001,1,0,0,0,1008,1007,1,0, + 0,0,1009,117,1,0,0,0,1010,1015,3,120,60,0,1011,1012,5,112,0,0,1012, + 1014,3,120,60,0,1013,1011,1,0,0,0,1014,1017,1,0,0,0,1015,1013,1, + 0,0,0,1015,1016,1,0,0,0,1016,119,1,0,0,0,1017,1015,1,0,0,0,1018, + 1019,3,150,75,0,1019,1020,5,6,0,0,1020,1021,5,126,0,0,1021,1022, + 3,34,17,0,1022,1023,5,144,0,0,1023,1029,1,0,0,0,1024,1025,3,106, + 53,0,1025,1026,5,6,0,0,1026,1027,3,150,75,0,1027,1029,1,0,0,0,1028, + 1018,1,0,0,0,1028,1024,1,0,0,0,1029,121,1,0,0,0,1030,1038,3,154, + 77,0,1031,1032,3,130,65,0,1032,1033,5,116,0,0,1033,1035,1,0,0,0, + 1034,1031,1,0,0,0,1034,1035,1,0,0,0,1035,1036,1,0,0,0,1036,1038, + 3,124,62,0,1037,1030,1,0,0,0,1037,1034,1,0,0,0,1038,123,1,0,0,0, + 1039,1044,3,150,75,0,1040,1041,5,116,0,0,1041,1043,3,150,75,0,1042, + 1040,1,0,0,0,1043,1046,1,0,0,0,1044,1042,1,0,0,0,1044,1045,1,0,0, + 0,1045,125,1,0,0,0,1046,1044,1,0,0,0,1047,1048,6,63,-1,0,1048,1057, + 3,130,65,0,1049,1057,3,128,64,0,1050,1051,5,126,0,0,1051,1052,3, + 34,17,0,1052,1053,5,144,0,0,1053,1057,1,0,0,0,1054,1057,3,114,57, + 0,1055,1057,3,154,77,0,1056,1047,1,0,0,0,1056,1049,1,0,0,0,1056, + 1050,1,0,0,0,1056,1054,1,0,0,0,1056,1055,1,0,0,0,1057,1066,1,0,0, + 0,1058,1062,10,3,0,0,1059,1063,3,148,74,0,1060,1061,5,6,0,0,1061, + 1063,3,150,75,0,1062,1059,1,0,0,0,1062,1060,1,0,0,0,1063,1065,1, + 0,0,0,1064,1058,1,0,0,0,1065,1068,1,0,0,0,1066,1064,1,0,0,0,1066, + 1067,1,0,0,0,1067,127,1,0,0,0,1068,1066,1,0,0,0,1069,1070,3,150, + 75,0,1070,1072,5,126,0,0,1071,1073,3,132,66,0,1072,1071,1,0,0,0, + 1072,1073,1,0,0,0,1073,1074,1,0,0,0,1074,1075,5,144,0,0,1075,129, + 1,0,0,0,1076,1077,3,134,67,0,1077,1078,5,116,0,0,1078,1080,1,0,0, + 0,1079,1076,1,0,0,0,1079,1080,1,0,0,0,1080,1081,1,0,0,0,1081,1082, + 3,150,75,0,1082,131,1,0,0,0,1083,1088,3,106,53,0,1084,1085,5,112, + 0,0,1085,1087,3,106,53,0,1086,1084,1,0,0,0,1087,1090,1,0,0,0,1088, + 1086,1,0,0,0,1088,1089,1,0,0,0,1089,133,1,0,0,0,1090,1088,1,0,0, + 0,1091,1092,3,150,75,0,1092,135,1,0,0,0,1093,1102,5,102,0,0,1094, + 1095,5,116,0,0,1095,1102,7,11,0,0,1096,1097,5,104,0,0,1097,1099, + 5,116,0,0,1098,1100,7,11,0,0,1099,1098,1,0,0,0,1099,1100,1,0,0,0, + 1100,1102,1,0,0,0,1101,1093,1,0,0,0,1101,1094,1,0,0,0,1101,1096, + 1,0,0,0,1102,137,1,0,0,0,1103,1105,7,12,0,0,1104,1103,1,0,0,0,1104, + 1105,1,0,0,0,1105,1112,1,0,0,0,1106,1113,3,136,68,0,1107,1113,5, + 103,0,0,1108,1113,5,104,0,0,1109,1113,5,105,0,0,1110,1113,5,41,0, + 0,1111,1113,5,55,0,0,1112,1106,1,0,0,0,1112,1107,1,0,0,0,1112,1108, + 1,0,0,0,1112,1109,1,0,0,0,1112,1110,1,0,0,0,1112,1111,1,0,0,0,1113, + 139,1,0,0,0,1114,1118,3,138,69,0,1115,1118,5,106,0,0,1116,1118,5, + 57,0,0,1117,1114,1,0,0,0,1117,1115,1,0,0,0,1117,1116,1,0,0,0,1118, + 141,1,0,0,0,1119,1120,7,13,0,0,1120,143,1,0,0,0,1121,1122,7,14,0, + 0,1122,145,1,0,0,0,1123,1124,7,15,0,0,1124,147,1,0,0,0,1125,1128, + 5,101,0,0,1126,1128,3,146,73,0,1127,1125,1,0,0,0,1127,1126,1,0,0, + 0,1128,149,1,0,0,0,1129,1133,5,101,0,0,1130,1133,3,142,71,0,1131, + 1133,3,144,72,0,1132,1129,1,0,0,0,1132,1130,1,0,0,0,1132,1131,1, + 0,0,0,1133,151,1,0,0,0,1134,1135,3,156,78,0,1135,1136,5,118,0,0, + 1136,1137,3,138,69,0,1137,153,1,0,0,0,1138,1139,5,124,0,0,1139,1140, + 3,150,75,0,1140,1141,5,142,0,0,1141,155,1,0,0,0,1142,1145,5,106, + 0,0,1143,1145,3,158,79,0,1144,1142,1,0,0,0,1144,1143,1,0,0,0,1145, + 157,1,0,0,0,1146,1150,5,137,0,0,1147,1149,3,160,80,0,1148,1147,1, + 0,0,0,1149,1152,1,0,0,0,1150,1148,1,0,0,0,1150,1151,1,0,0,0,1151, + 1153,1,0,0,0,1152,1150,1,0,0,0,1153,1154,5,139,0,0,1154,159,1,0, + 0,0,1155,1156,5,152,0,0,1156,1157,3,106,53,0,1157,1158,5,142,0,0, + 1158,1161,1,0,0,0,1159,1161,5,151,0,0,1160,1155,1,0,0,0,1160,1159, + 1,0,0,0,1161,161,1,0,0,0,1162,1166,5,138,0,0,1163,1165,3,164,82, + 0,1164,1163,1,0,0,0,1165,1168,1,0,0,0,1166,1164,1,0,0,0,1166,1167, + 1,0,0,0,1167,1169,1,0,0,0,1168,1166,1,0,0,0,1169,1170,5,0,0,1,1170, + 163,1,0,0,0,1171,1172,5,154,0,0,1172,1173,3,106,53,0,1173,1174,5, + 142,0,0,1174,1177,1,0,0,0,1175,1177,5,153,0,0,1176,1171,1,0,0,0, + 1176,1175,1,0,0,0,1177,165,1,0,0,0,145,169,176,185,192,203,207,210, + 219,227,233,245,253,267,273,283,292,295,299,302,306,309,312,315, + 318,322,326,329,332,335,339,342,351,357,378,395,412,418,424,435, + 437,448,451,457,465,471,473,477,482,485,488,492,496,499,501,504, + 508,512,515,517,519,524,535,541,548,553,557,561,567,569,576,584, + 587,590,609,623,639,651,663,671,675,682,688,697,701,725,742,748, + 751,754,764,770,773,776,784,787,791,794,808,825,830,834,840,847, + 859,863,866,875,889,916,924,926,928,936,941,949,959,962,972,983, + 988,995,1008,1015,1028,1034,1037,1044,1056,1062,1066,1072,1079,1088, + 1099,1101,1104,1112,1117,1127,1132,1144,1150,1160,1166,1176 ] class HogQLParser ( Parser ): @@ -557,14 +558,14 @@ class HogQLParser ( Parser ): RULE_declaration = 1 RULE_expression = 2 RULE_varDecl = 3 - RULE_varAssignment = 4 - RULE_identifierList = 5 - RULE_statement = 6 - RULE_exprStmt = 7 - RULE_ifStmt = 8 - RULE_whileStmt = 9 - RULE_returnStmt = 10 - RULE_funcStmt = 11 + RULE_identifierList = 4 + RULE_statement = 5 + RULE_returnStmt = 6 + RULE_ifStmt = 7 + RULE_whileStmt = 8 + RULE_funcStmt = 9 + RULE_varAssignment = 10 + RULE_exprStmt = 11 RULE_emptyStmt = 12 RULE_block = 13 RULE_kvPair = 14 @@ -637,12 +638,12 @@ class HogQLParser ( Parser ): RULE_fullTemplateString = 81 RULE_stringContentsFull = 82 - ruleNames = [ "program", "declaration", "expression", "varDecl", "varAssignment", - "identifierList", "statement", "exprStmt", "ifStmt", - "whileStmt", "returnStmt", "funcStmt", "emptyStmt", "block", - "kvPair", "kvPairList", "select", "selectUnionStmt", - "selectStmtWithParens", "selectStmt", "withClause", "topClause", - "fromClause", "arrayJoinClause", "windowClause", "prewhereClause", + ruleNames = [ "program", "declaration", "expression", "varDecl", "identifierList", + "statement", "returnStmt", "ifStmt", "whileStmt", "funcStmt", + "varAssignment", "exprStmt", "emptyStmt", "block", "kvPair", + "kvPairList", "select", "selectUnionStmt", "selectStmtWithParens", + "selectStmt", "withClause", "topClause", "fromClause", + "arrayJoinClause", "windowClause", "prewhereClause", "whereClause", "groupByClause", "havingClause", "orderByClause", "projectionOrderByClause", "limitAndOffsetClause", "offsetOnlyClause", "settingsClause", "joinExpr", "joinOp", "joinOpCross", @@ -994,9 +995,6 @@ def identifier(self): return self.getTypedRuleContext(HogQLParser.IdentifierContext,0) - def SEMICOLON(self): - return self.getToken(HogQLParser.SEMICOLON, 0) - def COLON(self): return self.getToken(HogQLParser.COLON, 0) @@ -1042,68 +1040,6 @@ def varDecl(self): self.expression() - self.state = 187 - self.match(HogQLParser.SEMICOLON) - except RecognitionException as re: - localctx.exception = re - self._errHandler.reportError(self, re) - self._errHandler.recover(self, re) - finally: - self.exitRule() - return localctx - - - class VarAssignmentContext(ParserRuleContext): - __slots__ = 'parser' - - def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): - super().__init__(parent, invokingState) - self.parser = parser - - def expression(self, i:int=None): - if i is None: - return self.getTypedRuleContexts(HogQLParser.ExpressionContext) - else: - return self.getTypedRuleContext(HogQLParser.ExpressionContext,i) - - - def COLON(self): - return self.getToken(HogQLParser.COLON, 0) - - def EQ_SINGLE(self): - return self.getToken(HogQLParser.EQ_SINGLE, 0) - - def SEMICOLON(self): - return self.getToken(HogQLParser.SEMICOLON, 0) - - def getRuleIndex(self): - return HogQLParser.RULE_varAssignment - - def accept(self, visitor:ParseTreeVisitor): - if hasattr( visitor, "visitVarAssignment" ): - return visitor.visitVarAssignment(self) - else: - return visitor.visitChildren(self) - - - - - def varAssignment(self): - - localctx = HogQLParser.VarAssignmentContext(self, self._ctx, self.state) - self.enterRule(localctx, 8, self.RULE_varAssignment) - try: - self.enterOuterAlt(localctx, 1) - self.state = 189 - self.expression() - self.state = 190 - self.match(HogQLParser.COLON) - self.state = 191 - self.match(HogQLParser.EQ_SINGLE) - self.state = 192 - self.expression() - self.state = 193 - self.match(HogQLParser.SEMICOLON) except RecognitionException as re: localctx.exception = re self._errHandler.reportError(self, re) @@ -1148,21 +1084,21 @@ def accept(self, visitor:ParseTreeVisitor): def identifierList(self): localctx = HogQLParser.IdentifierListContext(self, self._ctx, self.state) - self.enterRule(localctx, 10, self.RULE_identifierList) + self.enterRule(localctx, 8, self.RULE_identifierList) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 195 + self.state = 187 self.identifier() - self.state = 200 + self.state = 192 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 196 + self.state = 188 self.match(HogQLParser.COMMA) - self.state = 197 + self.state = 189 self.identifier() - self.state = 202 + self.state = 194 self._errHandler.sync(self) _la = self._input.LA(1) @@ -1186,14 +1122,6 @@ def returnStmt(self): return self.getTypedRuleContext(HogQLParser.ReturnStmtContext,0) - def emptyStmt(self): - return self.getTypedRuleContext(HogQLParser.EmptyStmtContext,0) - - - def exprStmt(self): - return self.getTypedRuleContext(HogQLParser.ExprStmtContext,0) - - def ifStmt(self): return self.getTypedRuleContext(HogQLParser.IfStmtContext,0) @@ -1210,6 +1138,14 @@ def varAssignment(self): return self.getTypedRuleContext(HogQLParser.VarAssignmentContext,0) + def exprStmt(self): + return self.getTypedRuleContext(HogQLParser.ExprStmtContext,0) + + + def emptyStmt(self): + return self.getTypedRuleContext(HogQLParser.EmptyStmtContext,0) + + def block(self): return self.getTypedRuleContext(HogQLParser.BlockContext,0) @@ -1229,62 +1165,56 @@ def accept(self, visitor:ParseTreeVisitor): def statement(self): localctx = HogQLParser.StatementContext(self, self._ctx, self.state) - self.enterRule(localctx, 12, self.RULE_statement) + self.enterRule(localctx, 10, self.RULE_statement) try: - self.state = 212 + self.state = 203 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,4,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 203 + self.state = 195 self.returnStmt() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 204 - self.emptyStmt() + self.state = 196 + self.ifStmt() pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 205 - self.exprStmt() + self.state = 197 + self.whileStmt() pass elif la_ == 4: self.enterOuterAlt(localctx, 4) - self.state = 206 - self.ifStmt() + self.state = 198 + self.funcStmt() pass elif la_ == 5: self.enterOuterAlt(localctx, 5) - self.state = 207 - self.whileStmt() + self.state = 199 + self.varAssignment() pass elif la_ == 6: self.enterOuterAlt(localctx, 6) - self.state = 208 - self.funcStmt() + self.state = 200 + self.exprStmt() pass elif la_ == 7: self.enterOuterAlt(localctx, 7) - self.state = 209 - self.varAssignment() + self.state = 201 + self.emptyStmt() pass elif la_ == 8: self.enterOuterAlt(localctx, 8) - self.state = 210 - self.returnStmt() - pass - - elif la_ == 9: - self.enterOuterAlt(localctx, 9) - self.state = 211 + self.state = 202 self.block() pass @@ -1298,13 +1228,16 @@ def statement(self): return localctx - class ExprStmtContext(ParserRuleContext): + class ReturnStmtContext(ParserRuleContext): __slots__ = 'parser' def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): super().__init__(parent, invokingState) self.parser = parser + def RETURN(self): + return self.getToken(HogQLParser.RETURN, 0) + def expression(self): return self.getTypedRuleContext(HogQLParser.ExpressionContext,0) @@ -1313,27 +1246,41 @@ def SEMICOLON(self): return self.getToken(HogQLParser.SEMICOLON, 0) def getRuleIndex(self): - return HogQLParser.RULE_exprStmt + return HogQLParser.RULE_returnStmt def accept(self, visitor:ParseTreeVisitor): - if hasattr( visitor, "visitExprStmt" ): - return visitor.visitExprStmt(self) + if hasattr( visitor, "visitReturnStmt" ): + return visitor.visitReturnStmt(self) else: return visitor.visitChildren(self) - def exprStmt(self): + def returnStmt(self): - localctx = HogQLParser.ExprStmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 14, self.RULE_exprStmt) + localctx = HogQLParser.ReturnStmtContext(self, self._ctx, self.state) + self.enterRule(localctx, 12, self.RULE_returnStmt) try: self.enterOuterAlt(localctx, 1) - self.state = 214 - self.expression() - self.state = 215 - self.match(HogQLParser.SEMICOLON) + self.state = 205 + self.match(HogQLParser.RETURN) + self.state = 207 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,5,self._ctx) + if la_ == 1: + self.state = 206 + self.expression() + + + self.state = 210 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,6,self._ctx) + if la_ == 1: + self.state = 209 + self.match(HogQLParser.SEMICOLON) + + except RecognitionException as re: localctx.exception = re self._errHandler.reportError(self, re) @@ -1388,26 +1335,26 @@ def accept(self, visitor:ParseTreeVisitor): def ifStmt(self): localctx = HogQLParser.IfStmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 16, self.RULE_ifStmt) + self.enterRule(localctx, 14, self.RULE_ifStmt) try: self.enterOuterAlt(localctx, 1) - self.state = 217 + self.state = 212 self.match(HogQLParser.IF) - self.state = 218 + self.state = 213 self.match(HogQLParser.LPAREN) - self.state = 219 + self.state = 214 self.expression() - self.state = 220 + self.state = 215 self.match(HogQLParser.RPAREN) - self.state = 221 + self.state = 216 self.statement() - self.state = 224 + self.state = 219 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,5,self._ctx) + la_ = self._interp.adaptivePredict(self._input,7,self._ctx) if la_ == 1: - self.state = 222 + self.state = 217 self.match(HogQLParser.ELSE) - self.state = 223 + self.state = 218 self.statement() @@ -1444,6 +1391,9 @@ def statement(self): return self.getTypedRuleContext(HogQLParser.StatementContext,0) + def SEMICOLON(self): + return self.getToken(HogQLParser.SEMICOLON, 0) + def getRuleIndex(self): return HogQLParser.RULE_whileStmt @@ -1459,19 +1409,27 @@ def accept(self, visitor:ParseTreeVisitor): def whileStmt(self): localctx = HogQLParser.WhileStmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 18, self.RULE_whileStmt) + self.enterRule(localctx, 16, self.RULE_whileStmt) try: self.enterOuterAlt(localctx, 1) - self.state = 226 + self.state = 221 self.match(HogQLParser.WHILE) - self.state = 227 + self.state = 222 self.match(HogQLParser.LPAREN) - self.state = 228 + self.state = 223 self.expression() - self.state = 229 + self.state = 224 self.match(HogQLParser.RPAREN) - self.state = 230 + self.state = 225 self.statement() + self.state = 227 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,8,self._ctx) + if la_ == 1: + self.state = 226 + self.match(HogQLParser.SEMICOLON) + + except RecognitionException as re: localctx.exception = re self._errHandler.reportError(self, re) @@ -1481,47 +1439,71 @@ def whileStmt(self): return localctx - class ReturnStmtContext(ParserRuleContext): + class FuncStmtContext(ParserRuleContext): __slots__ = 'parser' def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): super().__init__(parent, invokingState) self.parser = parser - def RETURN(self): - return self.getToken(HogQLParser.RETURN, 0) + def FN(self): + return self.getToken(HogQLParser.FN, 0) - def expression(self): - return self.getTypedRuleContext(HogQLParser.ExpressionContext,0) + def identifier(self): + return self.getTypedRuleContext(HogQLParser.IdentifierContext,0) - def SEMICOLON(self): - return self.getToken(HogQLParser.SEMICOLON, 0) + def LPAREN(self): + return self.getToken(HogQLParser.LPAREN, 0) + + def RPAREN(self): + return self.getToken(HogQLParser.RPAREN, 0) + + def block(self): + return self.getTypedRuleContext(HogQLParser.BlockContext,0) + + + def identifierList(self): + return self.getTypedRuleContext(HogQLParser.IdentifierListContext,0) + def getRuleIndex(self): - return HogQLParser.RULE_returnStmt + return HogQLParser.RULE_funcStmt def accept(self, visitor:ParseTreeVisitor): - if hasattr( visitor, "visitReturnStmt" ): - return visitor.visitReturnStmt(self) + if hasattr( visitor, "visitFuncStmt" ): + return visitor.visitFuncStmt(self) else: return visitor.visitChildren(self) - def returnStmt(self): + def funcStmt(self): - localctx = HogQLParser.ReturnStmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 20, self.RULE_returnStmt) + localctx = HogQLParser.FuncStmtContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_funcStmt) + self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 232 - self.match(HogQLParser.RETURN) + self.state = 229 + self.match(HogQLParser.FN) + self.state = 230 + self.identifier() + self.state = 231 + self.match(HogQLParser.LPAREN) self.state = 233 - self.expression() - self.state = 234 - self.match(HogQLParser.SEMICOLON) + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): + self.state = 232 + self.identifierList() + + + self.state = 235 + self.match(HogQLParser.RPAREN) + self.state = 236 + self.block() except RecognitionException as re: localctx.exception = re self._errHandler.reportError(self, re) @@ -1531,71 +1513,103 @@ def returnStmt(self): return localctx - class FuncStmtContext(ParserRuleContext): + class VarAssignmentContext(ParserRuleContext): __slots__ = 'parser' def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): super().__init__(parent, invokingState) self.parser = parser - def FN(self): - return self.getToken(HogQLParser.FN, 0) + def expression(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(HogQLParser.ExpressionContext) + else: + return self.getTypedRuleContext(HogQLParser.ExpressionContext,i) - def identifier(self): - return self.getTypedRuleContext(HogQLParser.IdentifierContext,0) + def COLON(self): + return self.getToken(HogQLParser.COLON, 0) - def LPAREN(self): - return self.getToken(HogQLParser.LPAREN, 0) + def EQ_SINGLE(self): + return self.getToken(HogQLParser.EQ_SINGLE, 0) - def RPAREN(self): - return self.getToken(HogQLParser.RPAREN, 0) + def getRuleIndex(self): + return HogQLParser.RULE_varAssignment - def block(self): - return self.getTypedRuleContext(HogQLParser.BlockContext,0) + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitVarAssignment" ): + return visitor.visitVarAssignment(self) + else: + return visitor.visitChildren(self) - def identifierList(self): - return self.getTypedRuleContext(HogQLParser.IdentifierListContext,0) + def varAssignment(self): + + localctx = HogQLParser.VarAssignmentContext(self, self._ctx, self.state) + self.enterRule(localctx, 20, self.RULE_varAssignment) + try: + self.enterOuterAlt(localctx, 1) + self.state = 238 + self.expression() + self.state = 239 + self.match(HogQLParser.COLON) + self.state = 240 + self.match(HogQLParser.EQ_SINGLE) + self.state = 241 + self.expression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + class ExprStmtContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def expression(self): + return self.getTypedRuleContext(HogQLParser.ExpressionContext,0) + + + def SEMICOLON(self): + return self.getToken(HogQLParser.SEMICOLON, 0) + def getRuleIndex(self): - return HogQLParser.RULE_funcStmt + return HogQLParser.RULE_exprStmt def accept(self, visitor:ParseTreeVisitor): - if hasattr( visitor, "visitFuncStmt" ): - return visitor.visitFuncStmt(self) + if hasattr( visitor, "visitExprStmt" ): + return visitor.visitExprStmt(self) else: return visitor.visitChildren(self) - def funcStmt(self): + def exprStmt(self): - localctx = HogQLParser.FuncStmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 22, self.RULE_funcStmt) - self._la = 0 # Token type + localctx = HogQLParser.ExprStmtContext(self, self._ctx, self.state) + self.enterRule(localctx, 22, self.RULE_exprStmt) try: self.enterOuterAlt(localctx, 1) - self.state = 236 - self.match(HogQLParser.FN) - self.state = 237 - self.identifier() - self.state = 238 - self.match(HogQLParser.LPAREN) - self.state = 240 + self.state = 243 + self.expression() + self.state = 245 self._errHandler.sync(self) - _la = self._input.LA(1) - if (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): - self.state = 239 - self.identifierList() + la_ = self._interp.adaptivePredict(self._input,10,self._ctx) + if la_ == 1: + self.state = 244 + self.match(HogQLParser.SEMICOLON) - self.state = 242 - self.match(HogQLParser.RPAREN) - self.state = 243 - self.block() except RecognitionException as re: localctx.exception = re self._errHandler.reportError(self, re) @@ -1633,7 +1647,7 @@ def emptyStmt(self): self.enterRule(localctx, 24, self.RULE_emptyStmt) try: self.enterOuterAlt(localctx, 1) - self.state = 245 + self.state = 247 self.match(HogQLParser.SEMICOLON) except RecognitionException as re: localctx.exception = re @@ -1683,19 +1697,19 @@ def block(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 247 + self.state = 249 self.match(HogQLParser.LBRACE) - self.state = 251 + self.state = 253 self._errHandler.sync(self) _la = self._input.LA(1) while (((_la) & ~0x3f) == 0 and ((1 << _la) & -2) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106351341731839) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 131649) != 0): - self.state = 248 + self.state = 250 self.declaration() - self.state = 253 + self.state = 255 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 254 + self.state = 256 self.match(HogQLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -1741,11 +1755,11 @@ def kvPair(self): self.enterRule(localctx, 28, self.RULE_kvPair) try: self.enterOuterAlt(localctx, 1) - self.state = 256 + self.state = 258 self.expression() - self.state = 257 + self.state = 259 self.match(HogQLParser.COLON) - self.state = 258 + self.state = 260 self.expression() except RecognitionException as re: localctx.exception = re @@ -1795,17 +1809,17 @@ def kvPairList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 260 + self.state = 262 self.kvPair() - self.state = 265 + self.state = 267 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 261 + self.state = 263 self.match(HogQLParser.COMMA) - self.state = 262 + self.state = 264 self.kvPair() - self.state = 267 + self.state = 269 self._errHandler.sync(self) _la = self._input.LA(1) @@ -1858,26 +1872,26 @@ def select(self): self.enterRule(localctx, 32, self.RULE_select) try: self.enterOuterAlt(localctx, 1) - self.state = 271 + self.state = 273 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,9,self._ctx) + la_ = self._interp.adaptivePredict(self._input,13,self._ctx) if la_ == 1: - self.state = 268 + self.state = 270 self.selectUnionStmt() pass elif la_ == 2: - self.state = 269 + self.state = 271 self.selectStmt() pass elif la_ == 3: - self.state = 270 + self.state = 272 self.hogqlxTagElement() pass - self.state = 273 + self.state = 275 self.match(HogQLParser.EOF) except RecognitionException as re: localctx.exception = re @@ -1933,19 +1947,19 @@ def selectUnionStmt(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 275 + self.state = 277 self.selectStmtWithParens() - self.state = 281 + self.state = 283 self._errHandler.sync(self) _la = self._input.LA(1) while _la==91: - self.state = 276 + self.state = 278 self.match(HogQLParser.UNION) - self.state = 277 + self.state = 279 self.match(HogQLParser.ALL) - self.state = 278 + self.state = 280 self.selectStmtWithParens() - self.state = 283 + self.state = 285 self._errHandler.sync(self) _la = self._input.LA(1) @@ -2000,26 +2014,26 @@ def selectStmtWithParens(self): localctx = HogQLParser.SelectStmtWithParensContext(self, self._ctx, self.state) self.enterRule(localctx, 36, self.RULE_selectStmtWithParens) try: - self.state = 290 + self.state = 292 self._errHandler.sync(self) token = self._input.LA(1) if token in [77, 98]: self.enterOuterAlt(localctx, 1) - self.state = 284 + self.state = 286 self.selectStmt() pass elif token in [126]: self.enterOuterAlt(localctx, 2) - self.state = 285 + self.state = 287 self.match(HogQLParser.LPAREN) - self.state = 286 + self.state = 288 self.selectUnionStmt() - self.state = 287 + self.state = 289 self.match(HogQLParser.RPAREN) pass elif token in [124]: self.enterOuterAlt(localctx, 3) - self.state = 289 + self.state = 291 self.placeholder() pass else: @@ -2141,81 +2155,81 @@ def selectStmt(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 293 + self.state = 295 self._errHandler.sync(self) _la = self._input.LA(1) if _la==98: - self.state = 292 + self.state = 294 localctx.with_ = self.withClause() - self.state = 295 - self.match(HogQLParser.SELECT) self.state = 297 + self.match(HogQLParser.SELECT) + self.state = 299 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,13,self._ctx) + la_ = self._interp.adaptivePredict(self._input,17,self._ctx) if la_ == 1: - self.state = 296 + self.state = 298 self.match(HogQLParser.DISTINCT) - self.state = 300 + self.state = 302 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,14,self._ctx) + la_ = self._interp.adaptivePredict(self._input,18,self._ctx) if la_ == 1: - self.state = 299 + self.state = 301 self.topClause() - self.state = 302 - localctx.columns = self.columnExprList() self.state = 304 + localctx.columns = self.columnExprList() + self.state = 306 self._errHandler.sync(self) _la = self._input.LA(1) if _la==32: - self.state = 303 + self.state = 305 localctx.from_ = self.fromClause() - self.state = 307 + self.state = 309 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & 567347999932448) != 0): - self.state = 306 + self.state = 308 self.arrayJoinClause() - self.state = 310 + self.state = 312 self._errHandler.sync(self) _la = self._input.LA(1) if _la==67: - self.state = 309 + self.state = 311 self.prewhereClause() - self.state = 313 + self.state = 315 self._errHandler.sync(self) _la = self._input.LA(1) if _la==95: - self.state = 312 + self.state = 314 localctx.where = self.whereClause() - self.state = 316 + self.state = 318 self._errHandler.sync(self) _la = self._input.LA(1) if _la==34: - self.state = 315 + self.state = 317 self.groupByClause() - self.state = 320 + self.state = 322 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,20,self._ctx) + la_ = self._interp.adaptivePredict(self._input,24,self._ctx) if la_ == 1: - self.state = 318 + self.state = 320 self.match(HogQLParser.WITH) - self.state = 319 + self.state = 321 _la = self._input.LA(1) if not(_la==17 or _la==72): self._errHandler.recoverInline(self) @@ -2224,60 +2238,60 @@ def selectStmt(self): self.consume() - self.state = 324 + self.state = 326 self._errHandler.sync(self) _la = self._input.LA(1) if _la==98: - self.state = 322 + self.state = 324 self.match(HogQLParser.WITH) - self.state = 323 + self.state = 325 self.match(HogQLParser.TOTALS) - self.state = 327 + self.state = 329 self._errHandler.sync(self) _la = self._input.LA(1) if _la==35: - self.state = 326 + self.state = 328 self.havingClause() - self.state = 330 + self.state = 332 self._errHandler.sync(self) _la = self._input.LA(1) if _la==97: - self.state = 329 + self.state = 331 self.windowClause() - self.state = 333 + self.state = 335 self._errHandler.sync(self) _la = self._input.LA(1) if _la==62: - self.state = 332 + self.state = 334 self.orderByClause() - self.state = 337 + self.state = 339 self._errHandler.sync(self) token = self._input.LA(1) if token in [52]: - self.state = 335 + self.state = 337 self.limitAndOffsetClause() pass elif token in [59]: - self.state = 336 + self.state = 338 self.offsetOnlyClause() pass elif token in [-1, 79, 91, 144]: pass else: pass - self.state = 340 + self.state = 342 self._errHandler.sync(self) _la = self._input.LA(1) if _la==79: - self.state = 339 + self.state = 341 self.settingsClause() @@ -2322,9 +2336,9 @@ def withClause(self): self.enterRule(localctx, 40, self.RULE_withClause) try: self.enterOuterAlt(localctx, 1) - self.state = 342 + self.state = 344 self.match(HogQLParser.WITH) - self.state = 343 + self.state = 345 self.withExprList() except RecognitionException as re: localctx.exception = re @@ -2372,17 +2386,17 @@ def topClause(self): self.enterRule(localctx, 42, self.RULE_topClause) try: self.enterOuterAlt(localctx, 1) - self.state = 345 + self.state = 347 self.match(HogQLParser.TOP) - self.state = 346 + self.state = 348 self.match(HogQLParser.DECIMAL_LITERAL) - self.state = 349 + self.state = 351 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,27,self._ctx) + la_ = self._interp.adaptivePredict(self._input,31,self._ctx) if la_ == 1: - self.state = 347 + self.state = 349 self.match(HogQLParser.WITH) - self.state = 348 + self.state = 350 self.match(HogQLParser.TIES) @@ -2427,9 +2441,9 @@ def fromClause(self): self.enterRule(localctx, 44, self.RULE_fromClause) try: self.enterOuterAlt(localctx, 1) - self.state = 351 + self.state = 353 self.match(HogQLParser.FROM) - self.state = 352 + self.state = 354 self.joinExpr(0) except RecognitionException as re: localctx.exception = re @@ -2482,11 +2496,11 @@ def arrayJoinClause(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 355 + self.state = 357 self._errHandler.sync(self) _la = self._input.LA(1) if _la==42 or _la==49: - self.state = 354 + self.state = 356 _la = self._input.LA(1) if not(_la==42 or _la==49): self._errHandler.recoverInline(self) @@ -2495,11 +2509,11 @@ def arrayJoinClause(self): self.consume() - self.state = 357 + self.state = 359 self.match(HogQLParser.ARRAY) - self.state = 358 + self.state = 360 self.match(HogQLParser.JOIN) - self.state = 359 + self.state = 361 self.columnExprList() except RecognitionException as re: localctx.exception = re @@ -2577,35 +2591,35 @@ def windowClause(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 361 + self.state = 363 self.match(HogQLParser.WINDOW) - self.state = 362 + self.state = 364 self.identifier() - self.state = 363 + self.state = 365 self.match(HogQLParser.AS) - self.state = 364 + self.state = 366 self.match(HogQLParser.LPAREN) - self.state = 365 + self.state = 367 self.windowExpr() - self.state = 366 + self.state = 368 self.match(HogQLParser.RPAREN) - self.state = 376 + self.state = 378 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 367 + self.state = 369 self.match(HogQLParser.COMMA) - self.state = 368 + self.state = 370 self.identifier() - self.state = 369 + self.state = 371 self.match(HogQLParser.AS) - self.state = 370 + self.state = 372 self.match(HogQLParser.LPAREN) - self.state = 371 + self.state = 373 self.windowExpr() - self.state = 372 + self.state = 374 self.match(HogQLParser.RPAREN) - self.state = 378 + self.state = 380 self._errHandler.sync(self) _la = self._input.LA(1) @@ -2650,9 +2664,9 @@ def prewhereClause(self): self.enterRule(localctx, 50, self.RULE_prewhereClause) try: self.enterOuterAlt(localctx, 1) - self.state = 379 + self.state = 381 self.match(HogQLParser.PREWHERE) - self.state = 380 + self.state = 382 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -2695,9 +2709,9 @@ def whereClause(self): self.enterRule(localctx, 52, self.RULE_whereClause) try: self.enterOuterAlt(localctx, 1) - self.state = 382 + self.state = 384 self.match(HogQLParser.WHERE) - self.state = 383 + self.state = 385 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -2756,31 +2770,31 @@ def groupByClause(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 385 + self.state = 387 self.match(HogQLParser.GROUP) - self.state = 386 + self.state = 388 self.match(HogQLParser.BY) - self.state = 393 + self.state = 395 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,30,self._ctx) + la_ = self._interp.adaptivePredict(self._input,34,self._ctx) if la_ == 1: - self.state = 387 + self.state = 389 _la = self._input.LA(1) if not(_la==17 or _la==72): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 388 + self.state = 390 self.match(HogQLParser.LPAREN) - self.state = 389 + self.state = 391 self.columnExprList() - self.state = 390 + self.state = 392 self.match(HogQLParser.RPAREN) pass elif la_ == 2: - self.state = 392 + self.state = 394 self.columnExprList() pass @@ -2826,9 +2840,9 @@ def havingClause(self): self.enterRule(localctx, 56, self.RULE_havingClause) try: self.enterOuterAlt(localctx, 1) - self.state = 395 + self.state = 397 self.match(HogQLParser.HAVING) - self.state = 396 + self.state = 398 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -2874,11 +2888,11 @@ def orderByClause(self): self.enterRule(localctx, 58, self.RULE_orderByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 398 + self.state = 400 self.match(HogQLParser.ORDER) - self.state = 399 + self.state = 401 self.match(HogQLParser.BY) - self.state = 400 + self.state = 402 self.orderExprList() except RecognitionException as re: localctx.exception = re @@ -2924,11 +2938,11 @@ def projectionOrderByClause(self): self.enterRule(localctx, 60, self.RULE_projectionOrderByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 402 + self.state = 404 self.match(HogQLParser.ORDER) - self.state = 403 + self.state = 405 self.match(HogQLParser.BY) - self.state = 404 + self.state = 406 self.columnExprList() except RecognitionException as re: localctx.exception = re @@ -2993,38 +3007,38 @@ def limitAndOffsetClause(self): self.enterRule(localctx, 62, self.RULE_limitAndOffsetClause) self._la = 0 # Token type try: - self.state = 435 + self.state = 437 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,35,self._ctx) + la_ = self._interp.adaptivePredict(self._input,39,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 406 + self.state = 408 self.match(HogQLParser.LIMIT) - self.state = 407 + self.state = 409 self.columnExpr(0) - self.state = 410 + self.state = 412 self._errHandler.sync(self) _la = self._input.LA(1) if _la==112: - self.state = 408 + self.state = 410 self.match(HogQLParser.COMMA) - self.state = 409 + self.state = 411 self.columnExpr(0) - self.state = 416 + self.state = 418 self._errHandler.sync(self) token = self._input.LA(1) if token in [98]: - self.state = 412 + self.state = 414 self.match(HogQLParser.WITH) - self.state = 413 + self.state = 415 self.match(HogQLParser.TIES) pass elif token in [11]: - self.state = 414 + self.state = 416 self.match(HogQLParser.BY) - self.state = 415 + self.state = 417 self.columnExprList() pass elif token in [-1, 79, 91, 144]: @@ -3035,43 +3049,43 @@ def limitAndOffsetClause(self): elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 418 + self.state = 420 self.match(HogQLParser.LIMIT) - self.state = 419 + self.state = 421 self.columnExpr(0) - self.state = 422 + self.state = 424 self._errHandler.sync(self) _la = self._input.LA(1) if _la==98: - self.state = 420 + self.state = 422 self.match(HogQLParser.WITH) - self.state = 421 + self.state = 423 self.match(HogQLParser.TIES) - self.state = 424 + self.state = 426 self.match(HogQLParser.OFFSET) - self.state = 425 + self.state = 427 self.columnExpr(0) pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 427 + self.state = 429 self.match(HogQLParser.LIMIT) - self.state = 428 + self.state = 430 self.columnExpr(0) - self.state = 429 + self.state = 431 self.match(HogQLParser.OFFSET) - self.state = 430 + self.state = 432 self.columnExpr(0) - self.state = 433 + self.state = 435 self._errHandler.sync(self) _la = self._input.LA(1) if _la==11: - self.state = 431 + self.state = 433 self.match(HogQLParser.BY) - self.state = 432 + self.state = 434 self.columnExprList() @@ -3119,9 +3133,9 @@ def offsetOnlyClause(self): self.enterRule(localctx, 64, self.RULE_offsetOnlyClause) try: self.enterOuterAlt(localctx, 1) - self.state = 437 + self.state = 439 self.match(HogQLParser.OFFSET) - self.state = 438 + self.state = 440 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -3164,9 +3178,9 @@ def settingsClause(self): self.enterRule(localctx, 66, self.RULE_settingsClause) try: self.enterOuterAlt(localctx, 1) - self.state = 440 + self.state = 442 self.match(HogQLParser.SETTINGS) - self.state = 441 + self.state = 443 self.settingExprList() except RecognitionException as re: localctx.exception = re @@ -3298,29 +3312,29 @@ def joinExpr(self, _p:int=0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 455 + self.state = 457 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,38,self._ctx) + la_ = self._interp.adaptivePredict(self._input,42,self._ctx) if la_ == 1: localctx = HogQLParser.JoinExprTableContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 444 - self.tableExpr(0) self.state = 446 + self.tableExpr(0) + self.state = 448 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,36,self._ctx) + la_ = self._interp.adaptivePredict(self._input,40,self._ctx) if la_ == 1: - self.state = 445 + self.state = 447 self.match(HogQLParser.FINAL) - self.state = 449 + self.state = 451 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,37,self._ctx) + la_ = self._interp.adaptivePredict(self._input,41,self._ctx) if la_ == 1: - self.state = 448 + self.state = 450 self.sampleClause() @@ -3330,67 +3344,67 @@ def joinExpr(self, _p:int=0): localctx = HogQLParser.JoinExprParensContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 451 + self.state = 453 self.match(HogQLParser.LPAREN) - self.state = 452 + self.state = 454 self.joinExpr(0) - self.state = 453 + self.state = 455 self.match(HogQLParser.RPAREN) pass self._ctx.stop = self._input.LT(-1) - self.state = 471 + self.state = 473 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,41,self._ctx) + _alt = self._interp.adaptivePredict(self._input,45,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 469 + self.state = 471 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,40,self._ctx) + la_ = self._interp.adaptivePredict(self._input,44,self._ctx) if la_ == 1: localctx = HogQLParser.JoinExprCrossOpContext(self, HogQLParser.JoinExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_joinExpr) - self.state = 457 + self.state = 459 if not self.precpred(self._ctx, 3): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 3)") - self.state = 458 + self.state = 460 self.joinOpCross() - self.state = 459 + self.state = 461 self.joinExpr(4) pass elif la_ == 2: localctx = HogQLParser.JoinExprOpContext(self, HogQLParser.JoinExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_joinExpr) - self.state = 461 + self.state = 463 if not self.precpred(self._ctx, 4): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 4)") - self.state = 463 + self.state = 465 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & 567356589867290) != 0) or _la==71 or _la==78: - self.state = 462 + self.state = 464 self.joinOp() - self.state = 465 + self.state = 467 self.match(HogQLParser.JOIN) - self.state = 466 + self.state = 468 self.joinExpr(0) - self.state = 467 + self.state = 469 self.joinConstraintClause() pass - self.state = 473 + self.state = 475 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,41,self._ctx) + _alt = self._interp.adaptivePredict(self._input,45,self._ctx) except RecognitionException as re: localctx.exception = re @@ -3499,21 +3513,21 @@ def joinOp(self): self.enterRule(localctx, 70, self.RULE_joinOp) self._la = 0 # Token type try: - self.state = 517 + self.state = 519 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,55,self._ctx) + la_ = self._interp.adaptivePredict(self._input,59,self._ctx) if la_ == 1: localctx = HogQLParser.JoinOpInnerContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 483 + self.state = 485 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,44,self._ctx) + la_ = self._interp.adaptivePredict(self._input,48,self._ctx) if la_ == 1: - self.state = 475 + self.state = 477 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & 274) != 0): - self.state = 474 + self.state = 476 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 274) != 0)): self._errHandler.recoverInline(self) @@ -3522,18 +3536,18 @@ def joinOp(self): self.consume() - self.state = 477 + self.state = 479 self.match(HogQLParser.INNER) pass elif la_ == 2: - self.state = 478 - self.match(HogQLParser.INNER) self.state = 480 + self.match(HogQLParser.INNER) + self.state = 482 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & 274) != 0): - self.state = 479 + self.state = 481 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 274) != 0)): self._errHandler.recoverInline(self) @@ -3545,7 +3559,7 @@ def joinOp(self): pass elif la_ == 3: - self.state = 482 + self.state = 484 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 274) != 0)): self._errHandler.recoverInline(self) @@ -3560,15 +3574,15 @@ def joinOp(self): elif la_ == 2: localctx = HogQLParser.JoinOpLeftRightContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 499 + self.state = 501 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,49,self._ctx) + la_ = self._interp.adaptivePredict(self._input,53,self._ctx) if la_ == 1: - self.state = 486 + self.state = 488 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & 282) != 0) or _la==78: - self.state = 485 + self.state = 487 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 282) != 0) or _la==78): self._errHandler.recoverInline(self) @@ -3577,44 +3591,44 @@ def joinOp(self): self.consume() - self.state = 488 + self.state = 490 _la = self._input.LA(1) if not(_la==49 or _la==71): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 490 + self.state = 492 self._errHandler.sync(self) _la = self._input.LA(1) if _la==63: - self.state = 489 + self.state = 491 self.match(HogQLParser.OUTER) pass elif la_ == 2: - self.state = 492 + self.state = 494 _la = self._input.LA(1) if not(_la==49 or _la==71): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 494 + self.state = 496 self._errHandler.sync(self) _la = self._input.LA(1) if _la==63: - self.state = 493 + self.state = 495 self.match(HogQLParser.OUTER) - self.state = 497 + self.state = 499 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & 282) != 0) or _la==78: - self.state = 496 + self.state = 498 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 282) != 0) or _la==78): self._errHandler.recoverInline(self) @@ -3631,15 +3645,15 @@ def joinOp(self): elif la_ == 3: localctx = HogQLParser.JoinOpFullContext(self, localctx) self.enterOuterAlt(localctx, 3) - self.state = 515 + self.state = 517 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,54,self._ctx) + la_ = self._interp.adaptivePredict(self._input,58,self._ctx) if la_ == 1: - self.state = 502 + self.state = 504 self._errHandler.sync(self) _la = self._input.LA(1) if _la==1 or _la==4: - self.state = 501 + self.state = 503 _la = self._input.LA(1) if not(_la==1 or _la==4): self._errHandler.recoverInline(self) @@ -3648,34 +3662,34 @@ def joinOp(self): self.consume() - self.state = 504 - self.match(HogQLParser.FULL) self.state = 506 + self.match(HogQLParser.FULL) + self.state = 508 self._errHandler.sync(self) _la = self._input.LA(1) if _la==63: - self.state = 505 + self.state = 507 self.match(HogQLParser.OUTER) pass elif la_ == 2: - self.state = 508 - self.match(HogQLParser.FULL) self.state = 510 + self.match(HogQLParser.FULL) + self.state = 512 self._errHandler.sync(self) _la = self._input.LA(1) if _la==63: - self.state = 509 + self.state = 511 self.match(HogQLParser.OUTER) - self.state = 513 + self.state = 515 self._errHandler.sync(self) _la = self._input.LA(1) if _la==1 or _la==4: - self.state = 512 + self.state = 514 _la = self._input.LA(1) if not(_la==1 or _la==4): self._errHandler.recoverInline(self) @@ -3732,19 +3746,19 @@ def joinOpCross(self): localctx = HogQLParser.JoinOpCrossContext(self, self._ctx, self.state) self.enterRule(localctx, 72, self.RULE_joinOpCross) try: - self.state = 522 + self.state = 524 self._errHandler.sync(self) token = self._input.LA(1) if token in [16]: self.enterOuterAlt(localctx, 1) - self.state = 519 + self.state = 521 self.match(HogQLParser.CROSS) - self.state = 520 + self.state = 522 self.match(HogQLParser.JOIN) pass elif token in [112]: self.enterOuterAlt(localctx, 2) - self.state = 521 + self.state = 523 self.match(HogQLParser.COMMA) pass else: @@ -3799,34 +3813,34 @@ def joinConstraintClause(self): localctx = HogQLParser.JoinConstraintClauseContext(self, self._ctx, self.state) self.enterRule(localctx, 74, self.RULE_joinConstraintClause) try: - self.state = 533 + self.state = 535 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,57,self._ctx) + la_ = self._interp.adaptivePredict(self._input,61,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 524 + self.state = 526 self.match(HogQLParser.ON) - self.state = 525 + self.state = 527 self.columnExprList() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 526 + self.state = 528 self.match(HogQLParser.USING) - self.state = 527 + self.state = 529 self.match(HogQLParser.LPAREN) - self.state = 528 + self.state = 530 self.columnExprList() - self.state = 529 + self.state = 531 self.match(HogQLParser.RPAREN) pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 531 + self.state = 533 self.match(HogQLParser.USING) - self.state = 532 + self.state = 534 self.columnExprList() pass @@ -3878,17 +3892,17 @@ def sampleClause(self): self.enterRule(localctx, 76, self.RULE_sampleClause) try: self.enterOuterAlt(localctx, 1) - self.state = 535 + self.state = 537 self.match(HogQLParser.SAMPLE) - self.state = 536 + self.state = 538 self.ratioExpr() - self.state = 539 + self.state = 541 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,58,self._ctx) + la_ = self._interp.adaptivePredict(self._input,62,self._ctx) if la_ == 1: - self.state = 537 + self.state = 539 self.match(HogQLParser.OFFSET) - self.state = 538 + self.state = 540 self.ratioExpr() @@ -3940,17 +3954,17 @@ def orderExprList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 541 + self.state = 543 self.orderExpr() - self.state = 546 + self.state = 548 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 542 + self.state = 544 self.match(HogQLParser.COMMA) - self.state = 543 + self.state = 545 self.orderExpr() - self.state = 548 + self.state = 550 self._errHandler.sync(self) _la = self._input.LA(1) @@ -4017,13 +4031,13 @@ def orderExpr(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 549 - self.columnExpr(0) self.state = 551 + self.columnExpr(0) + self.state = 553 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & 6291584) != 0): - self.state = 550 + self.state = 552 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 6291584) != 0)): self._errHandler.recoverInline(self) @@ -4032,13 +4046,13 @@ def orderExpr(self): self.consume() - self.state = 555 + self.state = 557 self._errHandler.sync(self) _la = self._input.LA(1) if _la==58: - self.state = 553 + self.state = 555 self.match(HogQLParser.NULLS) - self.state = 554 + self.state = 556 _la = self._input.LA(1) if not(_la==28 or _la==47): self._errHandler.recoverInline(self) @@ -4047,13 +4061,13 @@ def orderExpr(self): self.consume() - self.state = 559 + self.state = 561 self._errHandler.sync(self) _la = self._input.LA(1) if _la==15: - self.state = 557 + self.state = 559 self.match(HogQLParser.COLLATE) - self.state = 558 + self.state = 560 self.match(HogQLParser.STRING_LITERAL) @@ -4104,25 +4118,25 @@ def ratioExpr(self): localctx = HogQLParser.RatioExprContext(self, self._ctx, self.state) self.enterRule(localctx, 82, self.RULE_ratioExpr) try: - self.state = 567 + self.state = 569 self._errHandler.sync(self) token = self._input.LA(1) if token in [124]: self.enterOuterAlt(localctx, 1) - self.state = 561 + self.state = 563 self.placeholder() pass elif token in [41, 55, 102, 103, 104, 105, 114, 116, 134]: self.enterOuterAlt(localctx, 2) - self.state = 562 + self.state = 564 self.numberLiteral() - self.state = 565 + self.state = 567 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,63,self._ctx) + la_ = self._interp.adaptivePredict(self._input,67,self._ctx) if la_ == 1: - self.state = 563 + self.state = 565 self.match(HogQLParser.SLASH) - self.state = 564 + self.state = 566 self.numberLiteral() @@ -4178,17 +4192,17 @@ def settingExprList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 569 + self.state = 571 self.settingExpr() - self.state = 574 + self.state = 576 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 570 + self.state = 572 self.match(HogQLParser.COMMA) - self.state = 571 + self.state = 573 self.settingExpr() - self.state = 576 + self.state = 578 self._errHandler.sync(self) _la = self._input.LA(1) @@ -4237,11 +4251,11 @@ def settingExpr(self): self.enterRule(localctx, 86, self.RULE_settingExpr) try: self.enterOuterAlt(localctx, 1) - self.state = 577 + self.state = 579 self.identifier() - self.state = 578 + self.state = 580 self.match(HogQLParser.EQ_SINGLE) - self.state = 579 + self.state = 581 self.literal() except RecognitionException as re: localctx.exception = re @@ -4290,27 +4304,27 @@ def windowExpr(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 582 + self.state = 584 self._errHandler.sync(self) _la = self._input.LA(1) if _la==65: - self.state = 581 + self.state = 583 self.winPartitionByClause() - self.state = 585 + self.state = 587 self._errHandler.sync(self) _la = self._input.LA(1) if _la==62: - self.state = 584 + self.state = 586 self.winOrderByClause() - self.state = 588 + self.state = 590 self._errHandler.sync(self) _la = self._input.LA(1) if _la==69 or _la==74: - self.state = 587 + self.state = 589 self.winFrameClause() @@ -4358,11 +4372,11 @@ def winPartitionByClause(self): self.enterRule(localctx, 90, self.RULE_winPartitionByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 590 + self.state = 592 self.match(HogQLParser.PARTITION) - self.state = 591 + self.state = 593 self.match(HogQLParser.BY) - self.state = 592 + self.state = 594 self.columnExprList() except RecognitionException as re: localctx.exception = re @@ -4408,11 +4422,11 @@ def winOrderByClause(self): self.enterRule(localctx, 92, self.RULE_winOrderByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 594 + self.state = 596 self.match(HogQLParser.ORDER) - self.state = 595 + self.state = 597 self.match(HogQLParser.BY) - self.state = 596 + self.state = 598 self.orderExprList() except RecognitionException as re: localctx.exception = re @@ -4459,14 +4473,14 @@ def winFrameClause(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 598 + self.state = 600 _la = self._input.LA(1) if not(_la==69 or _la==74): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 599 + self.state = 601 self.winFrameExtend() except RecognitionException as re: localctx.exception = re @@ -4541,25 +4555,25 @@ def winFrameExtend(self): localctx = HogQLParser.WinFrameExtendContext(self, self._ctx, self.state) self.enterRule(localctx, 96, self.RULE_winFrameExtend) try: - self.state = 607 + self.state = 609 self._errHandler.sync(self) token = self._input.LA(1) if token in [18, 41, 55, 90, 102, 103, 104, 105, 114, 116, 134]: localctx = HogQLParser.FrameStartContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 601 + self.state = 603 self.winFrameBound() pass elif token in [9]: localctx = HogQLParser.FrameBetweenContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 602 + self.state = 604 self.match(HogQLParser.BETWEEN) - self.state = 603 + self.state = 605 self.winFrameBound() - self.state = 604 + self.state = 606 self.match(HogQLParser.AND) - self.state = 605 + self.state = 607 self.winFrameBound() pass else: @@ -4618,41 +4632,41 @@ def winFrameBound(self): self.enterRule(localctx, 98, self.RULE_winFrameBound) try: self.enterOuterAlt(localctx, 1) - self.state = 621 + self.state = 623 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,70,self._ctx) + la_ = self._interp.adaptivePredict(self._input,74,self._ctx) if la_ == 1: - self.state = 609 + self.state = 611 self.match(HogQLParser.CURRENT) - self.state = 610 + self.state = 612 self.match(HogQLParser.ROW) pass elif la_ == 2: - self.state = 611 + self.state = 613 self.match(HogQLParser.UNBOUNDED) - self.state = 612 + self.state = 614 self.match(HogQLParser.PRECEDING) pass elif la_ == 3: - self.state = 613 + self.state = 615 self.match(HogQLParser.UNBOUNDED) - self.state = 614 + self.state = 616 self.match(HogQLParser.FOLLOWING) pass elif la_ == 4: - self.state = 615 + self.state = 617 self.numberLiteral() - self.state = 616 + self.state = 618 self.match(HogQLParser.PRECEDING) pass elif la_ == 5: - self.state = 618 + self.state = 620 self.numberLiteral() - self.state = 619 + self.state = 621 self.match(HogQLParser.FOLLOWING) pass @@ -4698,9 +4712,9 @@ def expr(self): self.enterRule(localctx, 100, self.RULE_expr) try: self.enterOuterAlt(localctx, 1) - self.state = 623 + self.state = 625 self.columnExpr(0) - self.state = 624 + self.state = 626 self.match(HogQLParser.EOF) except RecognitionException as re: localctx.exception = re @@ -4875,111 +4889,111 @@ def columnTypeExpr(self): self.enterRule(localctx, 102, self.RULE_columnTypeExpr) self._la = 0 # Token type try: - self.state = 673 + self.state = 675 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,75,self._ctx) + la_ = self._interp.adaptivePredict(self._input,79,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnTypeExprSimpleContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 626 + self.state = 628 self.identifier() pass elif la_ == 2: localctx = HogQLParser.ColumnTypeExprNestedContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 627 - self.identifier() - self.state = 628 - self.match(HogQLParser.LPAREN) self.state = 629 self.identifier() self.state = 630 + self.match(HogQLParser.LPAREN) + self.state = 631 + self.identifier() + self.state = 632 self.columnTypeExpr() - self.state = 637 + self.state = 639 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 631 + self.state = 633 self.match(HogQLParser.COMMA) - self.state = 632 + self.state = 634 self.identifier() - self.state = 633 + self.state = 635 self.columnTypeExpr() - self.state = 639 + self.state = 641 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 640 + self.state = 642 self.match(HogQLParser.RPAREN) pass elif la_ == 3: localctx = HogQLParser.ColumnTypeExprEnumContext(self, localctx) self.enterOuterAlt(localctx, 3) - self.state = 642 + self.state = 644 self.identifier() - self.state = 643 + self.state = 645 self.match(HogQLParser.LPAREN) - self.state = 644 + self.state = 646 self.enumValue() - self.state = 649 + self.state = 651 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 645 + self.state = 647 self.match(HogQLParser.COMMA) - self.state = 646 + self.state = 648 self.enumValue() - self.state = 651 + self.state = 653 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 652 + self.state = 654 self.match(HogQLParser.RPAREN) pass elif la_ == 4: localctx = HogQLParser.ColumnTypeExprComplexContext(self, localctx) self.enterOuterAlt(localctx, 4) - self.state = 654 + self.state = 656 self.identifier() - self.state = 655 + self.state = 657 self.match(HogQLParser.LPAREN) - self.state = 656 + self.state = 658 self.columnTypeExpr() - self.state = 661 + self.state = 663 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 657 + self.state = 659 self.match(HogQLParser.COMMA) - self.state = 658 + self.state = 660 self.columnTypeExpr() - self.state = 663 + self.state = 665 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 664 + self.state = 666 self.match(HogQLParser.RPAREN) pass elif la_ == 5: localctx = HogQLParser.ColumnTypeExprParamContext(self, localctx) self.enterOuterAlt(localctx, 5) - self.state = 666 + self.state = 668 self.identifier() - self.state = 667 - self.match(HogQLParser.LPAREN) self.state = 669 + self.match(HogQLParser.LPAREN) + self.state = 671 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 668 + self.state = 670 self.columnExprList() - self.state = 671 + self.state = 673 self.match(HogQLParser.RPAREN) pass @@ -5031,20 +5045,20 @@ def columnExprList(self): self.enterRule(localctx, 104, self.RULE_columnExprList) try: self.enterOuterAlt(localctx, 1) - self.state = 675 + self.state = 677 self.columnExpr(0) - self.state = 680 + self.state = 682 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,76,self._ctx) + _alt = self._interp.adaptivePredict(self._input,80,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: - self.state = 676 + self.state = 678 self.match(HogQLParser.COMMA) - self.state = 677 + self.state = 679 self.columnExpr(0) - self.state = 682 + self.state = 684 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,76,self._ctx) + _alt = self._interp.adaptivePredict(self._input,80,self._ctx) except RecognitionException as re: localctx.exception = re @@ -5104,9 +5118,6 @@ def __init__(self, parser, ctx:ParserRuleContext): # actually a HogQLParser.Colu def columnExpr(self): return self.getTypedRuleContext(HogQLParser.ColumnExprContext,0) - def alias(self): - return self.getTypedRuleContext(HogQLParser.AliasContext,0) - def AS(self): return self.getToken(HogQLParser.AS, 0) def identifier(self): @@ -5969,53 +5980,53 @@ def columnExpr(self, _p:int=0): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 832 + self.state = 834 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,96,self._ctx) + la_ = self._interp.adaptivePredict(self._input,100,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnExprCaseContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 684 - self.match(HogQLParser.CASE) self.state = 686 + self.match(HogQLParser.CASE) + self.state = 688 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,77,self._ctx) + la_ = self._interp.adaptivePredict(self._input,81,self._ctx) if la_ == 1: - self.state = 685 + self.state = 687 localctx.caseExpr = self.columnExpr(0) - self.state = 693 + self.state = 695 self._errHandler.sync(self) _la = self._input.LA(1) while True: - self.state = 688 + self.state = 690 self.match(HogQLParser.WHEN) - self.state = 689 + self.state = 691 localctx.whenExpr = self.columnExpr(0) - self.state = 690 + self.state = 692 self.match(HogQLParser.THEN) - self.state = 691 + self.state = 693 localctx.thenExpr = self.columnExpr(0) - self.state = 695 + self.state = 697 self._errHandler.sync(self) _la = self._input.LA(1) if not (_la==94): break - self.state = 699 + self.state = 701 self._errHandler.sync(self) _la = self._input.LA(1) if _la==24: - self.state = 697 + self.state = 699 self.match(HogQLParser.ELSE) - self.state = 698 + self.state = 700 localctx.elseExpr = self.columnExpr(0) - self.state = 701 + self.state = 703 self.match(HogQLParser.END) pass @@ -6023,17 +6034,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprCastContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 703 + self.state = 705 self.match(HogQLParser.CAST) - self.state = 704 + self.state = 706 self.match(HogQLParser.LPAREN) - self.state = 705 + self.state = 707 self.columnExpr(0) - self.state = 706 + self.state = 708 self.match(HogQLParser.AS) - self.state = 707 + self.state = 709 self.columnTypeExpr() - self.state = 708 + self.state = 710 self.match(HogQLParser.RPAREN) pass @@ -6041,9 +6052,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprDateContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 710 + self.state = 712 self.match(HogQLParser.DATE) - self.state = 711 + self.state = 713 self.match(HogQLParser.STRING_LITERAL) pass @@ -6051,11 +6062,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprIntervalContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 712 + self.state = 714 self.match(HogQLParser.INTERVAL) - self.state = 713 + self.state = 715 self.columnExpr(0) - self.state = 714 + self.state = 716 self.interval() pass @@ -6063,27 +6074,27 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprSubstringContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 716 + self.state = 718 self.match(HogQLParser.SUBSTRING) - self.state = 717 + self.state = 719 self.match(HogQLParser.LPAREN) - self.state = 718 + self.state = 720 self.columnExpr(0) - self.state = 719 + self.state = 721 self.match(HogQLParser.FROM) - self.state = 720 + self.state = 722 self.columnExpr(0) - self.state = 723 + self.state = 725 self._errHandler.sync(self) _la = self._input.LA(1) if _la==31: - self.state = 721 + self.state = 723 self.match(HogQLParser.FOR) - self.state = 722 + self.state = 724 self.columnExpr(0) - self.state = 725 + self.state = 727 self.match(HogQLParser.RPAREN) pass @@ -6091,9 +6102,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTimestampContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 727 + self.state = 729 self.match(HogQLParser.TIMESTAMP) - self.state = 728 + self.state = 730 self.match(HogQLParser.STRING_LITERAL) pass @@ -6101,24 +6112,24 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTrimContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 729 + self.state = 731 self.match(HogQLParser.TRIM) - self.state = 730 + self.state = 732 self.match(HogQLParser.LPAREN) - self.state = 731 + self.state = 733 _la = self._input.LA(1) if not(_la==10 or _la==48 or _la==87): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 732 + self.state = 734 self.string() - self.state = 733 + self.state = 735 self.match(HogQLParser.FROM) - self.state = 734 + self.state = 736 self.columnExpr(0) - self.state = 735 + self.state = 737 self.match(HogQLParser.RPAREN) pass @@ -6126,54 +6137,54 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprWinFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 737 + self.state = 739 self.identifier() - self.state = 738 - self.match(HogQLParser.LPAREN) self.state = 740 + self.match(HogQLParser.LPAREN) + self.state = 742 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 739 + self.state = 741 self.columnExprList() - self.state = 742 + self.state = 744 self.match(HogQLParser.RPAREN) - self.state = 752 + self.state = 754 self._errHandler.sync(self) _la = self._input.LA(1) if _la==126: - self.state = 744 - self.match(HogQLParser.LPAREN) self.state = 746 + self.match(HogQLParser.LPAREN) + self.state = 748 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,82,self._ctx) + la_ = self._interp.adaptivePredict(self._input,86,self._ctx) if la_ == 1: - self.state = 745 + self.state = 747 self.match(HogQLParser.DISTINCT) - self.state = 749 + self.state = 751 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 748 + self.state = 750 self.columnArgList() - self.state = 751 + self.state = 753 self.match(HogQLParser.RPAREN) - self.state = 754 + self.state = 756 self.match(HogQLParser.OVER) - self.state = 755 + self.state = 757 self.match(HogQLParser.LPAREN) - self.state = 756 + self.state = 758 self.windowExpr() - self.state = 757 + self.state = 759 self.match(HogQLParser.RPAREN) pass @@ -6181,50 +6192,50 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprWinFunctionTargetContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 759 + self.state = 761 self.identifier() - self.state = 760 - self.match(HogQLParser.LPAREN) self.state = 762 + self.match(HogQLParser.LPAREN) + self.state = 764 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 761 + self.state = 763 self.columnExprList() - self.state = 764 + self.state = 766 self.match(HogQLParser.RPAREN) - self.state = 774 + self.state = 776 self._errHandler.sync(self) _la = self._input.LA(1) if _la==126: - self.state = 766 - self.match(HogQLParser.LPAREN) self.state = 768 + self.match(HogQLParser.LPAREN) + self.state = 770 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,86,self._ctx) + la_ = self._interp.adaptivePredict(self._input,90,self._ctx) if la_ == 1: - self.state = 767 + self.state = 769 self.match(HogQLParser.DISTINCT) - self.state = 771 + self.state = 773 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 770 + self.state = 772 self.columnArgList() - self.state = 773 + self.state = 775 self.match(HogQLParser.RPAREN) - self.state = 776 + self.state = 778 self.match(HogQLParser.OVER) - self.state = 777 + self.state = 779 self.identifier() pass @@ -6232,45 +6243,45 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 779 + self.state = 781 self.identifier() - self.state = 785 + self.state = 787 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,90,self._ctx) + la_ = self._interp.adaptivePredict(self._input,94,self._ctx) if la_ == 1: - self.state = 780 - self.match(HogQLParser.LPAREN) self.state = 782 + self.match(HogQLParser.LPAREN) + self.state = 784 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 781 + self.state = 783 self.columnExprList() - self.state = 784 + self.state = 786 self.match(HogQLParser.RPAREN) - self.state = 787 - self.match(HogQLParser.LPAREN) self.state = 789 + self.match(HogQLParser.LPAREN) + self.state = 791 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,91,self._ctx) + la_ = self._interp.adaptivePredict(self._input,95,self._ctx) if la_ == 1: - self.state = 788 + self.state = 790 self.match(HogQLParser.DISTINCT) - self.state = 792 + self.state = 794 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 791 + self.state = 793 self.columnArgList() - self.state = 794 + self.state = 796 self.match(HogQLParser.RPAREN) pass @@ -6278,7 +6289,7 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTagElementContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 796 + self.state = 798 self.hogqlxTagElement() pass @@ -6286,7 +6297,7 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTemplateStringContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 797 + self.state = 799 self.templateString() pass @@ -6294,7 +6305,7 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprLiteralContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 798 + self.state = 800 self.literal() pass @@ -6302,9 +6313,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprNegateContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 799 + self.state = 801 self.match(HogQLParser.DASH) - self.state = 800 + self.state = 802 self.columnExpr(19) pass @@ -6312,9 +6323,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprNotContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 801 + self.state = 803 self.match(HogQLParser.NOT) - self.state = 802 + self.state = 804 self.columnExpr(13) pass @@ -6322,17 +6333,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprAsteriskContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 806 + self.state = 808 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): - self.state = 803 + self.state = 805 self.tableIdentifier() - self.state = 804 + self.state = 806 self.match(HogQLParser.DOT) - self.state = 808 + self.state = 810 self.match(HogQLParser.ASTERISK) pass @@ -6340,11 +6351,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprSubqueryContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 809 + self.state = 811 self.match(HogQLParser.LPAREN) - self.state = 810 + self.state = 812 self.selectUnionStmt() - self.state = 811 + self.state = 813 self.match(HogQLParser.RPAREN) pass @@ -6352,11 +6363,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprParensContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 813 + self.state = 815 self.match(HogQLParser.LPAREN) - self.state = 814 + self.state = 816 self.columnExpr(0) - self.state = 815 + self.state = 817 self.match(HogQLParser.RPAREN) pass @@ -6364,11 +6375,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTupleContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 817 + self.state = 819 self.match(HogQLParser.LPAREN) - self.state = 818 + self.state = 820 self.columnExprList() - self.state = 819 + self.state = 821 self.match(HogQLParser.RPAREN) pass @@ -6376,17 +6387,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprArrayContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 821 - self.match(HogQLParser.LBRACKET) self.state = 823 + self.match(HogQLParser.LBRACKET) + self.state = 825 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 822 + self.state = 824 self.columnExprList() - self.state = 825 + self.state = 827 self.match(HogQLParser.RBRACKET) pass @@ -6394,17 +6405,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprDictContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 826 - self.match(HogQLParser.LBRACE) self.state = 828 + self.match(HogQLParser.LBRACE) + self.state = 830 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 827 + self.state = 829 self.kvPairList() - self.state = 830 + self.state = 832 self.match(HogQLParser.RBRACE) pass @@ -6412,50 +6423,50 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprIdentifierContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 831 + self.state = 833 self.columnIdentifier() pass self._ctx.stop = self._input.LT(-1) - self.state = 927 + self.state = 928 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,107,self._ctx) + _alt = self._interp.adaptivePredict(self._input,111,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 925 + self.state = 926 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,106,self._ctx) + la_ = self._interp.adaptivePredict(self._input,110,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnExprPrecedence1Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 834 + self.state = 836 if not self.precpred(self._ctx, 18): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 18)") - self.state = 838 + self.state = 840 self._errHandler.sync(self) token = self._input.LA(1) if token in [108]: - self.state = 835 + self.state = 837 localctx.operator = self.match(HogQLParser.ASTERISK) pass elif token in [146]: - self.state = 836 + self.state = 838 localctx.operator = self.match(HogQLParser.SLASH) pass elif token in [133]: - self.state = 837 + self.state = 839 localctx.operator = self.match(HogQLParser.PERCENT) pass else: raise NoViableAltException(self) - self.state = 840 + self.state = 842 localctx.right = self.columnExpr(19) pass @@ -6463,29 +6474,29 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprPrecedence2Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 841 + self.state = 843 if not self.precpred(self._ctx, 17): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 17)") - self.state = 845 + self.state = 847 self._errHandler.sync(self) token = self._input.LA(1) if token in [134]: - self.state = 842 + self.state = 844 localctx.operator = self.match(HogQLParser.PLUS) pass elif token in [114]: - self.state = 843 + self.state = 845 localctx.operator = self.match(HogQLParser.DASH) pass elif token in [113]: - self.state = 844 + self.state = 846 localctx.operator = self.match(HogQLParser.CONCAT) pass else: raise NoViableAltException(self) - self.state = 847 + self.state = 849 localctx.right = self.columnExpr(18) pass @@ -6493,79 +6504,79 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprPrecedence3Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 848 + self.state = 850 if not self.precpred(self._ctx, 16): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 16)") - self.state = 873 + self.state = 875 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,102,self._ctx) + la_ = self._interp.adaptivePredict(self._input,106,self._ctx) if la_ == 1: - self.state = 849 + self.state = 851 localctx.operator = self.match(HogQLParser.EQ_DOUBLE) pass elif la_ == 2: - self.state = 850 + self.state = 852 localctx.operator = self.match(HogQLParser.EQ_SINGLE) pass elif la_ == 3: - self.state = 851 + self.state = 853 localctx.operator = self.match(HogQLParser.NOT_EQ) pass elif la_ == 4: - self.state = 852 + self.state = 854 localctx.operator = self.match(HogQLParser.LT_EQ) pass elif la_ == 5: - self.state = 853 + self.state = 855 localctx.operator = self.match(HogQLParser.LT) pass elif la_ == 6: - self.state = 854 + self.state = 856 localctx.operator = self.match(HogQLParser.GT_EQ) pass elif la_ == 7: - self.state = 855 + self.state = 857 localctx.operator = self.match(HogQLParser.GT) pass elif la_ == 8: - self.state = 857 + self.state = 859 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 856 + self.state = 858 localctx.operator = self.match(HogQLParser.NOT) - self.state = 859 - self.match(HogQLParser.IN) self.state = 861 + self.match(HogQLParser.IN) + self.state = 863 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,100,self._ctx) + la_ = self._interp.adaptivePredict(self._input,104,self._ctx) if la_ == 1: - self.state = 860 + self.state = 862 self.match(HogQLParser.COHORT) pass elif la_ == 9: - self.state = 864 + self.state = 866 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 863 + self.state = 865 localctx.operator = self.match(HogQLParser.NOT) - self.state = 866 + self.state = 868 _la = self._input.LA(1) if not(_la==39 or _la==51): self._errHandler.recoverInline(self) @@ -6575,209 +6586,204 @@ def columnExpr(self, _p:int=0): pass elif la_ == 10: - self.state = 867 + self.state = 869 localctx.operator = self.match(HogQLParser.REGEX_SINGLE) pass elif la_ == 11: - self.state = 868 + self.state = 870 localctx.operator = self.match(HogQLParser.REGEX_DOUBLE) pass elif la_ == 12: - self.state = 869 + self.state = 871 localctx.operator = self.match(HogQLParser.NOT_REGEX) pass elif la_ == 13: - self.state = 870 + self.state = 872 localctx.operator = self.match(HogQLParser.IREGEX_SINGLE) pass elif la_ == 14: - self.state = 871 + self.state = 873 localctx.operator = self.match(HogQLParser.IREGEX_DOUBLE) pass elif la_ == 15: - self.state = 872 + self.state = 874 localctx.operator = self.match(HogQLParser.NOT_IREGEX) pass - self.state = 875 + self.state = 877 localctx.right = self.columnExpr(17) pass elif la_ == 4: localctx = HogQLParser.ColumnExprNullishContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 876 + self.state = 878 if not self.precpred(self._ctx, 14): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 14)") - self.state = 877 + self.state = 879 self.match(HogQLParser.NULLISH) - self.state = 878 + self.state = 880 self.columnExpr(15) pass elif la_ == 5: localctx = HogQLParser.ColumnExprAndContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 879 + self.state = 881 if not self.precpred(self._ctx, 12): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 12)") - self.state = 880 + self.state = 882 self.match(HogQLParser.AND) - self.state = 881 + self.state = 883 self.columnExpr(13) pass elif la_ == 6: localctx = HogQLParser.ColumnExprOrContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 882 + self.state = 884 if not self.precpred(self._ctx, 11): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 11)") - self.state = 883 + self.state = 885 self.match(HogQLParser.OR) - self.state = 884 + self.state = 886 self.columnExpr(12) pass elif la_ == 7: localctx = HogQLParser.ColumnExprBetweenContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 885 + self.state = 887 if not self.precpred(self._ctx, 10): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 10)") - self.state = 887 + self.state = 889 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 886 + self.state = 888 self.match(HogQLParser.NOT) - self.state = 889 + self.state = 891 self.match(HogQLParser.BETWEEN) - self.state = 890 + self.state = 892 self.columnExpr(0) - self.state = 891 + self.state = 893 self.match(HogQLParser.AND) - self.state = 892 + self.state = 894 self.columnExpr(11) pass elif la_ == 8: localctx = HogQLParser.ColumnExprTernaryOpContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 894 + self.state = 896 if not self.precpred(self._ctx, 9): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 9)") - self.state = 895 + self.state = 897 self.match(HogQLParser.QUERY) - self.state = 896 + self.state = 898 self.columnExpr(0) - self.state = 897 + self.state = 899 self.match(HogQLParser.COLON) - self.state = 898 + self.state = 900 self.columnExpr(9) pass elif la_ == 9: localctx = HogQLParser.ColumnExprArrayAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 900 + self.state = 902 if not self.precpred(self._ctx, 22): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 22)") - self.state = 901 + self.state = 903 self.match(HogQLParser.LBRACKET) - self.state = 902 + self.state = 904 self.columnExpr(0) - self.state = 903 + self.state = 905 self.match(HogQLParser.RBRACKET) pass elif la_ == 10: localctx = HogQLParser.ColumnExprTupleAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 905 + self.state = 907 if not self.precpred(self._ctx, 21): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 21)") - self.state = 906 + self.state = 908 self.match(HogQLParser.DOT) - self.state = 907 + self.state = 909 self.match(HogQLParser.DECIMAL_LITERAL) pass elif la_ == 11: localctx = HogQLParser.ColumnExprPropertyAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 908 + self.state = 910 if not self.precpred(self._ctx, 20): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 20)") - self.state = 909 + self.state = 911 self.match(HogQLParser.DOT) - self.state = 910 + self.state = 912 self.identifier() pass elif la_ == 12: localctx = HogQLParser.ColumnExprIsNullContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 911 + self.state = 913 if not self.precpred(self._ctx, 15): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 15)") - self.state = 912 - self.match(HogQLParser.IS) self.state = 914 + self.match(HogQLParser.IS) + self.state = 916 self._errHandler.sync(self) _la = self._input.LA(1) if _la==56: - self.state = 913 + self.state = 915 self.match(HogQLParser.NOT) - self.state = 916 + self.state = 918 self.match(HogQLParser.NULL_SQL) pass elif la_ == 13: localctx = HogQLParser.ColumnExprAliasContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 917 + self.state = 919 if not self.precpred(self._ctx, 8): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 8)") - self.state = 923 + self.state = 924 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,105,self._ctx) + la_ = self._interp.adaptivePredict(self._input,109,self._ctx) if la_ == 1: - self.state = 918 - self.alias() - pass - - elif la_ == 2: - self.state = 919 - self.match(HogQLParser.AS) self.state = 920 + self.match(HogQLParser.AS) + self.state = 921 self.identifier() pass - elif la_ == 3: - self.state = 921 - self.match(HogQLParser.AS) + elif la_ == 2: self.state = 922 + self.match(HogQLParser.AS) + self.state = 923 self.match(HogQLParser.STRING_LITERAL) pass @@ -6785,9 +6791,9 @@ def columnExpr(self, _p:int=0): pass - self.state = 929 + self.state = 930 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,107,self._ctx) + _alt = self._interp.adaptivePredict(self._input,111,self._ctx) except RecognitionException as re: localctx.exception = re @@ -6837,17 +6843,17 @@ def columnArgList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 930 + self.state = 931 self.columnArgExpr() - self.state = 935 + self.state = 936 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 931 - self.match(HogQLParser.COMMA) self.state = 932 + self.match(HogQLParser.COMMA) + self.state = 933 self.columnArgExpr() - self.state = 937 + self.state = 938 self._errHandler.sync(self) _la = self._input.LA(1) @@ -6892,18 +6898,18 @@ def columnArgExpr(self): localctx = HogQLParser.ColumnArgExprContext(self, self._ctx, self.state) self.enterRule(localctx, 110, self.RULE_columnArgExpr) try: - self.state = 940 + self.state = 941 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,109,self._ctx) + la_ = self._interp.adaptivePredict(self._input,113,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 938 + self.state = 939 self.columnLambdaExpr() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 939 + self.state = 940 self.columnExpr(0) pass @@ -6969,41 +6975,41 @@ def columnLambdaExpr(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 961 + self.state = 962 self._errHandler.sync(self) token = self._input.LA(1) if token in [126]: - self.state = 942 - self.match(HogQLParser.LPAREN) self.state = 943 + self.match(HogQLParser.LPAREN) + self.state = 944 self.identifier() - self.state = 948 + self.state = 949 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 944 - self.match(HogQLParser.COMMA) self.state = 945 + self.match(HogQLParser.COMMA) + self.state = 946 self.identifier() - self.state = 950 + self.state = 951 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 951 + self.state = 952 self.match(HogQLParser.RPAREN) pass elif token in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 97, 98, 99, 101]: - self.state = 953 + self.state = 954 self.identifier() - self.state = 958 + self.state = 959 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 954 - self.match(HogQLParser.COMMA) self.state = 955 + self.match(HogQLParser.COMMA) + self.state = 956 self.identifier() - self.state = 960 + self.state = 961 self._errHandler.sync(self) _la = self._input.LA(1) @@ -7011,9 +7017,9 @@ def columnLambdaExpr(self): else: raise NoViableAltException(self) - self.state = 963 - self.match(HogQLParser.ARROW) self.state = 964 + self.match(HogQLParser.ARROW) + self.state = 965 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -7118,66 +7124,66 @@ def hogqlxTagElement(self): self.enterRule(localctx, 114, self.RULE_hogqlxTagElement) self._la = 0 # Token type try: - self.state = 994 + self.state = 995 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,116,self._ctx) + la_ = self._interp.adaptivePredict(self._input,120,self._ctx) if la_ == 1: localctx = HogQLParser.HogqlxTagElementClosedContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 966 - self.match(HogQLParser.LT) self.state = 967 + self.match(HogQLParser.LT) + self.state = 968 self.identifier() - self.state = 971 + self.state = 972 self._errHandler.sync(self) _la = self._input.LA(1) while (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): - self.state = 968 + self.state = 969 self.hogqlxTagAttribute() - self.state = 973 + self.state = 974 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 974 - self.match(HogQLParser.SLASH) self.state = 975 + self.match(HogQLParser.SLASH) + self.state = 976 self.match(HogQLParser.GT) pass elif la_ == 2: localctx = HogQLParser.HogqlxTagElementNestedContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 977 - self.match(HogQLParser.LT) self.state = 978 + self.match(HogQLParser.LT) + self.state = 979 self.identifier() - self.state = 982 + self.state = 983 self._errHandler.sync(self) _la = self._input.LA(1) while (((_la) & ~0x3f) == 0 and ((1 << _la) & -181272084561788930) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 201863462911) != 0): - self.state = 979 + self.state = 980 self.hogqlxTagAttribute() - self.state = 984 + self.state = 985 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 985 + self.state = 986 self.match(HogQLParser.GT) - self.state = 987 + self.state = 988 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,115,self._ctx) + la_ = self._interp.adaptivePredict(self._input,119,self._ctx) if la_ == 1: - self.state = 986 + self.state = 987 self.hogqlxTagElement() - self.state = 989 - self.match(HogQLParser.LT) self.state = 990 - self.match(HogQLParser.SLASH) + self.match(HogQLParser.LT) self.state = 991 - self.identifier() + self.match(HogQLParser.SLASH) self.state = 992 + self.identifier() + self.state = 993 self.match(HogQLParser.GT) pass @@ -7236,36 +7242,36 @@ def hogqlxTagAttribute(self): localctx = HogQLParser.HogqlxTagAttributeContext(self, self._ctx, self.state) self.enterRule(localctx, 116, self.RULE_hogqlxTagAttribute) try: - self.state = 1007 + self.state = 1008 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,117,self._ctx) + la_ = self._interp.adaptivePredict(self._input,121,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 996 - self.identifier() self.state = 997 - self.match(HogQLParser.EQ_SINGLE) + self.identifier() self.state = 998 + self.match(HogQLParser.EQ_SINGLE) + self.state = 999 self.string() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 1000 - self.identifier() self.state = 1001 - self.match(HogQLParser.EQ_SINGLE) + self.identifier() self.state = 1002 - self.match(HogQLParser.LBRACE) + self.match(HogQLParser.EQ_SINGLE) self.state = 1003 - self.columnExpr(0) + self.match(HogQLParser.LBRACE) self.state = 1004 + self.columnExpr(0) + self.state = 1005 self.match(HogQLParser.RBRACE) pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 1006 + self.state = 1007 self.identifier() pass @@ -7318,17 +7324,17 @@ def withExprList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1009 + self.state = 1010 self.withExpr() - self.state = 1014 + self.state = 1015 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 1010 - self.match(HogQLParser.COMMA) self.state = 1011 + self.match(HogQLParser.COMMA) + self.state = 1012 self.withExpr() - self.state = 1016 + self.state = 1017 self._errHandler.sync(self) _la = self._input.LA(1) @@ -7412,32 +7418,32 @@ def withExpr(self): localctx = HogQLParser.WithExprContext(self, self._ctx, self.state) self.enterRule(localctx, 120, self.RULE_withExpr) try: - self.state = 1027 + self.state = 1028 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,119,self._ctx) + la_ = self._interp.adaptivePredict(self._input,123,self._ctx) if la_ == 1: localctx = HogQLParser.WithExprSubqueryContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 1017 - self.identifier() self.state = 1018 - self.match(HogQLParser.AS) + self.identifier() self.state = 1019 - self.match(HogQLParser.LPAREN) + self.match(HogQLParser.AS) self.state = 1020 - self.selectUnionStmt() + self.match(HogQLParser.LPAREN) self.state = 1021 + self.selectUnionStmt() + self.state = 1022 self.match(HogQLParser.RPAREN) pass elif la_ == 2: localctx = HogQLParser.WithExprColumnContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 1023 - self.columnExpr(0) self.state = 1024 - self.match(HogQLParser.AS) + self.columnExpr(0) self.state = 1025 + self.match(HogQLParser.AS) + self.state = 1026 self.identifier() pass @@ -7490,27 +7496,27 @@ def columnIdentifier(self): localctx = HogQLParser.ColumnIdentifierContext(self, self._ctx, self.state) self.enterRule(localctx, 122, self.RULE_columnIdentifier) try: - self.state = 1036 + self.state = 1037 self._errHandler.sync(self) token = self._input.LA(1) if token in [124]: self.enterOuterAlt(localctx, 1) - self.state = 1029 + self.state = 1030 self.placeholder() pass elif token in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 97, 98, 99, 101]: self.enterOuterAlt(localctx, 2) - self.state = 1033 + self.state = 1034 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,120,self._ctx) + la_ = self._interp.adaptivePredict(self._input,124,self._ctx) if la_ == 1: - self.state = 1030 - self.tableIdentifier() self.state = 1031 + self.tableIdentifier() + self.state = 1032 self.match(HogQLParser.DOT) - self.state = 1035 + self.state = 1036 self.nestedIdentifier() pass else: @@ -7563,20 +7569,20 @@ def nestedIdentifier(self): self.enterRule(localctx, 124, self.RULE_nestedIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 1038 + self.state = 1039 self.identifier() - self.state = 1043 + self.state = 1044 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,122,self._ctx) + _alt = self._interp.adaptivePredict(self._input,126,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: - self.state = 1039 - self.match(HogQLParser.DOT) self.state = 1040 + self.match(HogQLParser.DOT) + self.state = 1041 self.identifier() - self.state = 1045 + self.state = 1046 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,122,self._ctx) + _alt = self._interp.adaptivePredict(self._input,126,self._ctx) except RecognitionException as re: localctx.exception = re @@ -7727,15 +7733,15 @@ def tableExpr(self, _p:int=0): self.enterRecursionRule(localctx, 126, self.RULE_tableExpr, _p) try: self.enterOuterAlt(localctx, 1) - self.state = 1055 + self.state = 1056 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,123,self._ctx) + la_ = self._interp.adaptivePredict(self._input,127,self._ctx) if la_ == 1: localctx = HogQLParser.TableExprIdentifierContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1047 + self.state = 1048 self.tableIdentifier() pass @@ -7743,7 +7749,7 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1048 + self.state = 1049 self.tableFunctionExpr() pass @@ -7751,11 +7757,11 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprSubqueryContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1049 - self.match(HogQLParser.LPAREN) self.state = 1050 - self.selectUnionStmt() + self.match(HogQLParser.LPAREN) self.state = 1051 + self.selectUnionStmt() + self.state = 1052 self.match(HogQLParser.RPAREN) pass @@ -7763,7 +7769,7 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprTagContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1053 + self.state = 1054 self.hogqlxTagElement() pass @@ -7771,15 +7777,15 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprPlaceholderContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 1054 + self.state = 1055 self.placeholder() pass self._ctx.stop = self._input.LT(-1) - self.state = 1065 + self.state = 1066 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,125,self._ctx) + _alt = self._interp.adaptivePredict(self._input,129,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: @@ -7787,29 +7793,29 @@ def tableExpr(self, _p:int=0): _prevctx = localctx localctx = HogQLParser.TableExprAliasContext(self, HogQLParser.TableExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_tableExpr) - self.state = 1057 + self.state = 1058 if not self.precpred(self._ctx, 3): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 3)") - self.state = 1061 + self.state = 1062 self._errHandler.sync(self) token = self._input.LA(1) if token in [19, 28, 37, 46, 101]: - self.state = 1058 + self.state = 1059 self.alias() pass elif token in [6]: - self.state = 1059 - self.match(HogQLParser.AS) self.state = 1060 + self.match(HogQLParser.AS) + self.state = 1061 self.identifier() pass else: raise NoViableAltException(self) - self.state = 1067 + self.state = 1068 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,125,self._ctx) + _alt = self._interp.adaptivePredict(self._input,129,self._ctx) except RecognitionException as re: localctx.exception = re @@ -7860,19 +7866,19 @@ def tableFunctionExpr(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1068 - self.identifier() self.state = 1069 + self.identifier() + self.state = 1070 self.match(HogQLParser.LPAREN) - self.state = 1071 + self.state = 1072 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la) & ~0x3f) == 0 and ((1 << _la) & -1125900443713538) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 8076106347046764543) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 577) != 0): - self.state = 1070 + self.state = 1071 self.tableArgList() - self.state = 1073 + self.state = 1074 self.match(HogQLParser.RPAREN) except RecognitionException as re: localctx.exception = re @@ -7919,17 +7925,17 @@ def tableIdentifier(self): self.enterRule(localctx, 130, self.RULE_tableIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 1078 + self.state = 1079 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,127,self._ctx) + la_ = self._interp.adaptivePredict(self._input,131,self._ctx) if la_ == 1: - self.state = 1075 - self.databaseIdentifier() self.state = 1076 + self.databaseIdentifier() + self.state = 1077 self.match(HogQLParser.DOT) - self.state = 1080 + self.state = 1081 self.identifier() except RecognitionException as re: localctx.exception = re @@ -7979,17 +7985,17 @@ def tableArgList(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1082 + self.state = 1083 self.columnExpr(0) - self.state = 1087 + self.state = 1088 self._errHandler.sync(self) _la = self._input.LA(1) while _la==112: - self.state = 1083 - self.match(HogQLParser.COMMA) self.state = 1084 + self.match(HogQLParser.COMMA) + self.state = 1085 self.columnExpr(0) - self.state = 1089 + self.state = 1090 self._errHandler.sync(self) _la = self._input.LA(1) @@ -8031,7 +8037,7 @@ def databaseIdentifier(self): self.enterRule(localctx, 134, self.RULE_databaseIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 1090 + self.state = 1091 self.identifier() except RecognitionException as re: localctx.exception = re @@ -8082,19 +8088,19 @@ def floatingLiteral(self): self.enterRule(localctx, 136, self.RULE_floatingLiteral) self._la = 0 # Token type try: - self.state = 1100 + self.state = 1101 self._errHandler.sync(self) token = self._input.LA(1) if token in [102]: self.enterOuterAlt(localctx, 1) - self.state = 1092 + self.state = 1093 self.match(HogQLParser.FLOATING_LITERAL) pass elif token in [116]: self.enterOuterAlt(localctx, 2) - self.state = 1093 - self.match(HogQLParser.DOT) self.state = 1094 + self.match(HogQLParser.DOT) + self.state = 1095 _la = self._input.LA(1) if not(_la==103 or _la==104): self._errHandler.recoverInline(self) @@ -8104,15 +8110,15 @@ def floatingLiteral(self): pass elif token in [104]: self.enterOuterAlt(localctx, 3) - self.state = 1095 - self.match(HogQLParser.DECIMAL_LITERAL) self.state = 1096 + self.match(HogQLParser.DECIMAL_LITERAL) + self.state = 1097 self.match(HogQLParser.DOT) - self.state = 1098 + self.state = 1099 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,129,self._ctx) + la_ = self._interp.adaptivePredict(self._input,133,self._ctx) if la_ == 1: - self.state = 1097 + self.state = 1098 _la = self._input.LA(1) if not(_la==103 or _la==104): self._errHandler.recoverInline(self) @@ -8185,11 +8191,11 @@ def numberLiteral(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1103 + self.state = 1104 self._errHandler.sync(self) _la = self._input.LA(1) if _la==114 or _la==134: - self.state = 1102 + self.state = 1103 _la = self._input.LA(1) if not(_la==114 or _la==134): self._errHandler.recoverInline(self) @@ -8198,36 +8204,36 @@ def numberLiteral(self): self.consume() - self.state = 1111 + self.state = 1112 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,132,self._ctx) + la_ = self._interp.adaptivePredict(self._input,136,self._ctx) if la_ == 1: - self.state = 1105 + self.state = 1106 self.floatingLiteral() pass elif la_ == 2: - self.state = 1106 + self.state = 1107 self.match(HogQLParser.OCTAL_LITERAL) pass elif la_ == 3: - self.state = 1107 + self.state = 1108 self.match(HogQLParser.DECIMAL_LITERAL) pass elif la_ == 4: - self.state = 1108 + self.state = 1109 self.match(HogQLParser.HEXADECIMAL_LITERAL) pass elif la_ == 5: - self.state = 1109 + self.state = 1110 self.match(HogQLParser.INF) pass elif la_ == 6: - self.state = 1110 + self.state = 1111 self.match(HogQLParser.NAN_SQL) pass @@ -8275,22 +8281,22 @@ def literal(self): localctx = HogQLParser.LiteralContext(self, self._ctx, self.state) self.enterRule(localctx, 140, self.RULE_literal) try: - self.state = 1116 + self.state = 1117 self._errHandler.sync(self) token = self._input.LA(1) if token in [41, 55, 102, 103, 104, 105, 114, 116, 134]: self.enterOuterAlt(localctx, 1) - self.state = 1113 + self.state = 1114 self.numberLiteral() pass elif token in [106]: self.enterOuterAlt(localctx, 2) - self.state = 1114 + self.state = 1115 self.match(HogQLParser.STRING_LITERAL) pass elif token in [57]: self.enterOuterAlt(localctx, 3) - self.state = 1115 + self.state = 1116 self.match(HogQLParser.NULL_SQL) pass else: @@ -8355,7 +8361,7 @@ def interval(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1118 + self.state = 1119 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 27021666484748288) != 0) or ((((_la - 68)) & ~0x3f) == 0 and ((1 << (_la - 68)) & 2181038337) != 0)): self._errHandler.recoverInline(self) @@ -8652,7 +8658,7 @@ def keyword(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1120 + self.state = 1121 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & -208293751046537218) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & 29527896047) != 0)): self._errHandler.recoverInline(self) @@ -8706,7 +8712,7 @@ def keywordForAlias(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1122 + self.state = 1123 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 70506452090880) != 0)): self._errHandler.recoverInline(self) @@ -8753,17 +8759,17 @@ def alias(self): localctx = HogQLParser.AliasContext(self, self._ctx, self.state) self.enterRule(localctx, 148, self.RULE_alias) try: - self.state = 1126 + self.state = 1127 self._errHandler.sync(self) token = self._input.LA(1) if token in [101]: self.enterOuterAlt(localctx, 1) - self.state = 1124 + self.state = 1125 self.match(HogQLParser.IDENTIFIER) pass elif token in [19, 28, 37, 46]: self.enterOuterAlt(localctx, 2) - self.state = 1125 + self.state = 1126 self.keywordForAlias() pass else: @@ -8813,22 +8819,22 @@ def identifier(self): localctx = HogQLParser.IdentifierContext(self, self._ctx, self.state) self.enterRule(localctx, 150, self.RULE_identifier) try: - self.state = 1131 + self.state = 1132 self._errHandler.sync(self) token = self._input.LA(1) if token in [101]: self.enterOuterAlt(localctx, 1) - self.state = 1128 + self.state = 1129 self.match(HogQLParser.IDENTIFIER) pass elif token in [20, 36, 53, 54, 68, 76, 93, 99]: self.enterOuterAlt(localctx, 2) - self.state = 1129 + self.state = 1130 self.interval() pass elif token in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 51, 52, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 69, 70, 71, 72, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 95, 97, 98]: self.enterOuterAlt(localctx, 3) - self.state = 1130 + self.state = 1131 self.keyword() pass else: @@ -8879,11 +8885,11 @@ def enumValue(self): self.enterRule(localctx, 152, self.RULE_enumValue) try: self.enterOuterAlt(localctx, 1) - self.state = 1133 - self.string() self.state = 1134 - self.match(HogQLParser.EQ_SINGLE) + self.string() self.state = 1135 + self.match(HogQLParser.EQ_SINGLE) + self.state = 1136 self.numberLiteral() except RecognitionException as re: localctx.exception = re @@ -8929,11 +8935,11 @@ def placeholder(self): self.enterRule(localctx, 154, self.RULE_placeholder) try: self.enterOuterAlt(localctx, 1) - self.state = 1137 - self.match(HogQLParser.LBRACE) self.state = 1138 - self.identifier() + self.match(HogQLParser.LBRACE) self.state = 1139 + self.identifier() + self.state = 1140 self.match(HogQLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -8975,17 +8981,17 @@ def string(self): localctx = HogQLParser.StringContext(self, self._ctx, self.state) self.enterRule(localctx, 156, self.RULE_string) try: - self.state = 1143 + self.state = 1144 self._errHandler.sync(self) token = self._input.LA(1) if token in [106]: self.enterOuterAlt(localctx, 1) - self.state = 1141 + self.state = 1142 self.match(HogQLParser.STRING_LITERAL) pass elif token in [137]: self.enterOuterAlt(localctx, 2) - self.state = 1142 + self.state = 1143 self.templateString() pass else: @@ -9039,19 +9045,19 @@ def templateString(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1145 + self.state = 1146 self.match(HogQLParser.QUOTE_SINGLE_TEMPLATE) - self.state = 1149 + self.state = 1150 self._errHandler.sync(self) _la = self._input.LA(1) while _la==151 or _la==152: - self.state = 1146 + self.state = 1147 self.stringContents() - self.state = 1151 + self.state = 1152 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 1152 + self.state = 1153 self.match(HogQLParser.QUOTE_SINGLE) except RecognitionException as re: localctx.exception = re @@ -9099,21 +9105,21 @@ def stringContents(self): localctx = HogQLParser.StringContentsContext(self, self._ctx, self.state) self.enterRule(localctx, 160, self.RULE_stringContents) try: - self.state = 1159 + self.state = 1160 self._errHandler.sync(self) token = self._input.LA(1) if token in [152]: self.enterOuterAlt(localctx, 1) - self.state = 1154 - self.match(HogQLParser.STRING_ESCAPE_TRIGGER) self.state = 1155 - self.columnExpr(0) + self.match(HogQLParser.STRING_ESCAPE_TRIGGER) self.state = 1156 + self.columnExpr(0) + self.state = 1157 self.match(HogQLParser.RBRACE) pass elif token in [151]: self.enterOuterAlt(localctx, 2) - self.state = 1158 + self.state = 1159 self.match(HogQLParser.STRING_TEXT) pass else: @@ -9167,19 +9173,19 @@ def fullTemplateString(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 1161 + self.state = 1162 self.match(HogQLParser.QUOTE_SINGLE_TEMPLATE_FULL) - self.state = 1165 + self.state = 1166 self._errHandler.sync(self) _la = self._input.LA(1) while _la==153 or _la==154: - self.state = 1162 + self.state = 1163 self.stringContentsFull() - self.state = 1167 + self.state = 1168 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 1168 + self.state = 1169 self.match(HogQLParser.EOF) except RecognitionException as re: localctx.exception = re @@ -9227,21 +9233,21 @@ def stringContentsFull(self): localctx = HogQLParser.StringContentsFullContext(self, self._ctx, self.state) self.enterRule(localctx, 164, self.RULE_stringContentsFull) try: - self.state = 1175 + self.state = 1176 self._errHandler.sync(self) token = self._input.LA(1) if token in [154]: self.enterOuterAlt(localctx, 1) - self.state = 1170 - self.match(HogQLParser.FULL_STRING_ESCAPE_TRIGGER) self.state = 1171 - self.columnExpr(0) + self.match(HogQLParser.FULL_STRING_ESCAPE_TRIGGER) self.state = 1172 + self.columnExpr(0) + self.state = 1173 self.match(HogQLParser.RBRACE) pass elif token in [153]: self.enterOuterAlt(localctx, 2) - self.state = 1174 + self.state = 1175 self.match(HogQLParser.FULL_STRING_TEXT) pass else: diff --git a/posthog/hogql/grammar/HogQLParserVisitor.py b/posthog/hogql/grammar/HogQLParserVisitor.py index 1645bdb1a30ab..3e71e7f8c1d4a 100644 --- a/posthog/hogql/grammar/HogQLParserVisitor.py +++ b/posthog/hogql/grammar/HogQLParserVisitor.py @@ -29,11 +29,6 @@ def visitVarDecl(self, ctx:HogQLParser.VarDeclContext): return self.visitChildren(ctx) - # Visit a parse tree produced by HogQLParser#varAssignment. - def visitVarAssignment(self, ctx:HogQLParser.VarAssignmentContext): - return self.visitChildren(ctx) - - # Visit a parse tree produced by HogQLParser#identifierList. def visitIdentifierList(self, ctx:HogQLParser.IdentifierListContext): return self.visitChildren(ctx) @@ -44,8 +39,8 @@ def visitStatement(self, ctx:HogQLParser.StatementContext): return self.visitChildren(ctx) - # Visit a parse tree produced by HogQLParser#exprStmt. - def visitExprStmt(self, ctx:HogQLParser.ExprStmtContext): + # Visit a parse tree produced by HogQLParser#returnStmt. + def visitReturnStmt(self, ctx:HogQLParser.ReturnStmtContext): return self.visitChildren(ctx) @@ -59,13 +54,18 @@ def visitWhileStmt(self, ctx:HogQLParser.WhileStmtContext): return self.visitChildren(ctx) - # Visit a parse tree produced by HogQLParser#returnStmt. - def visitReturnStmt(self, ctx:HogQLParser.ReturnStmtContext): + # Visit a parse tree produced by HogQLParser#funcStmt. + def visitFuncStmt(self, ctx:HogQLParser.FuncStmtContext): return self.visitChildren(ctx) - # Visit a parse tree produced by HogQLParser#funcStmt. - def visitFuncStmt(self, ctx:HogQLParser.FuncStmtContext): + # Visit a parse tree produced by HogQLParser#varAssignment. + def visitVarAssignment(self, ctx:HogQLParser.VarAssignmentContext): + return self.visitChildren(ctx) + + + # Visit a parse tree produced by HogQLParser#exprStmt. + def visitExprStmt(self, ctx:HogQLParser.ExprStmtContext): return self.visitChildren(ctx) diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index bf08f5c122635..09252af4fabca 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -185,7 +185,12 @@ def visit(self, ctx: ParserRuleContext): raise e def visitProgram(self, ctx: HogQLParser.ProgramContext): - return ast.Program(declarations=[self.visit(declaration) for declaration in ctx.declaration()]) + declarations: list[ast.Declaration] = [] + for declaration in ctx.declaration(): + if not declaration.statement() or not declaration.statement().emptyStmt(): + statement = self.visit(declaration) + declarations.append(cast(ast.Declaration, statement)) + return ast.Program(declarations=declarations) def visitDeclaration(self, ctx: HogQLParser.DeclarationContext): return self.visitChildren(ctx) @@ -245,10 +250,15 @@ def visitIdentifierList(self, ctx: HogQLParser.IdentifierListContext): return [ident.getText() for ident in ctx.identifier()] def visitEmptyStmt(self, ctx: HogQLParser.EmptyStmtContext): - return ast.ExprStatement(expr=ast.Constant(value=True)) + return ast.ExprStatement(expr=None) def visitBlock(self, ctx: HogQLParser.BlockContext): - return ast.Block(declarations=[self.visit(declaration) for declaration in ctx.declaration()]) + declarations: list[ast.Declaration] = [] + for declaration in ctx.declaration(): + if not declaration.statement() or not declaration.statement().emptyStmt(): + statement = self.visit(declaration) + declarations.append(cast(ast.Declaration, statement)) + return ast.Block(declarations=declarations) def visitSelect(self, ctx: HogQLParser.SelectContext): return self.visit(ctx.selectUnionStmt() or ctx.selectStmt() or ctx.hogqlxTagElement()) @@ -573,9 +583,7 @@ def visitColumnExprTernaryOp(self, ctx: HogQLParser.ColumnExprTernaryOpContext): def visitColumnExprAlias(self, ctx: HogQLParser.ColumnExprAliasContext): alias: str - if ctx.alias(): - alias = self.visit(ctx.alias()) - elif ctx.identifier(): + if ctx.identifier(): alias = self.visit(ctx.identifier()) elif ctx.STRING_LITERAL(): alias = parse_string_literal_ctx(ctx.STRING_LITERAL()) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 3104d121112de..b44191b41095e 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -173,7 +173,9 @@ def __init__( def indent(self, extra: int = 0): return " " * self.tab_size * (self._indent + extra) - def visit(self, node: AST): + def visit(self, node: AST | None): + if node is None: + return "" self.stack.append(node) self._indent += 1 response = super().visit(node) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 38442bc26eef2..cee6802a4498a 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -110,7 +110,7 @@ def __init__( self.database = context.database self.cte_counter = 0 - def visit(self, node: ast.Expr) -> ast.Expr: + def visit(self, node: ast.Expr | None) -> ast.Expr: if isinstance(node, ast.Expr) and node.type is not None: raise ResolutionError( f"Type already resolved for {type(node).__name__} ({type(node.type).__name__}). Can't run again." diff --git a/posthog/hogql/test/_test_parser.py b/posthog/hogql/test/_test_parser.py index d1bff88ebfcff..708ff0822fc3b 100644 --- a/posthog/hogql/test/_test_parser.py +++ b/posthog/hogql/test/_test_parser.py @@ -957,7 +957,7 @@ def test_select_from_cross_join(self): def test_select_array_join(self): self.assertEqual( - self._select("select a from events ARRAY JOIN [1,2,3] a"), + self._select("select a from events ARRAY JOIN [1,2,3] as a"), ast.SelectQuery( select=[ast.Field(chain=["a"])], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), @@ -977,7 +977,7 @@ def test_select_array_join(self): ), ) self.assertEqual( - self._select("select a from events INNER ARRAY JOIN [1,2,3] a"), + self._select("select a from events INNER ARRAY JOIN [1,2,3] as a"), ast.SelectQuery( select=[ast.Field(chain=["a"])], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), @@ -997,7 +997,7 @@ def test_select_array_join(self): ), ) self.assertEqual( - self._select("select 1, b from events LEFT ARRAY JOIN [1,2,3] a, [4,5,6] AS b"), + self._select("select 1, b from events LEFT ARRAY JOIN [1,2,3] as a, [4,5,6] AS b"), ast.SelectQuery( select=[ast.Constant(value=1), ast.Field(chain=["b"])], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 5bf05573e671f..5cd30291e1f0f 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -243,7 +243,7 @@ def test_hogql_properties(self): ) self._assert_expr_error( "properties.'no strings'", - "no viable alternative at input '.'no strings'", + "mismatched input", "hogql", ) @@ -393,10 +393,10 @@ def test_expr_syntax_errors(self): self._assert_expr_error("(", "no viable alternative at input '('") self._assert_expr_error("())", "no viable alternative at input '()'") self._assert_expr_error("(3 57", "no viable alternative at input '(3 57'") - self._assert_expr_error("select query from events", "mismatched input 'from' expecting ") - self._assert_expr_error("this makes little sense", "Unable to resolve field: this") + self._assert_expr_error("select query from events", "mismatched input 'query' expecting ") + self._assert_expr_error("this makes little sense", "mismatched input 'makes' expecting ") self._assert_expr_error("1;2", "mismatched input ';' expecting ") - self._assert_expr_error("b.a(bla)", "mismatched input '(' expecting '.'") + self._assert_expr_error("b.a(bla)", "mismatched input '(' expecting ") def test_logic(self): self.assertEqual( @@ -504,11 +504,6 @@ def test_alias_keywords(self): self._select("select 1 as `-- select team_id` from events"), f"SELECT 1 AS `-- select team_id` FROM events WHERE equals(events.team_id, {self.team.pk}) LIMIT {MAX_SELECT_RETURNED_ROWS}", ) - # Some aliases are funny, but that's what the antlr syntax permits, and ClickHouse doesn't complain either - self.assertEqual( - self._expr("event makes little sense"), - "((events.event AS makes) AS little) AS sense", - ) def test_case_when(self): self.assertEqual(self._expr("case when 1 then 2 else 3 end"), "if(1, 2, 3)") diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 53304738ff524..278471152a9f5 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -624,7 +624,7 @@ def test_tuple_access(self): self._create_random_events() # sample pivot table, testing tuple access query = """ - select col_a, arrayZip( (sumMap( g.1, g.2 ) as x).1, x.2) r from ( + select col_a, arrayZip( (sumMap( g.1, g.2 ) as x).1, x.2) as r from ( select col_a, groupArray( (col_b, col_c) ) as g from ( SELECT properties.index as col_a, @@ -894,7 +894,7 @@ def test_with_pivot_table_1_level(self): group by col_a ), PIVOT_FUNCTION_2 AS ( - select col_a, arrayZip( (sumMap( g.1, g.2 ) as x).1, x.2) r from + select col_a, arrayZip( (sumMap( g.1, g.2 ) as x).1, x.2) as r from PIVOT_FUNCTION_1 group by col_a ) @@ -933,7 +933,7 @@ def test_with_pivot_table_2_levels(self): group by col_a ), PIVOT_FUNCTION_2 AS ( - select col_a, arrayZip( (sumMap( g.1, g.2 ) as x).1, x.2) r from + select col_a, arrayZip( (sumMap( g.1, g.2 ) as x).1, x.2) as r from PIVOT_FUNCTION_1 group by col_a ), diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index f4d8c6f308c20..d03e691b640ec 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -18,9 +18,9 @@ def clear_locations(expr: Expr) -> Expr: class Visitor(Generic[T]): - def visit(self, node: AST) -> T: + def visit(self, node: AST | None) -> T: if node is None: - return node + return node # type: ignore try: return node.accept(self) @@ -509,7 +509,7 @@ def visit_select_query(self, node: ast.SelectQuery): type=None if self.clear_types else node.type, ctes={key: self.visit(expr) for key, expr in node.ctes.items()} if node.ctes else None, # to not traverse select_from=self.visit(node.select_from), # keep "select_from" before "select" to resolve tables first - select=[self.visit(expr) for expr in node.select] if node.select else None, + select=[self.visit(expr) for expr in node.select] if node.select else [], array_join_op=node.array_join_op, array_join_list=[self.visit(expr) for expr in node.array_join_list] if node.array_join_list else None, where=self.visit(node.where), diff --git a/posthog/hogql_queries/insights/funnels/base.py b/posthog/hogql_queries/insights/funnels/base.py index e673f59c467ef..591c8fe95af32 100644 --- a/posthog/hogql_queries/insights/funnels/base.py +++ b/posthog/hogql_queries/insights/funnels/base.py @@ -645,7 +645,7 @@ def _get_count_columns(self, max_steps: int) -> list[ast.Expr]: exprs: list[ast.Expr] = [] for i in range(max_steps): - exprs.append(parse_expr(f"countIf(steps = {i + 1}) step_{i + 1}")) + exprs.append(parse_expr(f"countIf(steps = {i + 1}) as step_{i + 1}")) return exprs @@ -686,7 +686,7 @@ def _get_matching_events(self, max_steps: int) -> list[ast.Expr]: for i in range(0, max_steps): event_fields = ["latest", *self.extra_event_fields_and_properties] event_fields_with_step = ", ".join([f"{field}_{i}" for field in event_fields]) - event_clause = f"({event_fields_with_step}) as step_{i}_matching_event" + event_clause = f"({event_fields_with_step}) AS step_{i}_matching_event" events.append(parse_expr(event_clause)) return [*events, *self._get_final_matching_event(max_steps)] @@ -700,8 +700,8 @@ def _get_matching_event_arrays(self, max_steps: int) -> list[ast.Expr]: and self.context.actorsQuery.includeRecordings ): for i in range(0, max_steps): - exprs.append(parse_expr(f"groupArray(10)(step_{i}_matching_event) as step_{i}_matching_events")) - exprs.append(parse_expr(f"groupArray(10)(final_matching_event) as final_matching_events")) + exprs.append(parse_expr(f"groupArray(10)(step_{i}_matching_event) AS step_{i}_matching_events")) + exprs.append(parse_expr(f"groupArray(10)(final_matching_event) AS final_matching_events")) return exprs def _get_step_time_avgs(self, max_steps: int, inner_query: bool = False) -> list[ast.Expr]: @@ -709,9 +709,9 @@ def _get_step_time_avgs(self, max_steps: int, inner_query: bool = False) -> list for i in range(1, max_steps): exprs.append( - parse_expr(f"avg(step_{i}_conversion_time) step_{i}_average_conversion_time_inner") + parse_expr(f"avg(step_{i}_conversion_time) as step_{i}_average_conversion_time_inner") if inner_query - else parse_expr(f"avg(step_{i}_average_conversion_time_inner) step_{i}_average_conversion_time") + else parse_expr(f"avg(step_{i}_average_conversion_time_inner) as step_{i}_average_conversion_time") ) return exprs @@ -721,9 +721,9 @@ def _get_step_time_median(self, max_steps: int, inner_query: bool = False) -> li for i in range(1, max_steps): exprs.append( - parse_expr(f"median(step_{i}_conversion_time) step_{i}_median_conversion_time_inner") + parse_expr(f"median(step_{i}_conversion_time) as step_{i}_median_conversion_time_inner") if inner_query - else parse_expr(f"median(step_{i}_median_conversion_time_inner) step_{i}_median_conversion_time") + else parse_expr(f"median(step_{i}_median_conversion_time_inner) as step_{i}_median_conversion_time") ) return exprs @@ -732,7 +732,7 @@ def _get_step_time_array(self, max_steps: int) -> list[ast.Expr]: exprs: list[ast.Expr] = [] for i in range(1, max_steps): - exprs.append(parse_expr(f"groupArray(step_{i}_conversion_time) step_{i}_conversion_time_array")) + exprs.append(parse_expr(f"groupArray(step_{i}_conversion_time) as step_{i}_conversion_time_array")) return exprs @@ -742,7 +742,7 @@ def _get_step_time_array_avgs(self, max_steps: int) -> list[ast.Expr]: for i in range(1, max_steps): exprs.append( parse_expr( - f"if(isNaN(avgArray(step_{i}_conversion_time_array) as inter_{i}_conversion), NULL, inter_{i}_conversion) step_{i}_average_conversion_time" + f"if(isNaN(avgArray(step_{i}_conversion_time_array) as inter_{i}_conversion), NULL, inter_{i}_conversion) as step_{i}_average_conversion_time" ) ) @@ -754,7 +754,7 @@ def _get_step_time_array_median(self, max_steps: int) -> list[ast.Expr]: for i in range(1, max_steps): exprs.append( parse_expr( - f"if(isNaN(medianArray(step_{i}_conversion_time_array) as inter_{i}_median), NULL, inter_{i}_median) step_{i}_median_conversion_time" + f"if(isNaN(medianArray(step_{i}_conversion_time_array) as inter_{i}_median), NULL, inter_{i}_median) as step_{i}_median_conversion_time" ) ) @@ -796,8 +796,8 @@ def _get_timestamp_selects(self) -> tuple[list[ast.Expr], list[ast.Expr]]: return ( [ast.Field(chain=[f"latest_{target_step}"]), ast.Field(chain=[f"latest_{target_step - 1}"])], [ - parse_expr(f"argMax(latest_{target_step}, steps) as max_timestamp"), - parse_expr(f"argMax(latest_{target_step - 1}, steps) as min_timestamp"), + parse_expr(f"argMax(latest_{target_step}, steps) AS max_timestamp"), + parse_expr(f"argMax(latest_{target_step - 1}, steps) AS min_timestamp"), ], ) elif self.context.includeTimestamp: @@ -808,9 +808,9 @@ def _get_timestamp_selects(self) -> tuple[list[ast.Expr], list[ast.Expr]]: ast.Field(chain=[f"latest_{first_step}"]), ], [ - parse_expr(f"argMax(latest_{target_step}, steps) as timestamp"), - parse_expr(f"argMax(latest_{final_step}, steps) as final_timestamp"), - parse_expr(f"argMax(latest_{first_step}, steps) as first_timestamp"), + parse_expr(f"argMax(latest_{target_step}, steps) AS timestamp"), + parse_expr(f"argMax(latest_{final_step}, steps) AS final_timestamp"), + parse_expr(f"argMax(latest_{first_step}, steps) AS first_timestamp"), ], ) else: @@ -825,7 +825,7 @@ def _get_step_times(self, max_steps: int) -> list[ast.Expr]: for i in range(1, max_steps): exprs.append( parse_expr( - f"if(isNotNull(latest_{i}) AND latest_{i} <= toTimeZone(latest_{i-1}, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', latest_{i - 1}, latest_{i}), NULL) step_{i}_conversion_time" + f"if(isNotNull(latest_{i}) AND latest_{i} <= toTimeZone(latest_{i-1}, 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', latest_{i - 1}, latest_{i}), NULL) as step_{i}_conversion_time" ), ) @@ -859,14 +859,14 @@ def _get_partition_cols(self, level_index: int, max_steps: int) -> list[ast.Expr exprs.append( parse_expr( - f"min(latest_{i}) over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND {duplicate_event} PRECEDING) latest_{i}" + f"min(latest_{i}) over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND {duplicate_event} PRECEDING) as latest_{i}" ) ) for field in self.extra_event_fields_and_properties: exprs.append( parse_expr( - f'last_value("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND {duplicate_event} PRECEDING) "{field}_{i}"' + f'last_value("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND {duplicate_event} PRECEDING) as "{field}_{i}"' ) ) @@ -875,7 +875,7 @@ def _get_partition_cols(self, level_index: int, max_steps: int) -> list[ast.Expr if cast(int, exclusion.funnelFromStep) + 1 == i: exprs.append( parse_expr( - f"min(exclusion_{exclusion_id}_latest_{exclusion.funnelFromStep}) over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) exclusion_{exclusion_id}_latest_{exclusion.funnelFromStep}" + f"min(exclusion_{exclusion_id}_latest_{exclusion.funnelFromStep}) over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN UNBOUNDED PRECEDING AND 0 PRECEDING) as exclusion_{exclusion_id}_latest_{exclusion.funnelFromStep}" ) ) diff --git a/posthog/hogql_queries/insights/funnels/funnel_strict.py b/posthog/hogql_queries/insights/funnels/funnel_strict.py index 4bf9b5ce19ea1..dd7ecad30294d 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_strict.py +++ b/posthog/hogql_queries/insights/funnels/funnel_strict.py @@ -81,14 +81,14 @@ def _get_partition_cols(self, level_index: int, max_steps: int): else: exprs.append( parse_expr( - f"min(latest_{i}) over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN {i} PRECEDING AND {i} PRECEDING) latest_{i}" + f"min(latest_{i}) over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN {i} PRECEDING AND {i} PRECEDING) as latest_{i}" ) ) for field in self.extra_event_fields_and_properties: exprs.append( parse_expr( - f'min("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN {i} PRECEDING AND {i} PRECEDING) "{field}_{i}"' + f'min("{field}_{i}") over (PARTITION by aggregation_target {self._get_breakdown_prop()} ORDER BY timestamp DESC ROWS BETWEEN {i} PRECEDING AND {i} PRECEDING) as "{field}_{i}"' ) ) diff --git a/posthog/hogql_queries/insights/funnels/funnel_unordered.py b/posthog/hogql_queries/insights/funnels/funnel_unordered.py index 2bc3be1f8ca81..f718c7fe3e4df 100644 --- a/posthog/hogql_queries/insights/funnels/funnel_unordered.py +++ b/posthog/hogql_queries/insights/funnels/funnel_unordered.py @@ -132,7 +132,7 @@ def _get_step_times(self, max_steps: int) -> list[ast.Expr]: for i in range(1, max_steps): exprs.append( parse_expr( - f"if(isNotNull(conversion_times[{i+1}]) AND conversion_times[{i+1}] <= toTimeZone(conversion_times[{i}], 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', conversion_times[{i}], conversion_times[{i+1}]), NULL) step_{i}_conversion_time" + f"if(isNotNull(conversion_times[{i+1}]) AND conversion_times[{i+1}] <= toTimeZone(conversion_times[{i}], 'UTC') + INTERVAL {windowInterval} {windowIntervalUnit}, dateDiff('second', conversion_times[{i}], conversion_times[{i+1}]), NULL) as step_{i}_conversion_time" ) ) # array indices in ClickHouse are 1-based :shrug: diff --git a/posthog/migrations/0425_hogfunction.py b/posthog/migrations/0425_hogfunction.py new file mode 100644 index 0000000000000..a04b78d6d4ab9 --- /dev/null +++ b/posthog/migrations/0425_hogfunction.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.11 on 2024-06-10 08:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import posthog.models.utils + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0424_survey_current_iteration_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="HogFunction", + fields=[ + ( + "id", + models.UUIDField( + default=posthog.models.utils.UUIDT, editable=False, primary_key=True, serialize=False + ), + ), + ("name", models.CharField(blank=True, max_length=400, null=True)), + ("description", models.TextField(blank=True, default="")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("deleted", models.BooleanField(default=False)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("enabled", models.BooleanField(default=False)), + ("hog", models.TextField()), + ("bytecode", models.JSONField(blank=True, null=True)), + ("inputs_schema", models.JSONField(null=True)), + ("inputs", models.JSONField(null=True)), + ("filters", models.JSONField(blank=True, null=True)), + ( + "created_by", + models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL + ), + ), + ("team", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="posthog.team")), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/posthog/migrations/0426_externaldatasource_sync_frequency.py b/posthog/migrations/0426_externaldatasource_sync_frequency.py new file mode 100644 index 0000000000000..6bb13966e591b --- /dev/null +++ b/posthog/migrations/0426_externaldatasource_sync_frequency.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.11 on 2024-06-06 15:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("posthog", "0425_hogfunction"), + ] + + operations = [ + migrations.AddField( + model_name="externaldatasource", + name="sync_frequency", + field=models.CharField( + blank=True, + choices=[("day", "Daily"), ("week", "Weekly"), ("month", "Monthly")], + default="day", + max_length=128, + ), + ), + ] diff --git a/posthog/models/hog_functions/__init__.py b/posthog/models/hog_functions/__init__.py new file mode 100644 index 0000000000000..c2af1396e4079 --- /dev/null +++ b/posthog/models/hog_functions/__init__.py @@ -0,0 +1 @@ +from .hog_function import * diff --git a/posthog/models/hog_functions/hog_function.py b/posthog/models/hog_functions/hog_function.py new file mode 100644 index 0000000000000..8355832e0e2af --- /dev/null +++ b/posthog/models/hog_functions/hog_function.py @@ -0,0 +1,118 @@ +import json +from typing import Optional + +from django.db import models +from django.db.models.signals import post_save +from django.dispatch.dispatcher import receiver + +from posthog.models.action.action import Action +from posthog.models.team.team import Team +from posthog.models.utils import UUIDModel +from posthog.redis import get_client + + +class HogFunction(UUIDModel): + team: models.ForeignKey = models.ForeignKey("Team", on_delete=models.CASCADE) + name: models.CharField = models.CharField(max_length=400, null=True, blank=True) + description: models.TextField = models.TextField(blank=True, default="") + created_at: models.DateTimeField = models.DateTimeField(auto_now_add=True, blank=True) + created_by: models.ForeignKey = models.ForeignKey("User", on_delete=models.SET_NULL, null=True, blank=True) + deleted: models.BooleanField = models.BooleanField(default=False) + updated_at: models.DateTimeField = models.DateTimeField(auto_now=True) + enabled: models.BooleanField = models.BooleanField(default=False) + + hog: models.TextField = models.TextField() + bytecode: models.JSONField = models.JSONField(null=True, blank=True) + inputs_schema: models.JSONField = models.JSONField(null=True) + inputs: models.JSONField = models.JSONField(null=True) + filters: models.JSONField = models.JSONField(null=True, blank=True) + + @property + def filter_action_ids(self) -> list[int]: + if not self.filters: + return [] + try: + return [int(action["id"]) for action in self.filters.get("actions", [])] + except KeyError: + return [] + + def compile_filters_bytecode(self, actions: Optional[dict[int, Action]] = None): + from .utils import hog_function_filters_to_expr + from posthog.hogql.bytecode import create_bytecode + + self.filters = self.filters or {} + + if actions is None: + # If not provided as an optimization we fetch all actions + actions_list = ( + Action.objects.select_related("team").filter(team_id=self.team_id).filter(id__in=self.filter_action_ids) + ) + actions = {action.id: action for action in actions_list} + + try: + self.filters["bytecode"] = create_bytecode(hog_function_filters_to_expr(self.filters, self.team, actions)) + except Exception as e: + # TODO: Better reporting of this issue + self.filters["bytecode"] = None + self.filters["bytecode_error"] = str(e) + + def save(self, *args, **kwargs): + self.compile_filters_bytecode() + return super().save(*args, **kwargs) + + def __str__(self): + return self.name + + +@receiver(post_save, sender=HogFunction) +def hog_function_saved(sender, instance: HogFunction, created, **kwargs): + get_client().publish( + "reload-hog-function", + json.dumps({"teamId": instance.team_id, "hogFunctionId": str(instance.id)}), + ) + + +@receiver(post_save, sender=Action) +def action_saved(sender, instance: Action, created, **kwargs): + # Whenever an action is saved we want to load all hog functions using it + # and trigger a refresh of the filters bytecode + + affected_hog_functions = ( + HogFunction.objects.select_related("team") + .filter(team_id=instance.team_id) + .filter(filters__contains={"actions": [{"id": str(instance.id)}]}) + ) + + refresh_hog_functions(team_id=instance.team_id, affected_hog_functions=list(affected_hog_functions)) + + +@receiver(post_save, sender=Team) +def team_saved(sender, instance: Team, created, **kwargs): + affected_hog_functions = ( + HogFunction.objects.select_related("team") + .filter(team_id=instance.id) + .filter(filters__contains={"filter_test_accounts": True}) + ) + + refresh_hog_functions(team_id=instance.id, affected_hog_functions=list(affected_hog_functions)) + + +def refresh_hog_functions(team_id: int, affected_hog_functions: list[HogFunction]) -> int: + all_related_actions = ( + Action.objects.select_related("team") + .filter(team_id=team_id) + .filter( + id__in=[ + action_id for hog_function in affected_hog_functions for action_id in hog_function.filter_action_ids + ] + ) + ) + + actions_by_id = {action.id: action for action in all_related_actions} + + for hog_function in affected_hog_functions: + hog_function.compile_filters_bytecode(actions=actions_by_id) + + updates = HogFunction.objects.bulk_update(affected_hog_functions, ["filters"]) + + return updates diff --git a/posthog/models/hog_functions/utils.py b/posthog/models/hog_functions/utils.py new file mode 100644 index 0000000000000..5ec265487d2e3 --- /dev/null +++ b/posthog/models/hog_functions/utils.py @@ -0,0 +1,66 @@ +from typing import Any +from posthog.models.action.action import Action +from posthog.hogql.bytecode import create_bytecode +from posthog.hogql.parser import parse_expr, parse_string_template +from posthog.hogql.property import action_to_expr, property_to_expr, ast +from posthog.models.team.team import Team + + +def hog_function_filters_to_expr(filters: dict, team: Team, actions: dict[int, Action]) -> ast.Expr: + test_account_filters_exprs: list[ast.Expr] = [] + if filters.get("filter_test_accounts", False): + test_account_filters_exprs = [property_to_expr(property, team) for property in team.test_account_filters] + + all_filters = filters.get("events", []) + filters.get("actions", []) + all_filters_exprs: list[ast.Expr] = [] + + if not all_filters and test_account_filters_exprs: + # Always return test filters if set and no other filters + return ast.And(exprs=test_account_filters_exprs) + + for filter in all_filters: + exprs: list[ast.Expr] = [] + exprs.extend(test_account_filters_exprs) + + # Events + if filter.get("type") == "events" and filter.get("name"): + exprs.append(parse_expr("event = {event}", {"event": ast.Constant(value=filter["name"])})) + + # Actions + if filter.get("type") == "actions": + try: + action = actions[int(filter["id"])] + exprs.append(action_to_expr(action)) + except KeyError: + # If an action doesn't exist, we want to return no events + exprs.append(parse_expr("1 = 2")) + + # Properties + if filter.get("properties"): + exprs.append(property_to_expr(filter.get("properties"), team)) + + if len(exprs) == 0: + all_filters_exprs.append(ast.Constant(value=True)) + + all_filters_exprs.append(ast.And(exprs=exprs)) + + if all_filters_exprs: + final_expr = ast.Or(exprs=all_filters_exprs) + return final_expr + else: + return ast.Constant(value=True) + + +def generate_template_bytecode(obj: Any) -> Any: + """ + Clones an object, compiling any string values to bytecode templates + """ + + if isinstance(obj, dict): + return {key: generate_template_bytecode(value) for key, value in obj.items()} + elif isinstance(obj, list): + return [generate_template_bytecode(item) for item in obj] + elif isinstance(obj, str): + return create_bytecode(parse_string_template(obj)) + else: + return obj diff --git a/posthog/models/person/person.py b/posthog/models/person/person.py index 20f9dd7675487..72a5bd7c79948 100644 --- a/posthog/models/person/person.py +++ b/posthog/models/person/person.py @@ -6,6 +6,7 @@ from posthog.models.utils import UUIDT from ..team import Team +from .missing_person import uuidFromDistinctId MAX_LIMIT_DISTINCT_IDS = 2500 @@ -51,7 +52,9 @@ def _add_distinct_ids(self, distinct_ids: list[str]) -> None: self.add_distinct_id(distinct_id) def split_person(self, main_distinct_id: Optional[str], max_splits: Optional[int] = None): - distinct_ids = Person.objects.get(pk=self.pk).distinct_ids + original_person = Person.objects.get(pk=self.pk) + distinct_ids = original_person.distinct_ids + original_person_version = original_person.version or 0 if not main_distinct_id: self.properties = {} self.save() @@ -65,7 +68,13 @@ def split_person(self, main_distinct_id: Optional[str], max_splits: Optional[int if not distinct_id == main_distinct_id: with transaction.atomic(): pdi = PersonDistinctId.objects.select_for_update().get(person=self, distinct_id=distinct_id) - person = Person.objects.create(team_id=self.team_id) + person, _ = Person.objects.get_or_create( + uuid=uuidFromDistinctId(self.team_id, distinct_id), + team_id=self.team_id, + defaults={ + "version": original_person_version + 1, + }, + ) pdi.person_id = str(person.id) pdi.version = (pdi.version or 0) + 1 pdi.save(update_fields=["version", "person_id"]) @@ -83,9 +92,7 @@ def split_person(self, main_distinct_id: Optional[str], max_splits: Optional[int version=pdi.version, ) create_person( - team_id=self.team_id, - uuid=str(person.uuid), - version=person.version or 0, + team_id=self.team_id, uuid=str(person.uuid), version=person.version, created_at=person.created_at ) objects = PersonManager() diff --git a/posthog/models/test/test_hog_function.py b/posthog/models/test/test_hog_function.py new file mode 100644 index 0000000000000..35779b7efbad2 --- /dev/null +++ b/posthog/models/test/test_hog_function.py @@ -0,0 +1,283 @@ +import json +from django.test import TestCase +from inline_snapshot import snapshot + +from posthog.models.action.action import Action +from posthog.models.hog_functions.hog_function import HogFunction +from posthog.models.user import User +from posthog.test.base import QueryMatchingTest + + +to_dict = lambda x: json.loads(json.dumps(x)) + + +class TestHogFunction(TestCase): + def setUp(self): + super().setUp() + org, team, user = User.objects.bootstrap("Test org", "ben@posthog.com", None) + self.team = team + self.user = user + self.org = org + + def test_hog_function_basic(self): + item = HogFunction.objects.create(name="Test", team=self.team) + assert item.name == "Test" + assert item.hog == "" + assert not item.enabled + + def test_hog_function_team_no_filters_compilation(self): + item = HogFunction.objects.create(name="Test", team=self.team) + + # Some json serialization is needed to compare the bytecode more easily in tests + json_filters = to_dict(item.filters) + assert json_filters["bytecode"] == ["_h", 29] # TRUE + + def test_hog_function_filters_compilation(self): + item = HogFunction.objects.create( + name="Test", + team=self.team, + filters={ + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + "actions": [{"id": "9", "name": "Test Action", "type": "actions", "order": 1}], + "filter_test_accounts": True, + }, + ) + + # Some json serialization is needed to compare the bytecode more easily in tests + json_filters = to_dict(item.filters) + + assert json_filters == { + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + "actions": [{"id": "9", "name": "Test Action", "type": "actions", "order": 1}], + "filter_test_accounts": True, + "bytecode": [ + "_h", + 33, + 2, + 33, + 1, + 11, + 29, + 32, + "^(localhost|127\\.0\\.0\\.1)($|:)", + 32, + "$host", + 32, + "properties", + 1, + 2, + 2, + "toString", + 1, + 2, + "match", + 2, + 5, + 2, + "ifNull", + 2, + 3, + 2, + 32, + "$pageview", + 32, + "event", + 1, + 1, + 11, + 29, + 32, + "^(localhost|127\\.0\\.0\\.1)($|:)", + 32, + "$host", + 32, + "properties", + 1, + 2, + 2, + "toString", + 1, + 2, + "match", + 2, + 5, + 2, + "ifNull", + 2, + 3, + 2, + 4, + 2, + ], + } + + def test_hog_function_team_filters_only_compilation(self): + item = HogFunction.objects.create( + name="Test", + team=self.team, + filters={ + "filter_test_accounts": True, + }, + ) + + # Some json serialization is needed to compare the bytecode more easily in tests + json_filters = to_dict(item.filters) + + assert json.dumps(json_filters["bytecode"]) == snapshot( + '["_h", 29, 32, "^(localhost|127\\\\.0\\\\.0\\\\.1)($|:)", 32, "$host", 32, "properties", 1, 2, 2, "toString", 1, 2, "match", 2, 5, 2, "ifNull", 2, 3, 1]' + ) + + +class TestHogFunctionsBackgroundReloading(TestCase, QueryMatchingTest): + def setUp(self): + super().setUp() + org, team, user = User.objects.bootstrap("Test org", "ben@posthog.com", None) + self.team = team + self.user = user + self.org = org + + self.action = Action.objects.create( + team=self.team, + name="Test Action", + steps_json=[ + { + "event": "test-event", + "properties": [ + { + "key": "prop-1", + "operator": "exact", + "value": "old-value-1", + "type": "event", + } + ], + } + ], + ) + + self.action2 = Action.objects.create( + team=self.team, + name="Test Action", + steps_json=[ + { + "event": None, + "properties": [ + { + "key": "prop-2", + "operator": "exact", + "value": "old-value-2", + "type": "event", + } + ], + } + ], + ) + + def test_hog_functions_reload_on_action_saved(self): + hog_function_1 = HogFunction.objects.create( + name="func 1", + team=self.team, + filters={ + "actions": [ + {"id": str(self.action.id), "name": "Test Action", "type": "actions", "order": 1}, + {"id": str(self.action2.id), "name": "Test Action 2", "type": "actions", "order": 2}, + ], + }, + ) + hog_function_2 = HogFunction.objects.create( + name="func 2", + team=self.team, + filters={ + "actions": [ + {"id": str(self.action.id), "name": "Test Action", "type": "actions", "order": 1}, + ], + }, + ) + + # Check that the bytecode is correct + assert json.dumps(hog_function_1.filters["bytecode"]) == snapshot( + '["_h", 32, "old-value-2", 32, "prop-2", 32, "properties", 1, 2, 11, 3, 1, 32, "old-value-1", 32, "prop-1", 32, "properties", 1, 2, 11, 32, "test-event", 32, "event", 1, 1, 11, 3, 2, 3, 1, 4, 2]' + ) + + assert json.dumps(hog_function_2.filters["bytecode"]) == snapshot( + '["_h", 32, "old-value-1", 32, "prop-1", 32, "properties", 1, 2, 11, 32, "test-event", 32, "event", 1, 1, 11, 3, 2, 3, 1, 4, 1]' + ) + + # Modify the action and check that the bytecode is updated + self.action.steps_json = [ + { + "event": "test-event", + "properties": [ + { + "key": "prop-1", + "operator": "exact", + "value": "change-value", + "type": "event", + } + ], + } + ] + # 1 update action, 1 load hog functions, 1 load all related actions, 1 bulk update hog functions + with self.assertNumQueries(4): + self.action.save() + hog_function_1.refresh_from_db() + hog_function_2.refresh_from_db() + + assert json.dumps(hog_function_1.filters["bytecode"]) == snapshot( + '["_h", 32, "old-value-2", 32, "prop-2", 32, "properties", 1, 2, 11, 3, 1, 32, "change-value", 32, "prop-1", 32, "properties", 1, 2, 11, 32, "test-event", 32, "event", 1, 1, 11, 3, 2, 3, 1, 4, 2]' + ) + assert json.dumps(hog_function_2.filters["bytecode"]) == snapshot( + '["_h", 32, "change-value", 32, "prop-1", 32, "properties", 1, 2, 11, 32, "test-event", 32, "event", 1, 1, 11, 3, 2, 3, 1, 4, 1]' + ) + + def test_hog_functions_reload_on_team_saved(self): + self.team.test_account_filters = [] + self.team.save() + hog_function_1 = HogFunction.objects.create( + name="func 1", + team=self.team, + filters={ + "filter_test_accounts": True, + }, + ) + hog_function_2 = HogFunction.objects.create( + name="func 2", + team=self.team, + filters={ + "filter_test_accounts": True, + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + }, + ) + hog_function_3 = HogFunction.objects.create( + name="func 3", + team=self.team, + filters={ + "filter_test_accounts": False, + }, + ) + + # Check that the bytecode is correct + assert json.dumps(hog_function_1.filters["bytecode"]) == snapshot('["_h", 29]') + assert json.dumps(hog_function_2.filters["bytecode"]) == snapshot( + '["_h", 32, "$pageview", 32, "event", 1, 1, 11, 3, 1, 4, 1]' + ) + assert json.dumps(hog_function_3.filters["bytecode"]) == snapshot('["_h", 29]') + + # Modify the action and check that the bytecode is updated + self.team.test_account_filters = [ + {"key": "$host", "operator": "regex", "value": "^(localhost|127\\.0\\.0\\.1)($|:)"}, + {"key": "$pageview", "operator": "regex", "value": "test"}, + ] + # 1 update team, 1 load hog functions, 1 update hog functions + with self.assertNumQueries(3): + self.team.save() + hog_function_1.refresh_from_db() + hog_function_2.refresh_from_db() + hog_function_3.refresh_from_db() + + assert json.dumps(hog_function_1.filters["bytecode"]) == snapshot( + '["_h", 30, 32, "test", 32, "$pageview", 32, "properties", 1, 2, 2, "toString", 1, 2, "match", 2, 2, "ifNull", 2, 30, 32, "^(localhost|127\\\\.0\\\\.0\\\\.1)($|:)", 32, "$host", 32, "properties", 1, 2, 2, "toString", 1, 2, "match", 2, 2, "ifNull", 2, 3, 2]' + ) + assert json.dumps(hog_function_2.filters["bytecode"]) == snapshot( + '["_h", 32, "$pageview", 32, "event", 1, 1, 11, 30, 32, "test", 32, "$pageview", 32, "properties", 1, 2, 2, "toString", 1, 2, "match", 2, 2, "ifNull", 2, 30, 32, "^(localhost|127\\\\.0\\\\.0\\\\.1)($|:)", 32, "$host", 32, "properties", 1, 2, 2, "toString", 1, 2, "match", 2, 2, "ifNull", 2, 3, 3, 4, 1]' + ) + assert json.dumps(hog_function_3.filters["bytecode"]) == snapshot('["_h", 29]') diff --git a/posthog/queries/breakdown_props.py b/posthog/queries/breakdown_props.py index 23f4b0d51ddc4..010d61aa8d16c 100644 --- a/posthog/queries/breakdown_props.py +++ b/posthog/queries/breakdown_props.py @@ -36,7 +36,7 @@ HISTOGRAM_ELEMENTS_ARRAY_OF_KEY_SQL, TOP_ELEMENTS_ARRAY_OF_KEY_SQL, ) -from posthog.queries.util import PersonPropertiesMode +from posthog.queries.util import PersonPropertiesMode, alias_poe_mode_for_legacy ALL_USERS_COHORT_ID = 0 @@ -86,7 +86,9 @@ def get_breakdown_prop_values( sessions_join_params: dict = {} null_person_filter = ( - f"AND notEmpty(e.person_id)" if team.person_on_events_mode != PersonsOnEventsMode.DISABLED else "" + f"AND notEmpty(e.person_id)" + if alias_poe_mode_for_legacy(team.person_on_events_mode) != PersonsOnEventsMode.DISABLED + else "" ) if person_properties_mode == PersonPropertiesMode.DIRECT_ON_EVENTS: diff --git a/posthog/queries/event_query/event_query.py b/posthog/queries/event_query/event_query.py index d8816634d6ac1..af70ac5f2672c 100644 --- a/posthog/queries/event_query/event_query.py +++ b/posthog/queries/event_query/event_query.py @@ -20,7 +20,7 @@ from posthog.queries.query_date_range import QueryDateRange from posthog.schema import PersonsOnEventsMode from posthog.session_recordings.queries.session_query import SessionQuery -from posthog.queries.util import PersonPropertiesMode +from posthog.queries.util import PersonPropertiesMode, alias_poe_mode_for_legacy from posthog.queries.person_on_events_v2_sql import PERSON_DISTINCT_ID_OVERRIDES_JOIN_SQL @@ -88,7 +88,7 @@ def __init__( self._should_join_persons = should_join_persons self._should_join_sessions = should_join_sessions self._extra_fields = extra_fields - self._person_on_events_mode = person_on_events_mode + self._person_on_events_mode = alias_poe_mode_for_legacy(person_on_events_mode) # Guards against a ClickHouse bug involving multiple joins against the same table with the same column name. # This issue manifests for us with formulas, where on queries A and B we join events against itself diff --git a/posthog/queries/funnels/base.py b/posthog/queries/funnels/base.py index 30265cace41e3..ea337267a2c57 100644 --- a/posthog/queries/funnels/base.py +++ b/posthog/queries/funnels/base.py @@ -32,7 +32,7 @@ ) from posthog.queries.funnels.funnel_event_query import FunnelEventQuery from posthog.queries.insight import insight_sync_execute -from posthog.queries.util import correct_result_for_sampling, get_person_properties_mode +from posthog.queries.util import alias_poe_mode_for_legacy, correct_result_for_sampling, get_person_properties_mode from posthog.schema import PersonsOnEventsMode from posthog.utils import relative_date_parse, generate_short_id @@ -730,7 +730,7 @@ def _get_breakdown_select_prop(self) -> tuple[str, dict[str, Any]]: self.params.update({"breakdown": self._filter.breakdown}) if self._filter.breakdown_type == "person": - if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED: + if alias_poe_mode_for_legacy(self._team.person_on_events_mode) != PersonsOnEventsMode.DISABLED: basic_prop_selector, basic_prop_params = get_single_or_multi_property_string_expr( self._filter.breakdown, table="events", @@ -760,7 +760,10 @@ def _get_breakdown_select_prop(self) -> tuple[str, dict[str, Any]]: # :TRICKY: We only support string breakdown for group properties assert isinstance(self._filter.breakdown, str) - if self._team.person_on_events_mode != PersonsOnEventsMode.DISABLED and groups_on_events_querying_enabled(): + if ( + alias_poe_mode_for_legacy(self._team.person_on_events_mode) != PersonsOnEventsMode.DISABLED + and groups_on_events_querying_enabled() + ): properties_field = f"group{self._filter.breakdown_group_type_index}_properties" expression, _ = get_property_string_expr( table="events", diff --git a/posthog/queries/groups_join_query/groups_join_query.py b/posthog/queries/groups_join_query/groups_join_query.py index 128398584a352..6d57f5eb35436 100644 --- a/posthog/queries/groups_join_query/groups_join_query.py +++ b/posthog/queries/groups_join_query/groups_join_query.py @@ -5,6 +5,7 @@ from posthog.models.filters.retention_filter import RetentionFilter from posthog.models.filters.stickiness_filter import StickinessFilter from posthog.queries.column_optimizer.column_optimizer import ColumnOptimizer +from posthog.queries.util import alias_poe_mode_for_legacy from posthog.schema import PersonsOnEventsMode @@ -29,7 +30,7 @@ def __init__( self._team_id = team_id self._column_optimizer = column_optimizer or ColumnOptimizer(self._filter, self._team_id) self._join_key = join_key - self._person_on_events_mode = person_on_events_mode + self._person_on_events_mode = alias_poe_mode_for_legacy(person_on_events_mode) def get_join_query(self) -> tuple[str, dict]: return "", {} diff --git a/posthog/queries/trends/breakdown.py b/posthog/queries/trends/breakdown.py index db6fd0860c38f..e0bab69fe666d 100644 --- a/posthog/queries/trends/breakdown.py +++ b/posthog/queries/trends/breakdown.py @@ -74,6 +74,7 @@ process_math, ) from posthog.queries.util import ( + alias_poe_mode_for_legacy, get_interval_func_ch, get_person_properties_mode, get_start_of_interval_sql, @@ -108,7 +109,7 @@ def __init__( self.params: dict[str, Any] = {"team_id": team.pk} self.column_optimizer = column_optimizer or ColumnOptimizer(self.filter, self.team_id) self.add_person_urls = add_person_urls - self.person_on_events_mode = person_on_events_mode + self.person_on_events_mode = alias_poe_mode_for_legacy(person_on_events_mode) if person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: self._person_id_alias = f"if(notEmpty({self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.distinct_id), {self.PERSON_ID_OVERRIDES_TABLE_ALIAS}.person_id, {self.EVENT_TABLE_ALIAS}.person_id)" elif person_on_events_mode == PersonsOnEventsMode.PERSON_ID_NO_OVERRIDE_PROPERTIES_ON_EVENTS: diff --git a/posthog/queries/util.py b/posthog/queries/util.py index 44dac7dd8fdb9..a113ae609517a 100644 --- a/posthog/queries/util.py +++ b/posthog/queries/util.py @@ -40,6 +40,14 @@ class PersonPropertiesMode(Enum): """ +def alias_poe_mode_for_legacy(persons_on_events_mode: PersonsOnEventsMode) -> PersonsOnEventsMode: + if persons_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_JOINED: + # PERSON_ID_OVERRIDE_PROPERTIES_JOINED is not implemented in legacy insights + # It's functionally the same as DISABLED, just slower - hence aliasing to DISABLED + return PersonsOnEventsMode.DISABLED + return persons_on_events_mode + + EARLIEST_TIMESTAMP = "2015-01-01" GET_EARLIEST_TIMESTAMP_SQL = """ @@ -178,10 +186,13 @@ def correct_result_for_sampling( def get_person_properties_mode(team: Team) -> PersonPropertiesMode: - if team.person_on_events_mode == PersonsOnEventsMode.DISABLED: + if alias_poe_mode_for_legacy(team.person_on_events_mode) == PersonsOnEventsMode.DISABLED: return PersonPropertiesMode.USING_PERSON_PROPERTIES_COLUMN - if team.person_on_events_mode == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS: + if ( + alias_poe_mode_for_legacy(team.person_on_events_mode) + == PersonsOnEventsMode.PERSON_ID_OVERRIDE_PROPERTIES_ON_EVENTS + ): return PersonPropertiesMode.DIRECT_ON_EVENTS_WITH_POE_V2 return PersonPropertiesMode.DIRECT_ON_EVENTS diff --git a/posthog/temporal/data_imports/pipelines/helpers.py b/posthog/temporal/data_imports/pipelines/helpers.py index 318d4503ce04e..9e38be0fd919e 100644 --- a/posthog/temporal/data_imports/pipelines/helpers.py +++ b/posthog/temporal/data_imports/pipelines/helpers.py @@ -2,22 +2,14 @@ from django.db.models import F from posthog.warehouse.util import database_sync_to_async -CHUNK_SIZE = 10_000 - -async def check_limit( +async def is_job_cancelled( team_id: int, job_id: str, - new_count: int, -): +) -> bool: model = await aget_external_data_job(team_id, job_id) - if new_count >= CHUNK_SIZE: - new_count = 0 - - status = model.status - - return new_count, status + return model.status == ExternalDataJob.Status.CANCELLED @database_sync_to_async diff --git a/posthog/temporal/data_imports/pipelines/rest_source/__init__.py b/posthog/temporal/data_imports/pipelines/rest_source/__init__.py new file mode 100644 index 0000000000000..85ee731cf8fc6 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/rest_source/__init__.py @@ -0,0 +1,370 @@ +"""Generic API Source""" + +from typing import ( + Any, + Optional, + cast, +) +from collections.abc import AsyncGenerator, Iterator +from collections.abc import Callable +import graphlib # type: ignore[import,unused-ignore] + +import dlt +from dlt.common.validation import validate_dict +from dlt.common import jsonpath +from dlt.common.schema.schema import Schema +from dlt.common.schema.typing import TSchemaContract +from dlt.common.configuration.specs import BaseConfiguration + +from dlt.extract.incremental import Incremental +from dlt.extract.source import DltResource, DltSource + +from dlt.sources.helpers.rest_client.client import RESTClient +from dlt.sources.helpers.rest_client.paginators import BasePaginator +from dlt.sources.helpers.rest_client.typing import HTTPMethodBasic + +from posthog.temporal.data_imports.pipelines.helpers import is_job_cancelled +from .typing import ( + ClientConfig, + ResolvedParam, + Endpoint, + EndpointResource, + RESTAPIConfig, +) +from .config_setup import ( + IncrementalParam, + create_auth, + create_paginator, + build_resource_dependency_graph, + process_parent_data_item, + setup_incremental_object, + create_response_hooks, +) +from .utils import exclude_keys # noqa: F401 + + +def rest_api_source( + config: RESTAPIConfig, + team_id: int, + job_id: str, + name: Optional[str] = None, + section: Optional[str] = None, + max_table_nesting: Optional[int] = None, + root_key: bool = False, + schema: Optional[Schema] = None, + schema_contract: Optional[TSchemaContract] = None, + spec: Optional[type[BaseConfiguration]] = None, +) -> DltSource: + """Creates and configures a REST API source for data extraction. + + Args: + config (RESTAPIConfig): Configuration for the REST API source. + name (str, optional): Name of the source. + section (str, optional): Section of the configuration file. + max_table_nesting (int, optional): Maximum depth of nested table above which + the remaining nodes are loaded as structs or JSON. + root_key (bool, optional): Enables merging on all resources by propagating + root foreign key to child tables. This option is most useful if you + plan to change write disposition of a resource to disable/enable merge. + Defaults to False. + schema (Schema, optional): An explicit `Schema` instance to be associated + with the source. If not present, `dlt` creates a new `Schema` object + with provided `name`. If such `Schema` already exists in the same + folder as the module containing the decorated function, such schema + will be loaded from file. + schema_contract (TSchemaContract, optional): Schema contract settings + that will be applied to this resource. + spec (type[BaseConfiguration], optional): A specification of configuration + and secret values required by the source. + + Returns: + DltSource: A configured dlt source. + + Example: + pokemon_source = rest_api_source({ + "client": { + "base_url": "https://pokeapi.co/api/v2/", + "paginator": "json_response", + }, + "endpoints": { + "pokemon": { + "params": { + "limit": 100, # Default page size is 20 + }, + "resource": { + "primary_key": "id", + } + }, + }, + }) + """ + decorated = dlt.source( + rest_api_resources, + name, + section, + max_table_nesting, + root_key, + schema, + schema_contract, + spec, + ) + + return decorated(config, team_id, job_id) + + +def rest_api_resources(config: RESTAPIConfig, team_id: int, job_id: str) -> list[DltResource]: + """Creates a list of resources from a REST API configuration. + + Args: + config (RESTAPIConfig): Configuration for the REST API source. + + Returns: + list[DltResource]: List of dlt resources. + + Example: + github_source = rest_api_resources({ + "client": { + "base_url": "https://api.github.com/repos/dlt-hub/dlt/", + "auth": { + "token": dlt.secrets["token"], + }, + }, + "resource_defaults": { + "primary_key": "id", + "write_disposition": "merge", + "endpoint": { + "params": { + "per_page": 100, + }, + }, + }, + "resources": [ + { + "name": "issues", + "endpoint": { + "path": "issues", + "params": { + "sort": "updated", + "direction": "desc", + "state": "open", + "since": { + "type": "incremental", + "cursor_path": "updated_at", + "initial_value": "2024-01-25T11:21:28Z", + }, + }, + }, + }, + { + "name": "issue_comments", + "endpoint": { + "path": "issues/{issue_number}/comments", + "params": { + "issue_number": { + "type": "resolve", + "resource": "issues", + "field": "number", + } + }, + }, + }, + ], + }) + """ + + validate_dict(RESTAPIConfig, config, path=".") + + client_config = config["client"] + resource_defaults = config.get("resource_defaults", {}) + resource_list = config["resources"] + + ( + dependency_graph, + endpoint_resource_map, + resolved_param_map, + ) = build_resource_dependency_graph( + resource_defaults, + resource_list, + ) + + resources = create_resources( + client_config, + dependency_graph, + endpoint_resource_map, + resolved_param_map, + team_id=team_id, + job_id=job_id, + ) + + return list(resources.values()) + + +def create_resources( + client_config: ClientConfig, + dependency_graph: graphlib.TopologicalSorter, + endpoint_resource_map: dict[str, EndpointResource], + resolved_param_map: dict[str, Optional[ResolvedParam]], + team_id: int, + job_id: str, +) -> dict[str, DltResource]: + resources = {} + + for resource_name in dependency_graph.static_order(): + resource_name = cast(str, resource_name) + endpoint_resource = endpoint_resource_map[resource_name] + endpoint_config = cast(Endpoint, endpoint_resource.get("endpoint")) + request_params = endpoint_config.get("params", {}) + request_json = endpoint_config.get("json", None) + paginator = create_paginator(endpoint_config.get("paginator")) + + resolved_param: ResolvedParam | None = resolved_param_map[resource_name] + + include_from_parent: list[str] = endpoint_resource.get("include_from_parent", []) + if not resolved_param and include_from_parent: + raise ValueError( + f"Resource {resource_name} has include_from_parent but is not " "dependent on another resource" + ) + + ( + incremental_object, + incremental_param, + ) = setup_incremental_object(request_params, endpoint_config.get("incremental")) + + client = RESTClient( + base_url=client_config.get("base_url"), + headers=client_config.get("headers"), + auth=create_auth(client_config.get("auth")), + paginator=create_paginator(client_config.get("paginator")), + ) + + hooks = create_response_hooks(endpoint_config.get("response_actions")) + + resource_kwargs = exclude_keys(endpoint_resource, {"endpoint", "include_from_parent"}) + + if resolved_param is None: + + async def paginate_resource( + method: HTTPMethodBasic, + path: str, + params: dict[str, Any], + json: Optional[dict[str, Any]], + paginator: Optional[BasePaginator], + data_selector: Optional[jsonpath.TJsonPath], + hooks: Optional[dict[str, Any]], + client: RESTClient = client, + incremental_object: Optional[Incremental[Any]] = incremental_object, + incremental_param: IncrementalParam | None = incremental_param, + ) -> AsyncGenerator[Iterator[Any], Any]: + yield dlt.mark.materialize_table_schema() # type: ignore + + if await is_job_cancelled(team_id=team_id, job_id=job_id): + return + + if incremental_object and incremental_param: + params[incremental_param.start] = incremental_object.last_value + if incremental_param.end: + params[incremental_param.end] = incremental_object.end_value + + yield client.paginate( + method=method, + path=path, + params=params, + json=json, + paginator=paginator, + data_selector=data_selector, + hooks=hooks, + ) + + resources[resource_name] = dlt.resource( + paginate_resource, + **resource_kwargs, # TODO: implement typing.Unpack + )( + method=endpoint_config.get("method", "get"), + path=endpoint_config.get("path"), + params=request_params, + json=request_json, + paginator=paginator, + data_selector=endpoint_config.get("data_selector"), + hooks=hooks, + ) + + else: + predecessor = resources[resolved_param.resolve_config["resource"]] + + base_params = exclude_keys(request_params, {resolved_param.param_name}) + + async def paginate_dependent_resource( + items: list[dict[str, Any]], + method: HTTPMethodBasic, + path: str, + params: dict[str, Any], + paginator: Optional[BasePaginator], + data_selector: Optional[jsonpath.TJsonPath], + hooks: Optional[dict[str, Any]], + client: RESTClient = client, + resolved_param: ResolvedParam = resolved_param, + include_from_parent: list[str] = include_from_parent, + incremental_object: Optional[Incremental[Any]] = incremental_object, + incremental_param: IncrementalParam | None = incremental_param, + ) -> AsyncGenerator[Any, Any]: + yield dlt.mark.materialize_table_schema() # type: ignore + + if await is_job_cancelled(team_id=team_id, job_id=job_id): + return + + if incremental_object and incremental_param: + params[incremental_param.start] = incremental_object.last_value + if incremental_param.end: + params[incremental_param.end] = incremental_object.end_value + + for item in items: + formatted_path, parent_record = process_parent_data_item( + path, item, resolved_param, include_from_parent + ) + + for child_page in client.paginate( + method=method, + path=formatted_path, + params=params, + paginator=paginator, + data_selector=data_selector, + hooks=hooks, + ): + if parent_record: + for child_record in child_page: + child_record.update(parent_record) + yield child_page + + resources[resource_name] = dlt.resource( # type: ignore[call-overload] + paginate_dependent_resource, + data_from=predecessor, + **resource_kwargs, # TODO: implement typing.Unpack + )( + method=endpoint_config.get("method", "get"), + path=endpoint_config.get("path"), + params=base_params, + paginator=paginator, + data_selector=endpoint_config.get("data_selector"), + hooks=hooks, + ) + + return resources + + +# XXX: This is a workaround pass test_dlt_init.py +# since the source uses dlt.source as a function +def _register_source(source_func: Callable[..., DltSource]) -> None: + import inspect + from dlt.common.configuration import get_fun_spec + from dlt.common.source import _SOURCES, SourceInfo + + spec = get_fun_spec(source_func) + func_module = inspect.getmodule(source_func) + _SOURCES[source_func.__name__] = SourceInfo( + SPEC=spec, + f=source_func, + module=func_module, + ) + + +_register_source(rest_api_source) diff --git a/posthog/temporal/data_imports/pipelines/rest_source/config_setup.py b/posthog/temporal/data_imports/pipelines/rest_source/config_setup.py new file mode 100644 index 0000000000000..9eda391449d31 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/rest_source/config_setup.py @@ -0,0 +1,455 @@ +from copy import copy +from typing import ( + Any, + Optional, + cast, + NamedTuple, +) +from collections.abc import Callable +import graphlib # type: ignore[import,unused-ignore] +import string + +import dlt +from dlt.common import logger +from dlt.common.configuration import resolve_configuration +from dlt.common.schema.utils import merge_columns +from dlt.common.utils import update_dict_nested +from dlt.common import jsonpath + +from dlt.extract.incremental import Incremental +from dlt.extract.utils import ensure_table_schema_columns + +from dlt.sources.helpers.requests import Response +from dlt.sources.helpers.rest_client.paginators import ( + BasePaginator, + SinglePagePaginator, + HeaderLinkPaginator, + JSONResponsePaginator, + JSONResponseCursorPaginator, + OffsetPaginator, + PageNumberPaginator, +) +from dlt.sources.helpers.rest_client.detector import single_entity_path +from dlt.sources.helpers.rest_client.exceptions import IgnoreResponseException +from dlt.sources.helpers.rest_client.auth import ( + AuthConfigBase, + HttpBasicAuth, + BearerTokenAuth, + APIKeyAuth, +) + +from .typing import ( + EndpointResourceBase, + PaginatorType, + AuthType, + AuthConfig, + IncrementalArgs, + IncrementalConfig, + PaginatorConfig, + ResolvedParam, + ResponseAction, + Endpoint, + EndpointResource, +) +from .utils import exclude_keys + + +PAGINATOR_MAP: dict[PaginatorType, type[BasePaginator]] = { + "json_response": JSONResponsePaginator, + "header_link": HeaderLinkPaginator, + "auto": None, + "single_page": SinglePagePaginator, + "cursor": JSONResponseCursorPaginator, + "offset": OffsetPaginator, + "page_number": PageNumberPaginator, +} + +AUTH_MAP: dict[AuthType, type[AuthConfigBase]] = { + "bearer": BearerTokenAuth, + "api_key": APIKeyAuth, + "http_basic": HttpBasicAuth, +} + + +class IncrementalParam(NamedTuple): + start: str + end: Optional[str] + + +def get_paginator_class(paginator_type: PaginatorType) -> type[BasePaginator]: + try: + return PAGINATOR_MAP[paginator_type] + except KeyError: + available_options = ", ".join(PAGINATOR_MAP.keys()) + raise ValueError(f"Invalid paginator: {paginator_type}. " f"Available options: {available_options}") + + +def create_paginator( + paginator_config: Optional[PaginatorConfig], +) -> Optional[BasePaginator]: + if isinstance(paginator_config, BasePaginator): + return paginator_config + + if isinstance(paginator_config, str): + paginator_class = get_paginator_class(paginator_config) + try: + # `auto` has no associated class in `PAGINATOR_MAP` + return paginator_class() if paginator_class else None + except TypeError: + raise ValueError( + f"Paginator {paginator_config} requires arguments to create an instance. Use {paginator_class} instance instead." + ) + + if isinstance(paginator_config, dict): + paginator_type = paginator_config.get("type", "auto") + paginator_class = get_paginator_class(paginator_type) + return paginator_class(**exclude_keys(paginator_config, {"type"})) if paginator_class else None + + return None + + +def get_auth_class(auth_type: AuthType) -> type[AuthConfigBase]: + try: + return AUTH_MAP[auth_type] + except KeyError: + available_options = ", ".join(AUTH_MAP.keys()) + raise ValueError(f"Invalid paginator: {auth_type}. " f"Available options: {available_options}") + + +def create_auth(auth_config: Optional[AuthConfig]) -> Optional[AuthConfigBase]: + auth: AuthConfigBase = None + if isinstance(auth_config, AuthConfigBase): + auth = auth_config + + if isinstance(auth_config, str): + auth_class = get_auth_class(auth_config) + auth = auth_class() + + if isinstance(auth_config, dict): + auth_type = auth_config.get("type", "bearer") + auth_class = get_auth_class(auth_type) + auth = auth_class(**exclude_keys(auth_config, {"type"})) + + if auth: + # TODO: provide explicitly (non-default) values as explicit explicit_value=dict(auth) + # this will resolve auth which is a configuration using current section context + return resolve_configuration(auth) + + return None + + +def setup_incremental_object( + request_params: dict[str, Any], + incremental_config: Optional[IncrementalConfig] = None, +) -> tuple[Optional[Incremental[Any]], Optional[IncrementalParam]]: + for key, value in request_params.items(): + if isinstance(value, dlt.sources.incremental): + return value, IncrementalParam(start=key, end=None) + if isinstance(value, dict) and value.get("type") == "incremental": + config = exclude_keys(value, {"type"}) + # TODO: implement param type to bind incremental to + return ( + dlt.sources.incremental(**config), + IncrementalParam(start=key, end=None), + ) + if incremental_config: + config = exclude_keys(incremental_config, {"start_param", "end_param"}) + return ( + dlt.sources.incremental(**cast(IncrementalArgs, config)), + IncrementalParam( + start=incremental_config["start_param"], + end=incremental_config.get("end_param"), + ), + ) + + return None, None + + +def make_parent_key_name(resource_name: str, field_name: str) -> str: + return f"_{resource_name}_{field_name}" + + +def build_resource_dependency_graph( + resource_defaults: EndpointResourceBase, + resource_list: list[str | EndpointResource], +) -> tuple[Any, dict[str, EndpointResource], dict[str, Optional[ResolvedParam]]]: + dependency_graph = graphlib.TopologicalSorter() + endpoint_resource_map: dict[str, EndpointResource] = {} + resolved_param_map: dict[str, ResolvedParam] = {} + + # expand all resources and index them + for resource_kwargs in resource_list: + if isinstance(resource_kwargs, dict): + # clone resource here, otherwise it needs to be cloned in several other places + # note that this clones only dict structure, keeping all instances without deepcopy + resource_kwargs = update_dict_nested({}, resource_kwargs) # type: ignore[assignment] + + endpoint_resource = _make_endpoint_resource(resource_kwargs, resource_defaults) + assert isinstance(endpoint_resource["endpoint"], dict) + _setup_single_entity_endpoint(endpoint_resource["endpoint"]) + _bind_path_params(endpoint_resource) + + resource_name = endpoint_resource["name"] + assert isinstance(resource_name, str), f"Resource name must be a string, got {type(resource_name)}" + + if resource_name in endpoint_resource_map: + raise ValueError(f"Resource {resource_name} has already been defined") + endpoint_resource_map[resource_name] = endpoint_resource + + # create dependency graph + for resource_name, endpoint_resource in endpoint_resource_map.items(): + assert isinstance(endpoint_resource["endpoint"], dict) + # connect transformers to resources via resolved params + resolved_params = _find_resolved_params(endpoint_resource["endpoint"]) + if len(resolved_params) > 1: + raise ValueError(f"Multiple resolved params for resource {resource_name}: {resolved_params}") + elif len(resolved_params) == 1: + resolved_param = resolved_params[0] + predecessor = resolved_param.resolve_config["resource"] + if predecessor not in endpoint_resource_map: + raise ValueError( + f"A transformer resource {resource_name} refers to non existing parent resource {predecessor} on {resolved_param}" + ) + dependency_graph.add(resource_name, predecessor) + resolved_param_map[resource_name] = resolved_param + else: + dependency_graph.add(resource_name) + resolved_param_map[resource_name] = None + + return dependency_graph, endpoint_resource_map, resolved_param_map + + +def _make_endpoint_resource(resource: str | EndpointResource, default_config: EndpointResourceBase) -> EndpointResource: + """ + Creates an EndpointResource object based on the provided resource + definition and merges it with the default configuration. + + This function supports defining a resource in multiple formats: + - As a string: The string is interpreted as both the resource name + and its endpoint path. + - As a dictionary: The dictionary must include `name` and `endpoint` + keys. The `endpoint` can be a string representing the path, + or a dictionary for more complex configurations. If the `endpoint` + is missing the `path` key, the resource name is used as the `path`. + """ + if isinstance(resource, str): + resource = {"name": resource, "endpoint": {"path": resource}} + return _merge_resource_endpoints(default_config, resource) + + if "endpoint" in resource: + if isinstance(resource["endpoint"], str): + resource["endpoint"] = {"path": resource["endpoint"]} + else: + # endpoint is optional + resource["endpoint"] = {} + + if "path" not in resource["endpoint"]: + resource["endpoint"]["path"] = resource["name"] # type: ignore + + return _merge_resource_endpoints(default_config, resource) + + +def _bind_path_params(resource: EndpointResource) -> None: + """Binds params declared in path to params available in `params`. Pops the + bound params but. Params of type `resolve` and `incremental` are skipped + and bound later. + """ + path_params: dict[str, Any] = {} + assert isinstance(resource["endpoint"], dict) # type guard + resolve_params = [r.param_name for r in _find_resolved_params(resource["endpoint"])] + path = resource["endpoint"]["path"] + for format_ in string.Formatter().parse(path): + name = format_[1] + if name: + params = resource["endpoint"].get("params", {}) + if name not in params and name not in path_params: + raise ValueError( + f"The path {path} defined in resource {resource['name']} requires param with name {name} but it is not found in {params}" + ) + if name in resolve_params: + resolve_params.remove(name) + if name in params: + if not isinstance(params[name], dict): + # bind resolved param and pop it from endpoint + path_params[name] = params.pop(name) + else: + param_type = params[name].get("type") + if param_type != "resolve": + raise ValueError( + f"The path {path} defined in resource {resource['name']} tries to bind param {name} with type {param_type}. Paths can only bind 'resource' type params." + ) + # resolved params are bound later + path_params[name] = "{" + name + "}" + + if len(resolve_params) > 0: + raise NotImplementedError( + f"Resource {resource['name']} defines resolve params {resolve_params} that are not bound in path {path}. Resolve query params not supported yet." + ) + + resource["endpoint"]["path"] = path.format(**path_params) + + +def _setup_single_entity_endpoint(endpoint: Endpoint) -> Endpoint: + """Tries to guess if the endpoint refers to a single entity and when detected: + * if `data_selector` was not specified (or is None), "$" is selected + * if `paginator` was not specified (or is None), SinglePagePaginator is selected + + Endpoint is modified in place and returned + """ + # try to guess if list of entities or just single entity is returned + if single_entity_path(endpoint["path"]): + if endpoint.get("data_selector") is None: + endpoint["data_selector"] = "$" + if endpoint.get("paginator") is None: + endpoint["paginator"] = SinglePagePaginator() + return endpoint + + +def _find_resolved_params(endpoint_config: Endpoint) -> list[ResolvedParam]: + """ + Find all resolved params in the endpoint configuration and return + a list of ResolvedParam objects. + + Resolved params are of type ResolveParamConfig (bound param with a key "type" set to "resolve".) + """ + return [ + ResolvedParam(key, value) # type: ignore[arg-type] + for key, value in endpoint_config.get("params", {}).items() + if (isinstance(value, dict) and value.get("type") == "resolve") + ] + + +def _handle_response_actions(response: Response, actions: list[ResponseAction]) -> Optional[str]: + """Handle response actions based on the response and the provided actions.""" + content = response.text + + for action in actions: + status_code = action.get("status_code") + content_substr: str = action.get("content") + action_type: str = action.get("action") + + if status_code is not None and content_substr is not None: + if response.status_code == status_code and content_substr in content: + return action_type + + elif status_code is not None: + if response.status_code == status_code: + return action_type + + elif content_substr is not None: + if content_substr in content: + return action_type + + return None + + +def _create_response_actions_hook( + response_actions: list[ResponseAction], +) -> Callable[[Response, Any, Any], None]: + def response_actions_hook(response: Response, *args: Any, **kwargs: Any) -> None: + action_type = _handle_response_actions(response, response_actions) + if action_type == "ignore": + logger.info(f"Ignoring response with code {response.status_code} " f"and content '{response.json()}'.") + raise IgnoreResponseException + + # If no action has been taken and the status code indicates an error, + # raise an HTTP error based on the response status + if not action_type and response.status_code >= 400: + response.raise_for_status() + + return response_actions_hook + + +def create_response_hooks( + response_actions: Optional[list[ResponseAction]], +) -> Optional[dict[str, Any]]: + """Create response hooks based on the provided response actions. Note + that if the error status code is not handled by the response actions, + the default behavior is to raise an HTTP error. + + Example: + response_actions = [ + {"status_code": 404, "action": "ignore"}, + {"content": "Not found", "action": "ignore"}, + {"status_code": 429, "action": "retry"}, + {"status_code": 200, "content": "some text", "action": "retry"}, + ] + hooks = create_response_hooks(response_actions) + """ + if response_actions: + return {"response": [_create_response_actions_hook(response_actions)]} + return None + + +def process_parent_data_item( + path: str, + item: dict[str, Any], + resolved_param: ResolvedParam, + include_from_parent: list[str], +) -> tuple[str, dict[str, Any]]: + parent_resource_name = resolved_param.resolve_config["resource"] + + field_values = jsonpath.find_values(resolved_param.field_path, item) + + if not field_values: + field_path = resolved_param.resolve_config["field"] + raise ValueError( + f"Transformer expects a field '{field_path}' to be present in the incoming data from resource {parent_resource_name} in order to bind it to path param {resolved_param.param_name}. Available parent fields are {', '.join(item.keys())}" + ) + bound_path = path.format(**{resolved_param.param_name: field_values[0]}) + + parent_record: dict[str, Any] = {} + if include_from_parent: + for parent_key in include_from_parent: + child_key = make_parent_key_name(parent_resource_name, parent_key) + if parent_key not in item: + raise ValueError( + f"Transformer expects a field '{parent_key}' to be present in the incoming data from resource {parent_resource_name} in order to include it in child records under {child_key}. Available parent fields are {', '.join(item.keys())}" + ) + parent_record[child_key] = item[parent_key] + + return bound_path, parent_record + + +def _merge_resource_endpoints(default_config: EndpointResourceBase, config: EndpointResource) -> EndpointResource: + """Merges `default_config` and `config`, returns new instance of EndpointResource""" + # NOTE: config is normalized and always has "endpoint" field which is a dict + # TODO: could deep merge paginators and auths of the same type + + default_endpoint = default_config.get("endpoint", Endpoint()) + assert isinstance(default_endpoint, dict) + config_endpoint = config["endpoint"] + assert isinstance(config_endpoint, dict) + + merged_endpoint: Endpoint = { + **default_endpoint, + **{k: v for k, v in config_endpoint.items() if k not in ("json", "params")}, # type: ignore[typeddict-item] + } + # merge endpoint, only params and json are allowed to deep merge + if "json" in config_endpoint: + merged_endpoint["json"] = { + **(merged_endpoint.get("json", {})), + **config_endpoint["json"], + } + if "params" in config_endpoint: + merged_endpoint["params"] = { + **(merged_endpoint.get("json", {})), + **config_endpoint["params"], + } + # merge columns + if (default_columns := default_config.get("columns")) and (columns := config.get("columns")): + # merge only native dlt formats, skip pydantic and others + if isinstance(columns, list | dict) and isinstance(default_columns, list | dict): + # normalize columns + columns = ensure_table_schema_columns(columns) + default_columns = ensure_table_schema_columns(default_columns) + # merge columns with deep merging hints + config["columns"] = merge_columns(copy(default_columns), columns, merge_columns=True) + + # no need to deep merge resources + merged_resource: EndpointResource = { + **default_config, + **config, + "endpoint": merged_endpoint, + } + return merged_resource diff --git a/posthog/temporal/data_imports/pipelines/rest_source/exceptions.py b/posthog/temporal/data_imports/pipelines/rest_source/exceptions.py new file mode 100644 index 0000000000000..93e807d29b9fb --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/rest_source/exceptions.py @@ -0,0 +1,5 @@ +from dlt.common.exceptions import DltException + + +class RestApiException(DltException): + pass diff --git a/posthog/temporal/data_imports/pipelines/rest_source/typing.py b/posthog/temporal/data_imports/pipelines/rest_source/typing.py new file mode 100644 index 0000000000000..4a28912ccb238 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/rest_source/typing.py @@ -0,0 +1,254 @@ +from typing import ( + Any, + Literal, + Optional, + TypedDict, +) +from dataclasses import dataclass, field + +from dlt.common import jsonpath +from dlt.common.typing import TSortOrder +from dlt.common.schema.typing import ( + TColumnNames, + TTableFormat, + TAnySchemaColumns, + TWriteDispositionConfig, + TSchemaContract, +) + +from dlt.extract.items import TTableHintTemplate +from dlt.extract.incremental.typing import LastValueFunc + +from dlt.sources.helpers.rest_client.paginators import BasePaginator +from dlt.sources.helpers.rest_client.typing import HTTPMethodBasic +from dlt.sources.helpers.rest_client.auth import AuthConfigBase, TApiKeyLocation + +from dlt.sources.helpers.rest_client.paginators import ( + SinglePagePaginator, + HeaderLinkPaginator, + JSONResponsePaginator, + JSONResponseCursorPaginator, + OffsetPaginator, + PageNumberPaginator, +) +from dlt.sources.helpers.rest_client.auth import ( + HttpBasicAuth, + BearerTokenAuth, + APIKeyAuth, +) + +PaginatorType = Literal[ + "json_response", + "header_link", + "auto", + "single_page", + "cursor", + "offset", + "page_number", +] + + +class PaginatorTypeConfig(TypedDict, total=True): + type: PaginatorType # noqa + + +class PageNumberPaginatorConfig(PaginatorTypeConfig, total=False): + """A paginator that uses page number-based pagination strategy.""" + + initial_page: Optional[int] + page_param: Optional[str] + total_path: Optional[jsonpath.TJsonPath] + maximum_page: Optional[int] + + +class OffsetPaginatorConfig(PaginatorTypeConfig, total=False): + """A paginator that uses offset-based pagination strategy.""" + + limit: int + offset: Optional[int] + offset_param: Optional[str] + limit_param: Optional[str] + total_path: Optional[jsonpath.TJsonPath] + maximum_offset: Optional[int] + + +class HeaderLinkPaginatorConfig(PaginatorTypeConfig, total=False): + """A paginator that uses the 'Link' header in HTTP responses + for pagination.""" + + links_next_key: Optional[str] + + +class JSONResponsePaginatorConfig(PaginatorTypeConfig, total=False): + """Locates the next page URL within the JSON response body. The key + containing the URL can be specified using a JSON path.""" + + next_url_path: Optional[jsonpath.TJsonPath] + + +class JSONResponseCursorPaginatorConfig(PaginatorTypeConfig, total=False): + """Uses a cursor parameter for pagination, with the cursor value found in + the JSON response body.""" + + cursor_path: Optional[jsonpath.TJsonPath] + cursor_param: Optional[str] + + +PaginatorConfig = ( + PaginatorType + | PageNumberPaginatorConfig + | OffsetPaginatorConfig + | HeaderLinkPaginatorConfig + | JSONResponsePaginatorConfig + | JSONResponseCursorPaginatorConfig + | BasePaginator + | SinglePagePaginator + | HeaderLinkPaginator + | JSONResponsePaginator + | JSONResponseCursorPaginator + | OffsetPaginator + | PageNumberPaginator +) + + +AuthType = Literal["bearer", "api_key", "http_basic"] + + +class AuthTypeConfig(TypedDict, total=True): + type: AuthType # noqa + + +class BearerTokenAuthConfig(TypedDict, total=False): + """Uses `token` for Bearer authentication in "Authorization" header.""" + + # we allow for a shorthand form of bearer auth, without a type + type: Optional[AuthType] # noqa + token: str + + +class ApiKeyAuthConfig(AuthTypeConfig, total=False): + """Uses provided `api_key` to create authorization data in the specified `location` (query, param, header, cookie) under specified `name`""" + + name: Optional[str] + api_key: str + location: Optional[TApiKeyLocation] + + +class HttpBasicAuthConfig(AuthTypeConfig, total=True): + """Uses HTTP basic authentication""" + + username: str + password: str + + +# TODO: add later +# class OAuthJWTAuthConfig(AuthTypeConfig, total=True): + + +AuthConfig = ( + AuthConfigBase + | AuthType + | BearerTokenAuthConfig + | ApiKeyAuthConfig + | HttpBasicAuthConfig + | BearerTokenAuth + | APIKeyAuth + | HttpBasicAuth +) + + +class ClientConfig(TypedDict, total=False): + base_url: str + headers: Optional[dict[str, str]] + auth: Optional[AuthConfig] + paginator: Optional[PaginatorConfig] + + +class IncrementalArgs(TypedDict, total=False): + cursor_path: str + initial_value: Optional[str] + last_value_func: Optional[LastValueFunc[str]] + primary_key: Optional[TTableHintTemplate[TColumnNames]] + end_value: Optional[str] + row_order: Optional[TSortOrder] + + +class IncrementalConfig(IncrementalArgs, total=False): + start_param: str + end_param: Optional[str] + + +ParamBindType = Literal["resolve", "incremental"] + + +class ParamBindConfig(TypedDict): + type: ParamBindType # noqa + + +class ResolveParamConfig(ParamBindConfig): + resource: str + field: str + + +class IncrementalParamConfig(ParamBindConfig, IncrementalArgs): + pass + # TODO: implement param type to bind incremental to + # param_type: Optional[Literal["start_param", "end_param"]] + + +@dataclass +class ResolvedParam: + param_name: str + resolve_config: ResolveParamConfig + field_path: jsonpath.TJsonPath = field(init=False) + + def __post_init__(self) -> None: + self.field_path = jsonpath.compile_path(self.resolve_config["field"]) + + +class ResponseAction(TypedDict, total=False): + status_code: Optional[int | str] + content: Optional[str] + action: str + + +class Endpoint(TypedDict, total=False): + path: Optional[str] + method: Optional[HTTPMethodBasic] + params: Optional[dict[str, ResolveParamConfig | IncrementalParamConfig | Any]] + json: Optional[dict[str, Any]] + paginator: Optional[PaginatorConfig] + data_selector: Optional[jsonpath.TJsonPath] + response_actions: Optional[list[ResponseAction]] + incremental: Optional[IncrementalConfig] + + +class ResourceBase(TypedDict, total=False): + """Defines hints that may be passed to `dlt.resource` decorator""" + + table_name: Optional[TTableHintTemplate[str]] + max_table_nesting: Optional[int] + write_disposition: Optional[TTableHintTemplate[TWriteDispositionConfig]] + parent: Optional[TTableHintTemplate[str]] + columns: Optional[TTableHintTemplate[TAnySchemaColumns]] + primary_key: Optional[TTableHintTemplate[TColumnNames]] + merge_key: Optional[TTableHintTemplate[TColumnNames]] + schema_contract: Optional[TTableHintTemplate[TSchemaContract]] + table_format: Optional[TTableHintTemplate[TTableFormat]] + selected: Optional[bool] + parallelized: Optional[bool] + + +class EndpointResourceBase(ResourceBase, total=False): + endpoint: Optional[str | Endpoint] + include_from_parent: Optional[list[str]] + + +class EndpointResource(EndpointResourceBase, total=False): + name: TTableHintTemplate[str] + + +class RESTAPIConfig(TypedDict): + client: ClientConfig + resource_defaults: Optional[EndpointResourceBase] + resources: list[str | EndpointResource] diff --git a/posthog/temporal/data_imports/pipelines/rest_source/utils.py b/posthog/temporal/data_imports/pipelines/rest_source/utils.py new file mode 100644 index 0000000000000..91eca3cf48004 --- /dev/null +++ b/posthog/temporal/data_imports/pipelines/rest_source/utils.py @@ -0,0 +1,36 @@ +from typing import Any +from collections.abc import Mapping, Iterable + +from dlt.common import logger +from dlt.extract.source import DltSource + + +def join_url(base_url: str, path: str) -> str: + if not base_url.endswith("/"): + base_url += "/" + return base_url + path.lstrip("/") + + +def exclude_keys(d: Mapping[str, Any], keys: Iterable[str]) -> dict[str, Any]: + """Removes specified keys from a dictionary and returns a new dictionary. + + Args: + d (Mapping[str, Any]): The dictionary to remove keys from. + keys (Iterable[str]): The keys to remove. + + Returns: + Dict[str, Any]: A new dictionary with the specified keys removed. + """ + return {k: v for k, v in d.items() if k not in keys} + + +def check_connection( + source: DltSource, + *resource_names: str, +) -> tuple[bool, str]: + try: + list(source.with_resources(*resource_names).add_limit(1)) + return (True, "") + except Exception as e: + logger.error(f"Error checking connection: {e}") + return (False, str(e)) diff --git a/posthog/temporal/data_imports/pipelines/stripe/__init__.py b/posthog/temporal/data_imports/pipelines/stripe/__init__.py index 228e94778e689..d74c04ca5e7dc 100644 --- a/posthog/temporal/data_imports/pipelines/stripe/__init__.py +++ b/posthog/temporal/data_imports/pipelines/stripe/__init__.py @@ -3,15 +3,17 @@ from dlt.sources.helpers.requests import Response, Request from posthog.temporal.data_imports.pipelines.rest_source import RESTAPIConfig, rest_api_resources from posthog.temporal.data_imports.pipelines.rest_source.typing import EndpointResource +from posthog.warehouse.models.external_table_definitions import get_dlt_mapping_for_external_table def get_resource(name: str, is_incremental: bool) -> EndpointResource: resources: dict[str, EndpointResource] = { "BalanceTransaction": { "name": "BalanceTransaction", - "table_name": "balance_transaction", + "table_name": "balancetransaction", "primary_key": "id", "write_disposition": "merge", + "columns": get_dlt_mapping_for_external_table("stripe_balancetransaction"), # type: ignore "endpoint": { "data_selector": "data", "path": "/v1/balance_transactions", @@ -34,6 +36,7 @@ def get_resource(name: str, is_incremental: bool) -> EndpointResource: "table_name": "charge", "primary_key": "id", "write_disposition": "merge", + "columns": get_dlt_mapping_for_external_table("stripe_charge"), # type: ignore "endpoint": { "data_selector": "data", "path": "/v1/charges", @@ -55,6 +58,7 @@ def get_resource(name: str, is_incremental: bool) -> EndpointResource: "table_name": "customer", "primary_key": "id", "write_disposition": "merge", + "columns": get_dlt_mapping_for_external_table("stripe_customer"), # type: ignore "endpoint": { "data_selector": "data", "path": "/v1/customers", @@ -75,6 +79,7 @@ def get_resource(name: str, is_incremental: bool) -> EndpointResource: "table_name": "invoice", "primary_key": "id", "write_disposition": "merge", + "columns": get_dlt_mapping_for_external_table("stripe_invoice"), # type: ignore "endpoint": { "data_selector": "data", "path": "/v1/invoices", @@ -104,6 +109,7 @@ def get_resource(name: str, is_incremental: bool) -> EndpointResource: "table_name": "price", "primary_key": "id", "write_disposition": "merge", + "columns": get_dlt_mapping_for_external_table("stripe_price"), # type: ignore "endpoint": { "data_selector": "data", "path": "/v1/prices", @@ -128,6 +134,7 @@ def get_resource(name: str, is_incremental: bool) -> EndpointResource: "table_name": "product", "primary_key": "id", "write_disposition": "merge", + "columns": get_dlt_mapping_for_external_table("stripe_product"), # type: ignore "endpoint": { "data_selector": "data", "path": "/v1/products", @@ -150,6 +157,7 @@ def get_resource(name: str, is_incremental: bool) -> EndpointResource: "table_name": "subscription", "primary_key": "id", "write_disposition": "merge", + "columns": get_dlt_mapping_for_external_table("stripe_subscription"), # type: ignore "endpoint": { "data_selector": "data", "path": "/v1/subscriptions", @@ -201,7 +209,9 @@ def update_request(self, request: Request) -> None: @dlt.source(max_table_nesting=0) -def stripe_source(api_key: str, account_id: str, endpoint: str, is_incremental: bool = False): +def stripe_source( + api_key: str, account_id: str, endpoint: str, team_id: int, job_id: str, is_incremental: bool = False +): config: RESTAPIConfig = { "client": { "base_url": "https://api.stripe.com/", @@ -222,4 +232,4 @@ def stripe_source(api_key: str, account_id: str, endpoint: str, is_incremental: "resources": [get_resource(endpoint, is_incremental)], } - yield from rest_api_resources(config) + yield from rest_api_resources(config, team_id, job_id) diff --git a/posthog/temporal/data_imports/pipelines/test/test_pipeline.py b/posthog/temporal/data_imports/pipelines/test/test_pipeline.py index 23fc37c6d80f8..435bcda33a9c5 100644 --- a/posthog/temporal/data_imports/pipelines/test/test_pipeline.py +++ b/posthog/temporal/data_imports/pipelines/test/test_pipeline.py @@ -49,7 +49,14 @@ async def _create_pipeline(self, schema_name: str, incremental: bool): job_type="Stripe", team_id=self.team.pk, ), - source=stripe_source(api_key="", account_id="", endpoint=schema_name, is_incremental=False), + source=stripe_source( + api_key="", + account_id="", + endpoint=schema_name, + is_incremental=False, + team_id=self.team.pk, + job_id=str(job.pk), + ), logger=structlog.get_logger(), incremental=incremental, ) diff --git a/posthog/temporal/data_imports/workflow_activities/import_data.py b/posthog/temporal/data_imports/workflow_activities/import_data.py index 9062d389415ed..f622c42bf7e8a 100644 --- a/posthog/temporal/data_imports/workflow_activities/import_data.py +++ b/posthog/temporal/data_imports/workflow_activities/import_data.py @@ -62,9 +62,13 @@ async def import_data_activity(inputs: ImportDataActivityInputs) -> tuple[TSchem if not stripe_secret_key: raise ValueError(f"Stripe secret key not found for job {model.id}") - # TODO: add in check_limit to rest_source source = stripe_source( - api_key=stripe_secret_key, account_id=account_id, endpoint=schema.name, is_incremental=schema.is_incremental + api_key=stripe_secret_key, + account_id=account_id, + endpoint=schema.name, + team_id=inputs.team_id, + job_id=inputs.run_id, + is_incremental=schema.is_incremental, ) return await _run(job_inputs=job_inputs, source=source, logger=logger, inputs=inputs, schema=schema) diff --git a/posthog/temporal/tests/external_data/test_external_data_job.py b/posthog/temporal/tests/external_data/test_external_data_job.py index 33363e24d5854..c05cf9c181dca 100644 --- a/posthog/temporal/tests/external_data/test_external_data_job.py +++ b/posthog/temporal/tests/external_data/test_external_data_job.py @@ -493,7 +493,7 @@ def mock_customers_paginate( assert len(job_1_customer_objects["Contents"]) == 1 await sync_to_async(job_1.refresh_from_db)() - assert job_1.rows_synced == 1 + assert job_1.rows_synced == 0 @pytest.mark.django_db(transaction=True) @@ -554,7 +554,6 @@ def mock_customers_paginate( with ( mock.patch.object(RESTClient, "paginate", mock_customers_paginate), - mock.patch("posthog.temporal.data_imports.pipelines.helpers.CHUNK_SIZE", 0), override_settings( BUCKET_URL=f"s3://{BUCKET_NAME}", AIRBYTE_BUCKET_KEY=settings.OBJECT_STORAGE_ACCESS_KEY_ID,