diff --git a/cmd/parca/main.go b/cmd/parca/main.go index 64aa5a33a20..0924f2d5036 100644 --- a/cmd/parca/main.go +++ b/cmd/parca/main.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/env-jsonnet.sh b/env-jsonnet.sh index 061d3eb8d13..c5b9c091f86 100755 --- a/env-jsonnet.sh +++ b/env-jsonnet.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright 2023-2025 The Parca Authors +# Copyright 2023-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/env-local-test.sh b/env-local-test.sh index 8e055147309..15f08fba7bb 100755 --- a/env-local-test.sh +++ b/env-local-test.sh @@ -1,5 +1,5 @@ #! /usr/bin/env bash -# Copyright 2023-2025 The Parca Authors +# Copyright 2023-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/env-proto.sh b/env-proto.sh index 2da60eafeff..965f2383e9b 100755 --- a/env-proto.sh +++ b/env-proto.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright 2023-2025 The Parca Authors +# Copyright 2023-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/env.sh b/env.sh index 4b411248015..594d1b44fb0 100755 --- a/env.sh +++ b/env.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright 2023-2025 The Parca Authors +# Copyright 2023-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/gen/proto/go/parca/query/v1alpha1/validate.go b/gen/proto/go/parca/query/v1alpha1/validate.go index abfb1565b32..ba35fa4dc2c 100644 --- a/gen/proto/go/parca/query/v1alpha1/validate.go +++ b/gen/proto/go/parca/query/v1alpha1/validate.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/badgerlogger/badgerlogger.go b/pkg/badgerlogger/badgerlogger.go index f68d8f8eff4..7405eed83a0 100644 --- a/pkg/badgerlogger/badgerlogger.go +++ b/pkg/badgerlogger/badgerlogger.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index eb93d3224fb..42848d3be17 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index 79d6ab2d996..062587bb5b0 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/cache_with_eviction.go b/pkg/cache/cache_with_eviction.go index b8bb5f9d4cd..40721f777e3 100644 --- a/pkg/cache/cache_with_eviction.go +++ b/pkg/cache/cache_with_eviction.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/cache_with_ttl.go b/pkg/cache/cache_with_ttl.go index 251b1c6e23d..b1c4b6685ae 100644 --- a/pkg/cache/cache_with_ttl.go +++ b/pkg/cache/cache_with_ttl.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/loading.go b/pkg/cache/loading.go index c9a7890a2db..4c21612eadc 100644 --- a/pkg/cache/loading.go +++ b/pkg/cache/loading.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/loading_test.go b/pkg/cache/loading_test.go index 1a9648ef237..b58b08f74bf 100644 --- a/pkg/cache/loading_test.go +++ b/pkg/cache/loading_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/lru/lru.go b/pkg/cache/lru/lru.go index d4c196972ba..28a4bb85001 100644 --- a/pkg/cache/lru/lru.go +++ b/pkg/cache/lru/lru.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/lru/lru_test.go b/pkg/cache/lru/lru_test.go index 4792b519f3d..bafe165e052 100644 --- a/pkg/cache/lru/lru_test.go +++ b/pkg/cache/lru/lru_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/lru/lru_with_eviction_test.go b/pkg/cache/lru/lru_with_eviction_test.go index 12d533343a5..5498acf01e9 100644 --- a/pkg/cache/lru/lru_with_eviction_test.go +++ b/pkg/cache/lru/lru_with_eviction_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/lru/metrics.go b/pkg/cache/lru/metrics.go index 9ca416c57ae..1b5e0f7934d 100644 --- a/pkg/cache/lru/metrics.go +++ b/pkg/cache/lru/metrics.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/lru/options.go b/pkg/cache/lru/options.go index acd9c4b138b..8e92dd2da0b 100644 --- a/pkg/cache/lru/options.go +++ b/pkg/cache/lru/options.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/cache/noop.go b/pkg/cache/noop.go index 803078de226..db53ee44f00 100644 --- a/pkg/cache/noop.go +++ b/pkg/cache/noop.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/compactdictionary/compactdictionary.go b/pkg/compactdictionary/compactdictionary.go index c4ab0c0767e..ca3e10c7415 100644 --- a/pkg/compactdictionary/compactdictionary.go +++ b/pkg/compactdictionary/compactdictionary.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/config/config.go b/pkg/config/config.go index c37927a0e86..ed2753e0241 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 7e257d0ee92..06c41ea45b8 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/config/reloader.go b/pkg/config/reloader.go index db56f7faecb..695995d439e 100644 --- a/pkg/config/reloader.go +++ b/pkg/config/reloader.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/config/reloader_test.go b/pkg/config/reloader_test.go index f22d59a4369..f1625de3e3b 100644 --- a/pkg/config/reloader_test.go +++ b/pkg/config/reloader_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/config/secret.go b/pkg/config/secret.go index d42b74b308b..9ca056b898c 100644 --- a/pkg/config/secret.go +++ b/pkg/config/secret.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/config/validation.go b/pkg/config/validation.go index 3dbbddf20ab..11fcab0aad9 100644 --- a/pkg/config/validation.go +++ b/pkg/config/validation.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/client.go b/pkg/debuginfo/client.go index 64265b20635..903411b3ba1 100644 --- a/pkg/debuginfo/client.go +++ b/pkg/debuginfo/client.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/debuginfod.go b/pkg/debuginfo/debuginfod.go index c383e1011f3..84a8fe5ee67 100644 --- a/pkg/debuginfo/debuginfod.go +++ b/pkg/debuginfo/debuginfod.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/debuginfod_test.go b/pkg/debuginfo/debuginfod_test.go index 858181a8a6c..14faa30af06 100644 --- a/pkg/debuginfo/debuginfod_test.go +++ b/pkg/debuginfo/debuginfod_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/fetcher.go b/pkg/debuginfo/fetcher.go index ed710fee269..fd0c0cbc37f 100644 --- a/pkg/debuginfo/fetcher.go +++ b/pkg/debuginfo/fetcher.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/forwarder.go b/pkg/debuginfo/forwarder.go index 731e1923c3c..fb73602cc4e 100644 --- a/pkg/debuginfo/forwarder.go +++ b/pkg/debuginfo/forwarder.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/metadata.go b/pkg/debuginfo/metadata.go index fe56c7faa12..7dfbc7a3ebc 100644 --- a/pkg/debuginfo/metadata.go +++ b/pkg/debuginfo/metadata.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/metadata_test.go b/pkg/debuginfo/metadata_test.go index b1fa53400b9..dc52f8ef944 100644 --- a/pkg/debuginfo/metadata_test.go +++ b/pkg/debuginfo/metadata_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/reader.go b/pkg/debuginfo/reader.go index 88be2e04c41..1fe73132604 100644 --- a/pkg/debuginfo/reader.go +++ b/pkg/debuginfo/reader.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/store.go b/pkg/debuginfo/store.go index 0cb99095eec..109232ecd37 100644 --- a/pkg/debuginfo/store.go +++ b/pkg/debuginfo/store.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/debuginfo/store_test.go b/pkg/debuginfo/store_test.go index 0e936135ea5..27495dd322f 100644 --- a/pkg/debuginfo/store_test.go +++ b/pkg/debuginfo/store_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/demangle/demangle.go b/pkg/demangle/demangle.go index bdafec153f2..ac62f56033f 100644 --- a/pkg/demangle/demangle.go +++ b/pkg/demangle/demangle.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/demangle/rust_test.go b/pkg/demangle/rust_test.go index 6e8d1f03964..c8e8c06cae5 100644 --- a/pkg/demangle/rust_test.go +++ b/pkg/demangle/rust_test.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go index 045a9130e77..f40f4de204c 100644 --- a/pkg/hash/hash.go +++ b/pkg/hash/hash.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/ingester/ingester.go b/pkg/ingester/ingester.go index 829b7aed12c..7a57ada9732 100644 --- a/pkg/ingester/ingester.go +++ b/pkg/ingester/ingester.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/ingester/ingester_test.go b/pkg/ingester/ingester_test.go index 785f8d84644..01c2bc9ebc9 100644 --- a/pkg/ingester/ingester_test.go +++ b/pkg/ingester/ingester_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/kv/keymaker.go b/pkg/kv/keymaker.go index d9663a54a61..8fc7f37035a 100644 --- a/pkg/kv/keymaker.go +++ b/pkg/kv/keymaker.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/normalizer/arrow.go b/pkg/normalizer/arrow.go index 10d15e1bbab..4f90b18402f 100644 --- a/pkg/normalizer/arrow.go +++ b/pkg/normalizer/arrow.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/normalizer/normalizer.go b/pkg/normalizer/normalizer.go index 031e0e6a876..70b17c0dd70 100644 --- a/pkg/normalizer/normalizer.go +++ b/pkg/normalizer/normalizer.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/normalizer/normalizer_test.go b/pkg/normalizer/normalizer_test.go index 1757abd48d9..3954f7d067f 100644 --- a/pkg/normalizer/normalizer_test.go +++ b/pkg/normalizer/normalizer_test.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/normalizer/otel.go b/pkg/normalizer/otel.go index 7578da582fd..dad12fb318f 100644 --- a/pkg/normalizer/otel.go +++ b/pkg/normalizer/otel.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/normalizer/validate.go b/pkg/normalizer/validate.go index 1383eb88c49..73755e8782b 100644 --- a/pkg/normalizer/validate.go +++ b/pkg/normalizer/validate.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/parca/logger.go b/pkg/parca/logger.go index 0de5f5ec0f2..8f52f3b5958 100644 --- a/pkg/parca/logger.go +++ b/pkg/parca/logger.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/parca/parca.go b/pkg/parca/parca.go index 15bb3dc7dae..41e436f8d95 100644 --- a/pkg/parca/parca.go +++ b/pkg/parca/parca.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/parca/parca_test.go b/pkg/parca/parca_test.go index 4cdcf5b81ba..51f734f944e 100644 --- a/pkg/parca/parca_test.go +++ b/pkg/parca/parca_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/parca/testdata/pgotest.go b/pkg/parca/testdata/pgotest.go index 30e24d51f94..6f4759c18ad 100644 --- a/pkg/parca/testdata/pgotest.go +++ b/pkg/parca/testdata/pgotest.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/parcacol/arrow.go b/pkg/parcacol/arrow.go index c0b47c2bffb..8ecc62d65ce 100644 --- a/pkg/parcacol/arrow.go +++ b/pkg/parcacol/arrow.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/parcacol/arrow_test.go b/pkg/parcacol/arrow_test.go index 4ca282c1f3b..e0aacd416bf 100644 --- a/pkg/parcacol/arrow_test.go +++ b/pkg/parcacol/arrow_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/parcacol/querier.go b/pkg/parcacol/querier.go index 3b80928ac58..a61608373eb 100644 --- a/pkg/parcacol/querier.go +++ b/pkg/parcacol/querier.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/decode.go b/pkg/profile/decode.go index 286368823f1..70e00b2e80a 100644 --- a/pkg/profile/decode.go +++ b/pkg/profile/decode.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/encode.go b/pkg/profile/encode.go index bc7add87ddc..7314e37c90a 100644 --- a/pkg/profile/encode.go +++ b/pkg/profile/encode.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/encode_test.go b/pkg/profile/encode_test.go index 23889db05f6..2cfddb2d5d6 100644 --- a/pkg/profile/encode_test.go +++ b/pkg/profile/encode_test.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/executableinfo.go b/pkg/profile/executableinfo.go index 1b9a503a72c..d689586b5f8 100644 --- a/pkg/profile/executableinfo.go +++ b/pkg/profile/executableinfo.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/profile.go b/pkg/profile/profile.go index ce3cc7ba547..c8b9407d5d4 100644 --- a/pkg/profile/profile.go +++ b/pkg/profile/profile.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/reader.go b/pkg/profile/reader.go index 5a881650f33..f986b37fd9b 100644 --- a/pkg/profile/reader.go +++ b/pkg/profile/reader.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/schema.go b/pkg/profile/schema.go index 445ecf67a6c..401017bdcd1 100644 --- a/pkg/profile/schema.go +++ b/pkg/profile/schema.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/uvarint.go b/pkg/profile/uvarint.go index cfa77093b18..218b81b72ec 100644 --- a/pkg/profile/uvarint.go +++ b/pkg/profile/uvarint.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profile/writer.go b/pkg/profile/writer.go index b9eb0f113d1..41e3aca1db1 100644 --- a/pkg/profile/writer.go +++ b/pkg/profile/writer.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profilestore/grpc.go b/pkg/profilestore/grpc.go index f303ee0c5dc..0b4dab84544 100644 --- a/pkg/profilestore/grpc.go +++ b/pkg/profilestore/grpc.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profilestore/profilecolumnstore.go b/pkg/profilestore/profilecolumnstore.go index 128cc5a5744..edc9019127d 100644 --- a/pkg/profilestore/profilecolumnstore.go +++ b/pkg/profilestore/profilecolumnstore.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/profilestore/profilestore_test.go b/pkg/profilestore/profilestore_test.go index 5e25ecb606a..731e9a5ecd9 100644 --- a/pkg/profilestore/profilestore_test.go +++ b/pkg/profilestore/profilestore_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/callgraph.go b/pkg/query/callgraph.go index 3570637e318..9ac15570a20 100644 --- a/pkg/query/callgraph.go +++ b/pkg/query/callgraph.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/callgraph_test.go b/pkg/query/callgraph_test.go index 6a67695567d..51c2e9c5589 100644 --- a/pkg/query/callgraph_test.go +++ b/pkg/query/callgraph_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/columnquery.go b/pkg/query/columnquery.go index b6ec1bd7aad..62715c5ba7f 100644 --- a/pkg/query/columnquery.go +++ b/pkg/query/columnquery.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/columnquery_test.go b/pkg/query/columnquery_test.go index 7d3fd02e276..c0cafee484d 100644 --- a/pkg/query/columnquery_test.go +++ b/pkg/query/columnquery_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/flamegraph.go b/pkg/query/flamegraph.go index 04b4371d137..5311d3fd524 100644 --- a/pkg/query/flamegraph.go +++ b/pkg/query/flamegraph.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/flamegraph_arrow.go b/pkg/query/flamegraph_arrow.go index 154d22d581f..902ffd6a955 100644 --- a/pkg/query/flamegraph_arrow.go +++ b/pkg/query/flamegraph_arrow.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/flamegraph_arrow_test.go b/pkg/query/flamegraph_arrow_test.go index 36c29c6877e..e2d07247302 100644 --- a/pkg/query/flamegraph_arrow_test.go +++ b/pkg/query/flamegraph_arrow_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/flamegraph_flat.go b/pkg/query/flamegraph_flat.go index b6633e24be4..7126b022703 100644 --- a/pkg/query/flamegraph_flat.go +++ b/pkg/query/flamegraph_flat.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/flamegraph_flat_test.go b/pkg/query/flamegraph_flat_test.go index 6979540f81a..b32742afb01 100644 --- a/pkg/query/flamegraph_flat_test.go +++ b/pkg/query/flamegraph_flat_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/flamegraph_table.go b/pkg/query/flamegraph_table.go index f8baa891d2b..2c687742c7e 100644 --- a/pkg/query/flamegraph_table.go +++ b/pkg/query/flamegraph_table.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/flamegraph_table_test.go b/pkg/query/flamegraph_table_test.go index 3c282023940..6b00c1d0752 100644 --- a/pkg/query/flamegraph_table_test.go +++ b/pkg/query/flamegraph_table_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/multiple_filters_test.go b/pkg/query/multiple_filters_test.go index a843f00ae01..9fef362a659 100644 --- a/pkg/query/multiple_filters_test.go +++ b/pkg/query/multiple_filters_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/pprof.go b/pkg/query/pprof.go index a847981f1f5..d1729e3e06c 100644 --- a/pkg/query/pprof.go +++ b/pkg/query/pprof.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/pprof_test.go b/pkg/query/pprof_test.go index 97237d2b7ea..fb353e11267 100644 --- a/pkg/query/pprof_test.go +++ b/pkg/query/pprof_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/query_test.go b/pkg/query/query_test.go index b01e0816819..361ea0c4db8 100644 --- a/pkg/query/query_test.go +++ b/pkg/query/query_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/sources.go b/pkg/query/sources.go index 89d529797c0..e0392f9aa25 100644 --- a/pkg/query/sources.go +++ b/pkg/query/sources.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/sources_reader.go b/pkg/query/sources_reader.go index 11b66ca6ec7..380e22f5ff6 100644 --- a/pkg/query/sources_reader.go +++ b/pkg/query/sources_reader.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/sources_reader_test.go b/pkg/query/sources_reader_test.go index 4b2c1224643..dbe1b6f479a 100644 --- a/pkg/query/sources_reader_test.go +++ b/pkg/query/sources_reader_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/sources_test.go b/pkg/query/sources_test.go index 1a197b0b15a..c2c87b24c72 100644 --- a/pkg/query/sources_test.go +++ b/pkg/query/sources_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/string_match_bench_test.go b/pkg/query/string_match_bench_test.go index dfa9771d5e7..68625866818 100644 --- a/pkg/query/string_match_bench_test.go +++ b/pkg/query/string_match_bench_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/table.go b/pkg/query/table.go index 9a9fea2696a..a7a70e4bbd5 100644 --- a/pkg/query/table.go +++ b/pkg/query/table.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/table_test.go b/pkg/query/table_test.go index b60a27ed60f..ac6dd52cf45 100644 --- a/pkg/query/table_test.go +++ b/pkg/query/table_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/top.go b/pkg/query/top.go index 3e28b563e03..f8c829f1abe 100644 --- a/pkg/query/top.go +++ b/pkg/query/top.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/query/top_test.go b/pkg/query/top_test.go index d34f9c02fbb..2c334fa5168 100644 --- a/pkg/query/top_test.go +++ b/pkg/query/top_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/runutil/runutil.go b/pkg/runutil/runutil.go index 6e4209362c8..bc4411585d5 100644 --- a/pkg/runutil/runutil.go +++ b/pkg/runutil/runutil.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/scrape/manager.go b/pkg/scrape/manager.go index 890cfce4e0c..49f70954091 100644 --- a/pkg/scrape/manager.go +++ b/pkg/scrape/manager.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/scrape/scrape.go b/pkg/scrape/scrape.go index 96110e2dfdf..08a4d10f019 100644 --- a/pkg/scrape/scrape.go +++ b/pkg/scrape/scrape.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/scrape/scrape_test.go b/pkg/scrape/scrape_test.go index 35ae1781433..1631b897a96 100644 --- a/pkg/scrape/scrape_test.go +++ b/pkg/scrape/scrape_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/scrape/service.go b/pkg/scrape/service.go index 7004e439a4a..05e135ee611 100644 --- a/pkg/scrape/service.go +++ b/pkg/scrape/service.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/scrape/target.go b/pkg/scrape/target.go index 942e90dace6..8a5919f39d6 100644 --- a/pkg/scrape/target.go +++ b/pkg/scrape/target.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/scrape/target_test.go b/pkg/scrape/target_test.go index 551e3df30e8..4b87c617355 100644 --- a/pkg/scrape/target_test.go +++ b/pkg/scrape/target_test.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/server/fallback.go b/pkg/server/fallback.go index 85a476765c9..654335370bb 100644 --- a/pkg/server/fallback.go +++ b/pkg/server/fallback.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/server/grpc_codec.go b/pkg/server/grpc_codec.go index f0808b39036..5bac48bf5b8 100644 --- a/pkg/server/grpc_codec.go +++ b/pkg/server/grpc_codec.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/server/server.go b/pkg/server/server.go index 951647e6fb2..09caba9a50e 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/signedrequests/client.go b/pkg/signedrequests/client.go index 085f2c64775..18eea5b091d 100644 --- a/pkg/signedrequests/client.go +++ b/pkg/signedrequests/client.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/signedrequests/gcs.go b/pkg/signedrequests/gcs.go index 20a9e80fb6d..d0c0ba6fb83 100644 --- a/pkg/signedrequests/gcs.go +++ b/pkg/signedrequests/gcs.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/signedrequests/s3.go b/pkg/signedrequests/s3.go index 08fca9916df..cc6d0cf238e 100644 --- a/pkg/signedrequests/s3.go +++ b/pkg/signedrequests/s3.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/addr2line/doc.go b/pkg/symbol/addr2line/doc.go index 1e94389712a..2255cadbdca 100644 --- a/pkg/symbol/addr2line/doc.go +++ b/pkg/symbol/addr2line/doc.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/addr2line/dwarf.go b/pkg/symbol/addr2line/dwarf.go index 8e839c2041e..d9e655a26c8 100644 --- a/pkg/symbol/addr2line/dwarf.go +++ b/pkg/symbol/addr2line/dwarf.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/addr2line/dwarf_test.go b/pkg/symbol/addr2line/dwarf_test.go index 89f966bb876..cc5ebfb7cc2 100644 --- a/pkg/symbol/addr2line/dwarf_test.go +++ b/pkg/symbol/addr2line/dwarf_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/addr2line/go.go b/pkg/symbol/addr2line/go.go index a10708fca30..3eaff58eef2 100644 --- a/pkg/symbol/addr2line/go.go +++ b/pkg/symbol/addr2line/go.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/addr2line/symtab.go b/pkg/symbol/addr2line/symtab.go index f6c692f8761..637c8b2ec1f 100644 --- a/pkg/symbol/addr2line/symtab.go +++ b/pkg/symbol/addr2line/symtab.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/addr2line/symtab_test.go b/pkg/symbol/addr2line/symtab_test.go index bb403bbaa41..574533a44d7 100644 --- a/pkg/symbol/addr2line/symtab_test.go +++ b/pkg/symbol/addr2line/symtab_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/demangle/demangle.go b/pkg/symbol/demangle/demangle.go index 024ff2b2411..316b51ebd6d 100644 --- a/pkg/symbol/demangle/demangle.go +++ b/pkg/symbol/demangle/demangle.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/demangle/demangle_test.go b/pkg/symbol/demangle/demangle_test.go index 111e9f45071..29f05ccceba 100644 --- a/pkg/symbol/demangle/demangle_test.go +++ b/pkg/symbol/demangle/demangle_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/elfutils/debuginfofile.go b/pkg/symbol/elfutils/debuginfofile.go index e95960f965f..6591e7ae547 100644 --- a/pkg/symbol/elfutils/debuginfofile.go +++ b/pkg/symbol/elfutils/debuginfofile.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/elfutils/elfutils.go b/pkg/symbol/elfutils/elfutils.go index d53e6e624b9..de4dc2d99d9 100644 --- a/pkg/symbol/elfutils/elfutils.go +++ b/pkg/symbol/elfutils/elfutils.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/elfutils/elfutils_test.go b/pkg/symbol/elfutils/elfutils_test.go index 52d54ece721..bf1dc2509d6 100644 --- a/pkg/symbol/elfutils/elfutils_test.go +++ b/pkg/symbol/elfutils/elfutils_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/elfutils/testdata/main.go b/pkg/symbol/elfutils/testdata/main.go index 14848c6a9e0..155b59076f3 100644 --- a/pkg/symbol/elfutils/testdata/main.go +++ b/pkg/symbol/elfutils/testdata/main.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbol/symbolsearcher/symbol_searcher.go b/pkg/symbol/symbolsearcher/symbol_searcher.go index d7e32c39f73..2531e0feb4a 100644 --- a/pkg/symbol/symbolsearcher/symbol_searcher.go +++ b/pkg/symbol/symbolsearcher/symbol_searcher.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbolizer/cache.go b/pkg/symbolizer/cache.go index 2a9caac10c6..f9a0527d12c 100644 --- a/pkg/symbolizer/cache.go +++ b/pkg/symbolizer/cache.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbolizer/decode.go b/pkg/symbolizer/decode.go index 8ca6575c90e..879d6012377 100644 --- a/pkg/symbolizer/decode.go +++ b/pkg/symbolizer/decode.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbolizer/encode.go b/pkg/symbolizer/encode.go index adf3caea09f..571a8399c9e 100644 --- a/pkg/symbolizer/encode.go +++ b/pkg/symbolizer/encode.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbolizer/encode_test.go b/pkg/symbolizer/encode_test.go index a386f8a6b5e..c3abeab0dc0 100644 --- a/pkg/symbolizer/encode_test.go +++ b/pkg/symbolizer/encode_test.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbolizer/normalize.go b/pkg/symbolizer/normalize.go index f272f63a694..3fa081f1d4e 100644 --- a/pkg/symbolizer/normalize.go +++ b/pkg/symbolizer/normalize.go @@ -1,4 +1,4 @@ -// Copyright 2024-2025 The Parca Authors +// Copyright 2024-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbolizer/symbolizer.go b/pkg/symbolizer/symbolizer.go index 5fa55b47b08..72a2ae69321 100644 --- a/pkg/symbolizer/symbolizer.go +++ b/pkg/symbolizer/symbolizer.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/symbolizer/symbolizer_test.go b/pkg/symbolizer/symbolizer_test.go index c4f7104f682..a2559d671fa 100644 --- a/pkg/symbolizer/symbolizer_test.go +++ b/pkg/symbolizer/symbolizer_test.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 6119373ad4f..38092ddaf11 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/pkg/tracer/tracer.go b/pkg/tracer/tracer.go index 8388cc4d389..b28befb243f 100644 --- a/pkg/tracer/tracer.go +++ b/pkg/tracer/tracer.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/proto/buf.lock b/proto/buf.lock index c938ccdf750..9ae79671931 100644 --- a/proto/buf.lock +++ b/proto/buf.lock @@ -9,5 +9,5 @@ deps: - remote: buf.build owner: grpc-ecosystem repository: grpc-gateway - commit: 4c5ba75caaf84e928b7137ae5c18c26a - digest: shake256:e174ad9408f3e608f6157907153ffec8d310783ee354f821f57178ffbeeb8faa6bb70b41b61099c1783c82fe16210ebd1279bc9c9ee6da5cffba9f0e675b8b99 + commit: 6467306b4f624747aaf6266762ee7a1c + digest: shake256:833d648b99b9d2c18b6882ef41aaeb113e76fc38de20dda810c588d133846e6593b4da71b388bcd921b1c7ab41c7acf8f106663d7301ae9e82ceab22cf64b1b7 diff --git a/scripts/check-license.sh b/scripts/check-license.sh index a7c4ef0e69b..14789a0fb3d 100755 --- a/scripts/check-license.sh +++ b/scripts/check-license.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Copyright 2022-2025 The Parca Authors +# Copyright 2022-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/scripts/free_disk_space.sh b/scripts/free_disk_space.sh index 963635b5fe3..dadbbaedeaa 100755 --- a/scripts/free_disk_space.sh +++ b/scripts/free_disk_space.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Copyright 2024-2025 The Parca Authors +# Copyright 2024-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/scripts/install-minikube.sh b/scripts/install-minikube.sh index dc2b6664b9f..b55a90d3f6b 100755 --- a/scripts/install-minikube.sh +++ b/scripts/install-minikube.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Copyright 2022-2025 The Parca Authors +# Copyright 2022-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/scripts/local-dev.sh b/scripts/local-dev.sh index ce80d1349bd..39d752ab6d2 100644 --- a/scripts/local-dev.sh +++ b/scripts/local-dev.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2022-2025 The Parca Authors +# Copyright 2022-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/snap/hooks/configure b/snap/hooks/configure index 678ba872a9a..9eb239e98e8 100755 --- a/snap/hooks/configure +++ b/snap/hooks/configure @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2022-2025 The Parca Authors +# Copyright 2022-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/snap/parca-wrapper b/snap/parca-wrapper index 31dcb706461..250097772c6 100755 --- a/snap/parca-wrapper +++ b/snap/parca-wrapper @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2022-2025 The Parca Authors +# Copyright 2022-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/ui/packages/app/web/build/keep.go b/ui/packages/app/web/build/keep.go index 4917a45cd2b..b00a4b0bdbc 100644 --- a/ui/packages/app/web/build/keep.go +++ b/ui/packages/app/web/build/keep.go @@ -1,4 +1,4 @@ -// Copyright 2023-2025 The Parca Authors +// Copyright 2023-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at diff --git a/ui/packages/app/web/public/keep.go b/ui/packages/app/web/public/keep.go index 4917a45cd2b..03db4372b36 100644 --- a/ui/packages/app/web/public/keep.go +++ b/ui/packages/app/web/public/keep.go @@ -1,4 +1,6 @@ -// Copyright 2023-2025 The Parca Authors +#!/bin/bash + +// Copyright 2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -11,6 +13,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package build +# Copyright 2022-2026 The Parca Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -// this file is empty and only exists so that the embed.FS variable is populated. +pnpm run build +mkdir PATH_PREFIX_VAR +cp -r ./build/* ./PATH_PREFIX_VAR/ +mv ./PATH_PREFIX_VAR ./build/ diff --git a/ui/packages/app/web/scripts/build-preview.sh b/ui/packages/app/web/scripts/build-preview.sh index 20bf5b8926a..e7156f1bdc1 100755 --- a/ui/packages/app/web/scripts/build-preview.sh +++ b/ui/packages/app/web/scripts/build-preview.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2022-2025 The Parca Authors +# Copyright 2022-2026 The Parca Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/ui/packages/shared/components/src/ParcaContext/index.tsx b/ui/packages/shared/components/src/ParcaContext/index.tsx index 6ea59f6b55a..0830ef3f697 100644 --- a/ui/packages/shared/components/src/ParcaContext/index.tsx +++ b/ui/packages/shared/components/src/ParcaContext/index.tsx @@ -68,6 +68,7 @@ interface Props { disableProfileTypesDropdown?: boolean; labelnames?: string[]; disableExplorativeQuerying?: boolean; + profileFilterDefaults?: unknown[]; }; profileViewExternalMainActions?: ReactNode; profileViewExternalSubActions?: ReactNode; diff --git a/ui/packages/shared/components/src/hooks/URLState/index.test.tsx b/ui/packages/shared/components/src/hooks/URLState/index.test.tsx index 433717c9779..c2dff884fe7 100644 --- a/ui/packages/shared/components/src/hooks/URLState/index.test.tsx +++ b/ui/packages/shared/components/src/hooks/URLState/index.test.tsx @@ -11,10 +11,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ReactNode} from 'react'; +import {ReactNode, act} from 'react'; // eslint-disable-next-line import/named -import {act, renderHook, waitFor} from '@testing-library/react'; +import {renderHook, waitFor} from '@testing-library/react'; import {beforeEach, describe, expect, it, vi} from 'vitest'; import { @@ -647,4 +647,121 @@ describe('URLState Hooks', () => { consoleSpy.mockRestore(); }); }); + + describe('mergeStrategy option', () => { + it('should replace existing value by default', async () => { + const {result} = renderHook(() => useURLState('param'), {wrapper: createWrapper()}); + const [, setParam] = result.current; + + act(() => { + setParam('initial'); + }); + await waitFor(() => { + expect(result.current[0]).toBe('initial'); + }); + + act(() => { + setParam('replaced'); + }); + await waitFor(() => { + expect(result.current[0]).toBe('replaced'); + }); + }); + + it('should only set value when empty with preserve-existing strategy', async () => { + const {result} = renderHook( + () => useURLState('param', {mergeStrategy: 'preserve-existing'}), + {wrapper: createWrapper()} + ); + const [, setParam] = result.current; + + // Set when undefined - should work + act(() => { + setParam('first'); + }); + await waitFor(() => { + expect(result.current[0]).toBe('first'); + }); + + // Try to overwrite - should be ignored + act(() => { + setParam('second'); + }); + await waitFor(() => { + expect(result.current[0]).toBe('first'); + }); + }); + + it('should merge arrays with deduplication using append strategy', async () => { + const {result} = renderHook( + () => useURLState('param', {mergeStrategy: 'append'}), + {wrapper: createWrapper()} + ); + const [, setParam] = result.current; + + // Set initial array + act(() => { + setParam(['a', 'b']); + }); + await waitFor(() => { + expect(result.current[0]).toEqual(['a', 'b']); + }); + + // Append with overlap - should deduplicate + act(() => { + setParam(['b', 'c']); + }); + await waitFor(() => { + expect(result.current[0]).toEqual(['a', 'b', 'c']); + }); + + // Append string to array + act(() => { + setParam('d'); + }); + await waitFor(() => { + expect(result.current[0]).toEqual(['a', 'b', 'c', 'd']); + }); + + // Append duplicate string - should be ignored + act(() => { + setParam('a'); + }); + await waitFor(() => { + expect(result.current[0]).toEqual(['a', 'b', 'c', 'd']); + }); + }); + + it('should return undefined and no-op setter when enabled is false', async () => { + const {result} = renderHook(() => useURLState('param', {enabled: false}), { + wrapper: createWrapper(), + }); + + const [value, setParam] = result.current; + expect(value).toBeUndefined(); + + act(() => { + setParam('should-not-work'); + }); + await waitFor(() => { + expect(mockNavigateTo).not.toHaveBeenCalled(); + }); + }); + + it('should handle compare mode with enabled option', async () => { + const TestComponent = (): { + groupByA: string | string[] | undefined; + groupByB: string | string[] | undefined; + } => { + const [groupByA] = useURLState('group_by', {enabled: true, defaultValue: ['node']}); + const [groupByB] = useURLState('group_by', {enabled: false}); + return {groupByA, groupByB}; + }; + + const {result} = renderHook(() => TestComponent(), {wrapper: createWrapper()}); + + expect(result.current.groupByA).toEqual(['node']); + expect(result.current.groupByB).toBeUndefined(); + }); + }); }); diff --git a/ui/packages/shared/components/src/hooks/URLState/index.tsx b/ui/packages/shared/components/src/hooks/URLState/index.tsx index d35e76c297e..e4d8f2b1673 100644 --- a/ui/packages/shared/components/src/hooks/URLState/index.tsx +++ b/ui/packages/shared/components/src/hooks/URLState/index.tsx @@ -210,6 +210,8 @@ interface Options { defaultValue?: string | string[]; debugLog?: boolean; alwaysReturnArray?: boolean; + mergeStrategy?: 'replace' | 'append' | 'preserve-existing'; + enabled?: boolean; } export const useURLState = ( @@ -221,10 +223,13 @@ export const useURLState = ( throw new Error('useURLState must be used within a URLStateProvider'); } - const {debugLog, defaultValue, alwaysReturnArray} = _options ?? {}; + const {debugLog, defaultValue, alwaysReturnArray, mergeStrategy, enabled} = _options ?? {}; const {state, setState} = context; + // Create no-op setter unconditionally to satisfy hooks rules + const noOpSetter = useCallback(() => {}, []); + const setParam: ParamValueSetter = useCallback( (val: ParamValue) => { if (debugLog === true) { @@ -232,12 +237,58 @@ export const useURLState = ( } // Just update state - Provider handles URL sync automatically! - setState(currentState => ({ - ...currentState, - [param]: val, - })); + setState(currentState => { + const currentValue = currentState[param]; + let newValue: ParamValue; + + if (mergeStrategy === undefined || mergeStrategy === 'replace') { + newValue = val; + } else if (mergeStrategy === 'preserve-existing') { + // Only set if current is empty (including empty string) + if ( + currentValue === undefined || + currentValue === null || + currentValue === '' || + (Array.isArray(currentValue) && currentValue.length === 0) + ) { + newValue = val; + } else { + newValue = currentValue; // Keep existing + } + } else if (mergeStrategy === 'append') { + // Ignore undefined/null new values - keep current state + if (val === undefined || val === null) { + newValue = currentValue; + } else if (currentValue === undefined || currentValue === null || currentValue === '') { + // Current is empty, use new value + newValue = val; + } else if (Array.isArray(currentValue) && Array.isArray(val)) { + // Merge arrays and deduplicate + newValue = Array.from(new Set([...currentValue, ...val])); + } else if (Array.isArray(currentValue) && typeof val === 'string') { + // Add string to array if not present (deduplication) + newValue = currentValue.includes(val) ? currentValue : [...currentValue, val]; + } else if (typeof currentValue === 'string' && Array.isArray(val)) { + // Merge string with array and deduplicate + newValue = Array.from(new Set([currentValue, ...val])); + } else if (typeof currentValue === 'string' && typeof val === 'string') { + // Create array from both strings (deduplicate) + newValue = currentValue === val ? currentValue : [currentValue, val]; + } else { + // Fallback to replace for other cases + newValue = val; + } + } else { + newValue = val; + } + + return { + ...currentState, + [param]: newValue, + }; + }); }, - [param, setState, debugLog] + [param, setState, debugLog, mergeStrategy] ); if (debugLog === true) { @@ -293,6 +344,11 @@ export const useURLState = ( } } + // Return early if hook is disabled (after all hooks have been called) + if (enabled === false) { + return [undefined as T, noOpSetter]; + } + return [(value ?? defaultValue) as T, setParam]; }; diff --git a/ui/packages/shared/profile/src/ProfileSelector/index.tsx b/ui/packages/shared/profile/src/ProfileSelector/index.tsx index 92d63b574a3..6abb99ee94c 100644 --- a/ui/packages/shared/profile/src/ProfileSelector/index.tsx +++ b/ui/packages/shared/profile/src/ProfileSelector/index.tsx @@ -30,6 +30,10 @@ import {TEST_IDS, testId} from '@parca/test-utils'; import {millisToProtoTimestamp, type NavigateFunction} from '@parca/utilities'; import {useMetricsGraphDimensions} from '../MetricsGraph/useMetricsGraphDimensions'; +import { + ProfileFilter, + useProfileFilters, +} from '../ProfileView/components/ProfileFilters/useProfileFilters'; import {QueryControls} from '../QueryControls'; import {LabelsQueryProvider, useLabelsQueryProvider} from '../contexts/LabelsQueryProvider'; import {UnifiedLabelsProvider} from '../contexts/UnifiedLabelsContext'; @@ -119,6 +123,17 @@ const ProfileSelector = ({ const [queryBrowserMode, setQueryBrowserMode] = useURLState('query_browser_mode'); const batchUpdates = useURLStateBatch(); + const profileFilterDefaults = viewComponent?.profileFilterDefaults as ProfileFilter[] | undefined; + const {forceApplyFilters} = useProfileFilters({ + viewDefaults: profileFilterDefaults, + }); + + const handleProfileTypeChange = useCallback(() => { + if (profileFilterDefaults != null && profileFilterDefaults.length > 0) { + forceApplyFilters(profileFilterDefaults); + } + }, [forceApplyFilters, profileFilterDefaults]); + // Use the new useQueryState hook - reads directly from URL params const { querySelection, @@ -133,7 +148,7 @@ const ProfileSelector = ({ setProfileSelection, sumByLoading, draftParsedQuery, - } = useQueryState({suffix}); + } = useQueryState({suffix, onProfileTypeChange: handleProfileTypeChange}); // Use draft state for local state instead of committed state const [timeRangeSelection, setTimeRangeSelection] = useState( diff --git a/ui/packages/shared/profile/src/ProfileSelector/useAutoQuerySelector.ts b/ui/packages/shared/profile/src/ProfileSelector/useAutoQuerySelector.ts index c085840c900..7ce8dcf5c04 100644 --- a/ui/packages/shared/profile/src/ProfileSelector/useAutoQuerySelector.ts +++ b/ui/packages/shared/profile/src/ProfileSelector/useAutoQuerySelector.ts @@ -137,7 +137,7 @@ export const useAutoQuerySelector = ({ } dispatch(setAutoQuery('true')); let profileType = profileTypesData.types.find( - type => type.name === 'parca_agent' && type.delta + type => type.name === 'parca_agent' && type.sampleType === 'samples' && type.delta ); if (profileType == null) { profileType = profileTypesData.types.find( diff --git a/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFilters.ts b/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFilters.ts index 50b8f471c71..e7dce19d5db 100644 --- a/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFilters.ts +++ b/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFilters.ts @@ -225,7 +225,13 @@ export const convertToProtoFilters = (profileFilters: ProfileFilter[]): Filter[] }); }; -export const useProfileFilters = (): { +interface UseProfileFiltersOptions { + viewDefaults?: ProfileFilter[]; +} + +export const useProfileFilters = ( + options: UseProfileFiltersOptions = {} +): { localFilters: ProfileFilter[]; appliedFilters: ProfileFilter[]; protoFilters: Filter[]; @@ -237,8 +243,14 @@ export const useProfileFilters = (): { removeFilter: (id: string) => void; updateFilter: (id: string, updates: Partial) => void; resetFilters: () => void; + applyViewDefaults: () => void; + forceApplyFilters: (filters: ProfileFilter[]) => void; } => { - const {appliedFilters, setAppliedFilters} = useProfileFiltersUrlState(); + const {viewDefaults} = options; + const {appliedFilters, setAppliedFilters, applyViewDefaults, forceApplyFilters} = + useProfileFiltersUrlState({ + viewDefaults, + }); const resetFlameGraphState = useResetFlameGraphState(); const [localFilters, setLocalFilters] = useState(appliedFilters ?? []); @@ -422,5 +434,7 @@ export const useProfileFilters = (): { removeFilter, updateFilter, resetFilters, + applyViewDefaults, + forceApplyFilters, }; }; diff --git a/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx b/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx new file mode 100644 index 00000000000..23464559229 --- /dev/null +++ b/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx @@ -0,0 +1,884 @@ +// Copyright 2022 The Parca Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {type ReactNode} from 'react'; + +// eslint-disable-next-line import/named +import {act, renderHook, waitFor} from '@testing-library/react'; +import {beforeEach, describe, expect, it, vi} from 'vitest'; + +import {URLStateProvider} from '@parca/components'; + +import {type ProfileFilter} from './useProfileFilters'; +import {decodeProfileFilters, useProfileFiltersUrlState} from './useProfileFiltersUrlState'; + +// Mock window.location +const mockLocation = { + pathname: '/test', + search: '', +}; + +// Mock the navigate function +const mockNavigateTo = vi.fn((path: string, params: Record) => { + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + if (Array.isArray(value)) { + searchParams.set(key, value.join(',')); + } else { + searchParams.set(key, String(value)); + } + } + }); + mockLocation.search = `?${searchParams.toString()}`; +}); + +// Mock getQueryParamsFromURL +vi.mock('@parca/components/src/hooks/URLState/utils', async () => { + const actual = await vi.importActual('@parca/components/src/hooks/URLState/utils'); + return { + ...actual, + getQueryParamsFromURL: () => { + if (mockLocation.search === '') return {}; + const params = new URLSearchParams(mockLocation.search); + const result: Record = {}; + for (const [key, value] of params.entries()) { + const decodedValue = decodeURIComponent(value); + const existing = result[key]; + if (existing !== undefined) { + result[key] = Array.isArray(existing) + ? [...existing, decodedValue] + : [existing, decodedValue]; + } else { + result[key] = decodedValue; + } + } + return result; + }, + }; +}); + +// Helper to create wrapper with URLStateProvider +const createWrapper = (): (({children}: {children: ReactNode}) => JSX.Element) => { + const Wrapper = ({children}: {children: ReactNode}): JSX.Element => ( + {children} + ); + Wrapper.displayName = 'URLStateProviderWrapper'; + return Wrapper; +}; + +describe('useProfileFiltersUrlState', () => { + beforeEach(() => { + mockNavigateTo.mockClear(); + Object.defineProperty(window, 'location', { + value: mockLocation, + writable: true, + }); + mockLocation.search = ''; + }); + + describe('decodeProfileFilters', () => { + it('should return empty array for empty string', () => { + expect(decodeProfileFilters('')).toEqual([]); + }); + + it('should return empty array for undefined', () => { + expect(decodeProfileFilters(undefined as unknown as string)).toEqual([]); + }); + + it('should decode stack filter with function_name', () => { + // Format: type:field:match:value -> s:fn:=:testFunc + const encoded = 's:fn:=:testFunc'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'stack', + field: 'function_name', + matchType: 'equal', + value: 'testFunc', + }); + }); + + it('should decode frame filter with binary', () => { + const encoded = 'f:b:!=:libc.so'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'frame', + field: 'binary', + matchType: 'not_equal', + value: 'libc.so', + }); + }); + + it('should decode filter with contains match', () => { + const encoded = 's:fn:~:runtime'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'stack', + field: 'function_name', + matchType: 'contains', + value: 'runtime', + }); + }); + + it('should decode filter with not_contains match', () => { + const encoded = 'f:b:!~:node'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'node', + }); + }); + + it('should decode filter with starts_with match', () => { + const encoded = 's:fn:^:std::'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'stack', + field: 'function_name', + matchType: 'starts_with', + value: 'std::', + }); + }); + + it('should decode filter with not_starts_with match', () => { + const encoded = 'f:fn:!^:tokio::'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'frame', + field: 'function_name', + matchType: 'not_starts_with', + value: 'tokio::', + }); + }); + + it('should decode multiple filters', () => { + const encoded = 's:fn:=:testFunc,f:b:!=:libc.so'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + type: 'stack', + field: 'function_name', + matchType: 'equal', + value: 'testFunc', + }); + expect(result[1]).toMatchObject({ + type: 'frame', + field: 'binary', + matchType: 'not_equal', + value: 'libc.so', + }); + }); + + it('should decode preset filter', () => { + const encoded = 'p:hide_libc:enabled'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'hide_libc', + value: 'enabled', + }); + }); + + it('should handle values with colons', () => { + const encoded = 'p:some_preset:value:with:colons'; + const result = decodeProfileFilters(encoded); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + type: 'some_preset', + value: 'value:with:colons', + }); + }); + + it('should decode all field types', () => { + const testCases = [ + {encoded: 's:fn:=:test', expectedField: 'function_name'}, + {encoded: 's:b:=:test', expectedField: 'binary'}, + {encoded: 's:sn:=:test', expectedField: 'system_name'}, + {encoded: 's:f:=:test', expectedField: 'filename'}, + {encoded: 's:a:=:test', expectedField: 'address'}, + {encoded: 's:ln:=:test', expectedField: 'line_number'}, + ]; + + for (const {encoded, expectedField} of testCases) { + const result = decodeProfileFilters(encoded); + expect(result[0].field).toBe(expectedField); + } + }); + + it('should return empty array for malformed input', () => { + // This should not throw - it returns empty array on error + expect(() => decodeProfileFilters('malformed')).not.toThrow(); + }); + + it('should generate unique IDs for each filter', () => { + const encoded = 's:fn:=:func1,s:fn:=:func2,s:fn:=:func3'; + const result = decodeProfileFilters(encoded); + + const ids = result.map(f => f.id); + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(ids.length); + }); + }); + + describe('Basic functionality', () => { + it('should initialize with empty filters when no URL params', () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + expect(result.current.appliedFilters).toEqual([]); + }); + + it('should read filters from URL', async () => { + mockLocation.search = '?profile_filters=s:fn:=:testFunc'; + + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + expect(result.current.appliedFilters[0]).toMatchObject({ + type: 'stack', + field: 'function_name', + matchType: 'equal', + value: 'testFunc', + }); + }); + }); + + it('should update URL when setting filters', async () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + const newFilters: ProfileFilter[] = [ + { + id: 'test-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'libc.so', + }, + ]; + + act(() => { + result.current.setAppliedFilters(newFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.profile_filters).toBe('f:b:!~:libc.so'); + }); + }); + + it('should clear URL param when setting empty filters', async () => { + mockLocation.search = '?profile_filters=s:fn:=:testFunc'; + + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + act(() => { + result.current.setAppliedFilters([]); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // When filters are empty, the param is either empty string or undefined (removed) + expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true); + }); + }); + }); + + describe('View defaults', () => { + it('should provide applyViewDefaults method', () => { + const viewDefaults: ProfileFilter[] = [ + { + id: 'default-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'libc.so', + }, + ]; + + const {result} = renderHook(() => useProfileFiltersUrlState({viewDefaults}), { + wrapper: createWrapper(), + }); + + expect(typeof result.current.applyViewDefaults).toBe('function'); + }); + + it('should apply view defaults to empty URL', async () => { + const viewDefaults: ProfileFilter[] = [ + { + id: 'default-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'libc.so', + }, + ]; + + const {result} = renderHook(() => useProfileFiltersUrlState({viewDefaults}), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.applyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.profile_filters).toBe('f:b:!~:libc.so'); + }); + }); + + it('should not overwrite existing filters when applying view defaults (preserve-existing)', async () => { + mockLocation.search = '?profile_filters=s:fn:=:existingFunc'; + + const viewDefaults: ProfileFilter[] = [ + { + id: 'default-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'libc.so', + }, + ]; + + const {result} = renderHook(() => useProfileFiltersUrlState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Verify existing filter is loaded + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + expect(result.current.appliedFilters[0].value).toBe('existingFunc'); + }); + + mockNavigateTo.mockClear(); + + act(() => { + result.current.applyViewDefaults(); + }); + + // With preserve-existing strategy, the existing value should be preserved + await waitFor(() => { + // Either no navigation (because value already exists) or value is preserved + if (mockNavigateTo.mock.calls.length > 0) { + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // The existing filter should be preserved + expect(params.profile_filters).toBe('s:fn:=:existingFunc'); + } + }); + }); + + it('should do nothing when viewDefaults is undefined', async () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + mockNavigateTo.mockClear(); + + act(() => { + result.current.applyViewDefaults(); + }); + + // Should not navigate since there are no defaults to apply + expect(mockNavigateTo).not.toHaveBeenCalled(); + }); + + it('should do nothing when viewDefaults is empty array', async () => { + const {result} = renderHook(() => useProfileFiltersUrlState({viewDefaults: []}), { + wrapper: createWrapper(), + }); + + mockNavigateTo.mockClear(); + + act(() => { + result.current.applyViewDefaults(); + }); + + // Should not navigate since defaults array is empty + expect(mockNavigateTo).not.toHaveBeenCalled(); + }); + }); + + describe('forceApplyFilters', () => { + it('should provide forceApplyFilters method', () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + expect(typeof result.current.forceApplyFilters).toBe('function'); + }); + + it('should force apply filters overwriting existing', async () => { + mockLocation.search = '?profile_filters=s:fn:=:existingFunc'; + + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + // Verify existing filter is loaded + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + }); + + const newFilters: ProfileFilter[] = [ + { + id: 'forced-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'forcedValue', + }, + ]; + + act(() => { + result.current.forceApplyFilters(newFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.profile_filters).toBe('f:b:!~:forcedValue'); + }); + }); + + it('should clear filters when force applying empty array', async () => { + mockLocation.search = '?profile_filters=s:fn:=:existingFunc'; + + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + act(() => { + result.current.forceApplyFilters([]); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // When filters are empty, the param is either empty string or undefined (removed) + expect(params.profile_filters === '' || params.profile_filters === undefined).toBe(true); + }); + }); + }); + + describe('Preset filter encoding', () => { + it('should encode preset filters correctly', async () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + const presetFilters: ProfileFilter[] = [ + { + id: 'preset-1', + type: 'hide_libc', + value: 'enabled', + }, + ]; + + act(() => { + result.current.setAppliedFilters(presetFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.profile_filters).toBe('p:hide_libc:enabled'); + }); + }); + + it('should handle mixed preset and regular filters', async () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + const mixedFilters: ProfileFilter[] = [ + { + id: 'preset-1', + type: 'hide_libc', + value: 'enabled', + }, + { + id: 'regular-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'node', + }, + ]; + + act(() => { + result.current.setAppliedFilters(mixedFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.profile_filters).toBe('p:hide_libc:enabled,f:b:!~:node'); + }); + }); + }); + + describe('URL encoding edge cases', () => { + it('should handle special characters in filter values', async () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + const filtersWithSpecialChars: ProfileFilter[] = [ + { + id: 'special-1', + type: 'stack', + field: 'function_name', + matchType: 'contains', + value: 'std::vector', + }, + ]; + + act(() => { + result.current.setAppliedFilters(filtersWithSpecialChars); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Value should be URL encoded + expect(params.profile_filters).toContain('std%3A%3Avector%3Cint%3E'); + }); + }); + + it('should filter out incomplete filters when encoding', async () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + const incompleteFilters: ProfileFilter[] = [ + { + id: 'complete-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'valid', + }, + { + id: 'incomplete-1', + type: 'frame', + // Missing field, matchType + value: '', + }, + { + id: 'incomplete-2', + type: undefined, + value: 'value', + }, + ]; + + act(() => { + result.current.setAppliedFilters(incompleteFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Only the complete filter should be encoded + expect(params.profile_filters).toBe('f:b:!~:valid'); + }); + }); + }); + + describe('Memoization', () => { + it('should return empty array with consistent structure when no filters', () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + // Empty filters should be an empty array (not undefined or null) + expect(Array.isArray(result.current.appliedFilters)).toBe(true); + expect(result.current.appliedFilters).toHaveLength(0); + }); + + it('should always return array (never undefined)', () => { + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + expect(Array.isArray(result.current.appliedFilters)).toBe(true); + expect(result.current.appliedFilters).toEqual([]); + }); + + it('should return correctly structured filters from URL', async () => { + mockLocation.search = '?profile_filters=s:fn:=:testFunc'; + + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + }); + + // Verify the filter structure is correct + const filter = result.current.appliedFilters[0]; + expect(filter).toHaveProperty('id'); + expect(filter).toHaveProperty('type', 'stack'); + expect(filter).toHaveProperty('field', 'function_name'); + expect(filter).toHaveProperty('matchType', 'equal'); + expect(filter).toHaveProperty('value', 'testFunc'); + }); + }); + + describe('View switching scenarios', () => { + it('should completely replace filters when switching views using forceApplyFilters', async () => { + // Start with View A's filters (2 filters) + mockLocation.search = '?profile_filters=s:fn:=:viewAFunc,f:b:!=:viewABinary'; + + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(2); + expect(result.current.appliedFilters[0].value).toBe('viewAFunc'); + expect(result.current.appliedFilters[1].value).toBe('viewABinary'); + }); + + // Switch to View B (completely different filter) + const viewBFilters: ProfileFilter[] = [ + { + id: 'viewB-1', + type: 'frame', + field: 'function_name', + matchType: 'contains', + value: 'viewBOnly', + }, + ]; + + act(() => { + result.current.forceApplyFilters(viewBFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + + // View A's filters should be completely gone + expect(params.profile_filters).not.toContain('viewAFunc'); + expect(params.profile_filters).not.toContain('viewABinary'); + + // Only View B's filter should be present + expect(params.profile_filters).toBe('f:fn:~:viewBOnly'); + }); + }); + + it('should handle sequential view switches correctly', async () => { + // Simulate: [default] -> [storage] -> [testing-view] + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + // View 1: default view (1 filter) + const defaultFilters: ProfileFilter[] = [{id: 'd-1', type: 'hide_libc', value: 'enabled'}]; + + act(() => { + result.current.forceApplyFilters(defaultFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.profile_filters).toBe('p:hide_libc:enabled'); + }); + + mockNavigateTo.mockClear(); + + // View 2: storage view (3 filters) + const storageFilters: ProfileFilter[] = [ + {id: 's-1', type: 'stack', field: 'function_name', matchType: 'not_contains', value: 'io'}, + {id: 's-2', type: 'frame', field: 'binary', matchType: 'not_contains', value: 'disk'}, + {id: 's-3', type: 'frame', field: 'function_name', matchType: 'contains', value: 'storage'}, + ]; + + act(() => { + result.current.forceApplyFilters(storageFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Default view's filter should be gone + expect(params.profile_filters).not.toContain('hide_libc'); + // Storage view should have 3 filters + expect(params.profile_filters).toContain('io'); + expect(params.profile_filters).toContain('disk'); + expect(params.profile_filters).toContain('storage'); + }); + + mockNavigateTo.mockClear(); + + // View 3: testing-view (2 filters) + const testingFilters: ProfileFilter[] = [ + {id: 't-1', type: 'stack', field: 'function_name', matchType: 'equal', value: 'test_main'}, + {id: 't-2', type: 'frame', field: 'binary', matchType: 'contains', value: 'test'}, + ]; + + act(() => { + result.current.forceApplyFilters(testingFilters); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Storage view's filters should be gone + expect(params.profile_filters).not.toContain('io'); + expect(params.profile_filters).not.toContain('disk'); + expect(params.profile_filters).not.toContain('storage'); + // Testing view should have its 2 filters + expect(params.profile_filters).toContain('test_main'); + expect(params.profile_filters).toContain('test'); + }); + }); + + it('should not change filters when clicking the same view tab', async () => { + // Start with existing filters + mockLocation.search = '?profile_filters=s:fn:=:existingFilter'; + + const {result} = renderHook(() => useProfileFiltersUrlState(), {wrapper: createWrapper()}); + + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + }); + + mockNavigateTo.mockClear(); + + // Apply the same filters (simulating clicking the same view tab) + const sameFilters: ProfileFilter[] = [ + { + id: 'same-1', + type: 'stack', + field: 'function_name', + matchType: 'equal', + value: 'existingFilter', + }, + ]; + + act(() => { + result.current.forceApplyFilters(sameFilters); + }); + + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + expect(result.current.appliedFilters[0].value).toBe('existingFilter'); + }); + }); + }); + + describe('Page refresh persistence', () => { + it('should persist user customizations in URL after page refresh simulation', async () => { + const viewDefaults: ProfileFilter[] = [ + {id: 'default-1', type: 'hide_libc', value: 'enabled'}, + ]; + + // User has customized filters (different from defaults) + mockLocation.search = '?profile_filters=s:fn:=:userCustomFilter'; + + const {result, unmount} = renderHook(() => useProfileFiltersUrlState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Verify user's filter is loaded + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + expect(result.current.appliedFilters[0].value).toBe('userCustomFilter'); + }); + + // Apply view defaults - should NOT overwrite user's URL params (preserve-existing) + act(() => { + result.current.applyViewDefaults(); + }); + + // User's filter should still be preserved + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + expect(result.current.appliedFilters[0].value).toBe('userCustomFilter'); + }); + + // Simulate page refresh + unmount(); + mockNavigateTo.mockClear(); + + // Re-render hook (simulating page reload) + const {result: result2} = renderHook(() => useProfileFiltersUrlState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // After "refresh", filter should still be from URL + await waitFor(() => { + expect(result2.current.appliedFilters).toHaveLength(1); + expect(result2.current.appliedFilters[0].value).toBe('userCustomFilter'); + }); + }); + + it('should apply view defaults when URL is empty on page load', async () => { + const viewDefaults: ProfileFilter[] = [ + { + id: 'default-1', + type: 'frame', + field: 'binary', + matchType: 'not_contains', + value: 'libc.so', + }, + ]; + + // Empty URL (fresh page load) + mockLocation.search = ''; + + const {result} = renderHook(() => useProfileFiltersUrlState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Apply view defaults + act(() => { + result.current.applyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.profile_filters).toBe('f:b:!~:libc.so'); + }); + }); + + it('should handle shared/bookmarked URL with custom params', async () => { + const viewDefaults: ProfileFilter[] = [ + {id: 'default-1', type: 'hide_libc', value: 'enabled'}, + {id: 'default-2', type: 'hide_python_internals', value: 'enabled'}, + ]; + + // Shared URL with custom params (not matching view defaults) + mockLocation.search = '?profile_filters=s:fn:~:customSharedFilter'; + + const {result} = renderHook(() => useProfileFiltersUrlState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Verify custom params are loaded + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + expect(result.current.appliedFilters[0].value).toBe('customSharedFilter'); + }); + + // Apply view defaults - should NOT overwrite + act(() => { + result.current.applyViewDefaults(); + }); + + // Custom params should be honored over view defaults + await waitFor(() => { + expect(result.current.appliedFilters).toHaveLength(1); + expect(result.current.appliedFilters[0].value).toBe('customSharedFilter'); + }); + }); + }); +}); diff --git a/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts b/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts index b4a0f2aac06..4f0911abf4d 100644 --- a/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +++ b/ui/packages/shared/profile/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts @@ -11,9 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {useMemo} from 'react'; +import {useCallback, useMemo} from 'react'; -import {useURLStateCustom, type ParamValueSetterCustom} from '@parca/components'; +import {useURLStateBatch, useURLStateCustom, type ParamValueSetterCustom} from '@parca/components'; import {safeDecode} from '@parca/utilities'; import {isPresetKey} from './filterPresets'; @@ -137,13 +137,25 @@ export const decodeProfileFilters = (encoded: string): ProfileFilter[] => { } }; -export const useProfileFiltersUrlState = (): { +interface UseProfileFiltersUrlStateOptions { + viewDefaults?: ProfileFilter[]; +} + +export const useProfileFiltersUrlState = ( + options: UseProfileFiltersUrlStateOptions = {} +): { appliedFilters: ProfileFilter[]; setAppliedFilters: ParamValueSetterCustom; + applyViewDefaults: () => void; + forceApplyFilters: (filters: ProfileFilter[]) => void; } => { + const {viewDefaults} = options; + + const batchUpdates = useURLStateBatch(); + // Store applied filters in URL state for persistence using compact encoding const [appliedFilters, setAppliedFilters] = useURLStateCustom( - 'profile_filters', + `profile_filters`, { parse: value => { return decodeProfileFilters(value as string); @@ -155,12 +167,57 @@ export const useProfileFiltersUrlState = (): { } ); + // Setter with preserve-existing strategy for applying view defaults + const [, setAppliedFiltersWithPreserve] = useURLStateCustom(`profile_filters`, { + parse: value => { + const result = decodeProfileFilters(value as string); + return result; + }, + stringify: value => { + const result = encodeProfileFilters(value); + return result; + }, + mergeStrategy: 'preserve-existing', + }); + const memoizedAppliedFilters = useMemo(() => { return appliedFilters ?? []; }, [appliedFilters]); + // Apply view defaults (only if URL is empty) + const applyViewDefaults = useCallback(() => { + if (viewDefaults === undefined || viewDefaults.length === 0) { + return; + } + + batchUpdates(() => { + setAppliedFiltersWithPreserve(viewDefaults); + }); + }, [viewDefaults, batchUpdates, setAppliedFiltersWithPreserve]); + + // Force apply filters (bypasses preserve-existing strategy) + // This validates filters before applying, similar to onApplyFilters in useProfileFilters. + // Use this when switching views to completely replace the current filters. + const forceApplyFilters = useCallback( + (filters: ProfileFilter[]) => { + const validFilters = filters.filter(f => { + if (f.type != null && isPresetKey(f.type)) { + return f.value !== '' && f.type != null; + } + return f.value !== '' && f.type != null && f.field != null && f.matchType != null; + }); + + batchUpdates(() => { + setAppliedFilters(validFilters); + }); + }, + [batchUpdates, setAppliedFilters] + ); + return { appliedFilters: memoizedAppliedFilters, setAppliedFilters, + applyViewDefaults, + forceApplyFilters, }; }; diff --git a/ui/packages/shared/profile/src/QueryControls/index.tsx b/ui/packages/shared/profile/src/QueryControls/index.tsx index cc66510039c..9983358b39c 100644 --- a/ui/packages/shared/profile/src/QueryControls/index.tsx +++ b/ui/packages/shared/profile/src/QueryControls/index.tsx @@ -177,9 +177,7 @@ export function QueryControls({ {viewComponent?.createViewComponent} - {viewComponent?.disableExplorativeQuerying === true && - viewComponent?.labelnames !== undefined && - viewComponent?.labelnames.length >= 1 ? ( + {viewComponent?.labelnames !== undefined && viewComponent?.labelnames.length >= 1 ? ( ) : showAdvancedMode && advancedModeForQueryBrowser ? ( { - console.log('Label names query result:', {data, error, isLoading}); - }, [data, error, isLoading]); - return { result: {response: data, error: error as Error}, loading: isLoading, @@ -113,10 +107,6 @@ export const useLabelValues = ( }, }); - useEffect(() => { - console.log('Label values query result:', {data, error, isLoading, labelName}); - }, [data, error, isLoading, labelName]); - return { result: {response: data ?? [], error: error as Error}, loading: isLoading, diff --git a/ui/packages/shared/profile/src/hooks/useQueryState.test.tsx b/ui/packages/shared/profile/src/hooks/useQueryState.test.tsx index b465d96db48..92319b56ecd 100644 --- a/ui/packages/shared/profile/src/hooks/useQueryState.test.tsx +++ b/ui/packages/shared/profile/src/hooks/useQueryState.test.tsx @@ -11,10 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {ReactNode} from 'react'; +import {ReactNode, act} from 'react'; // eslint-disable-next-line import/named -import {act, renderHook, waitFor} from '@testing-library/react'; +import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; +import {renderHook, waitFor} from '@testing-library/react'; import {beforeEach, describe, expect, it, vi} from 'vitest'; import {URLStateProvider} from '@parca/components'; @@ -99,14 +100,60 @@ vi.mock('../useSumBy', async () => { }; }); +// Track profile types loading state for tests +let mockProfileTypesLoading = false; +let mockProfileTypesData: + | { + types: Array<{ + name: string; + sampleType: string; + sampleUnit: string; + periodType: string; + periodUnit: string; + delta: boolean; + }>; + } + | undefined; + +// Mock useProfileTypes to control loading state in tests +vi.mock('../ProfileSelector', async () => { + const actual = await vi.importActual('../ProfileSelector'); + return { + ...actual, + useProfileTypes: () => ({ + loading: mockProfileTypesLoading, + data: mockProfileTypesData, + error: null, + }), + }; +}); + +// Helper to set profile types loading state for tests +const setProfileTypesLoading = (loading: boolean): void => { + mockProfileTypesLoading = loading; +}; + +const setProfileTypesData = (data: typeof mockProfileTypesData): void => { + mockProfileTypesData = data; +}; + // Helper to create wrapper with URLStateProvider const createWrapper = ( paramPreferences = {} ): (({children}: {children: ReactNode}) => JSX.Element) => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); const Wrapper = ({children}: {children: ReactNode}): JSX.Element => ( - - {children} - + + + {children} + + ); Wrapper.displayName = 'URLStateProviderWrapper'; return Wrapper; @@ -120,6 +167,9 @@ describe('useQueryState', () => { writable: true, }); mockLocation.search = ''; + // Reset profile types mock state + setProfileTypesLoading(false); + setProfileTypesData(undefined); }); describe('Basic functionality', () => { @@ -127,7 +177,7 @@ describe('useQueryState', () => { const {result} = renderHook( () => useQueryState({ - defaultExpression: 'process_cpu{}', + defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{}', defaultTimeSelection: 'relative:hour|1', defaultFrom: 1000, defaultTo: 2000, @@ -136,7 +186,7 @@ describe('useQueryState', () => { ); const {querySelection} = result.current; - expect(querySelection.expression).toBe('process_cpu{}'); + expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}'); expect(querySelection.timeSelection).toBe('relative:hour|1'); // From/to should be calculated from the range expect(querySelection.from).toBeDefined(); @@ -524,7 +574,9 @@ describe('useQueryState', () => { }); describe('Edge cases', () => { - it('should handle invalid expression gracefully', () => { + it('should handle invalid expression gracefully and log warning', () => { + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const {result} = renderHook( () => useQueryState({ @@ -533,8 +585,33 @@ describe('useQueryState', () => { {wrapper: createWrapper()} ); - // Should not throw error + // Should not throw error - invalid expressions are caught and logged + expect(() => result.current.querySelection).not.toThrow(); + // Should fall back to empty expression + expect(result.current.querySelection.expression).toBe('invalid{{}expression'); + // Should log a warning about the parse failure + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to parse expression', + expect.objectContaining({ + expression: 'invalid{{}expression', + }) + ); + + consoleSpy.mockRestore(); + }); + + it('should handle empty expression gracefully', () => { + const {result} = renderHook( + () => + useQueryState({ + defaultExpression: '', + }), + {wrapper: createWrapper()} + ); + + // Should not throw error with empty expression expect(() => result.current.querySelection).not.toThrow(); + expect(result.current.querySelection.expression).toBe(''); }); it('should clear merge params for non-delta profiles', async () => { @@ -585,6 +662,37 @@ describe('useQueryState', () => { expect(params.unrelated).toBe('test'); }); }); + + it('should reset query to default state', async () => { + mockLocation.search = + '?expression=memory:inuse_space:bytes:space:bytes{}&sum_by=function&group_by=namespace'; + + const {result} = renderHook( + () => + useQueryState({ + defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{}', + }), + {wrapper: createWrapper()} + ); + + // Verify initial state from URL + expect(result.current.querySelection.expression).toBe( + 'memory:inuse_space:bytes:space:bytes{}' + ); + + // Reset query + act(() => { + result.current.resetQuery(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}'); + expect(params.sum_by).toBeUndefined(); + expect(params.group_by).toBeUndefined(); + }); + }); }); describe('Commit with refreshed time range (time range re-evaluation)', () => { @@ -1191,7 +1299,8 @@ describe('useQueryState', () => { }); it('should preserve other URL params when setting ProfileSelection', async () => { - mockLocation.search = '?expression_a=process_cpu{}&other_param=value&unrelated=test'; + mockLocation.search = + '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&other_param=value&unrelated=test'; const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()}); @@ -1212,10 +1321,293 @@ describe('useQueryState', () => { expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}'); // Other params should be preserved - expect(params.expression_a).toBe('process_cpu{}'); + expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}'); expect(params.other_param).toBe('value'); expect(params.unrelated).toBe('test'); }); }); }); + + describe('View defaults and profile type parsing', () => { + it('should apply view defaults only when URL params are empty', async () => { + // Start with empty URL + mockLocation.search = ''; + + const viewDefaults = { + expression: 'process_cpu:samples:count:cpu:nanoseconds:delta{job="default"}', + sumBy: ['function'], + groupBy: ['namespace'], + }; + + const {result} = renderHook(() => useQueryState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Apply view defaults + act(() => { + result.current.applyViewDefaults(); + }); + + await waitFor(() => { + // Should set all defaults since URL is empty + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.expression).toBe( + 'process_cpu:samples:count:cpu:nanoseconds:delta{job="default"}' + ); + expect(params.sum_by).toBe('function'); + expect(params.group_by).toBe('namespace'); + }); + + // Now set URL params manually + mockLocation.search = + '?expression=memory:inuse_space:bytes:space:bytes{}&sum_by=line&group_by=pod'; + mockNavigateTo.mockClear(); + + const {result: result2} = renderHook(() => useQueryState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Apply view defaults again + act(() => { + result2.current.applyViewDefaults(); + }); + + // Should NOT overwrite existing URL params (preserve-existing strategy) + // The hook shouldn't navigate since params already exist + await waitFor(() => { + // Either no navigation or navigation preserves existing values + if (mockNavigateTo.mock.calls.length > 0) { + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}'); + expect(params.sum_by).toBe('line'); + expect(params.group_by).toBe('pod'); + } + }); + }); + + it('should parse profile type from expression using Query.parse()', () => { + // Test with profile type + mockLocation.search = + '?expression=process_cpu:samples:count:cpu:nanoseconds:delta{job="test"}'; + + const {result} = renderHook(() => useQueryState({}), {wrapper: createWrapper()}); + + expect(result.current.hasProfileType).toBe(true); + expect(result.current.profileTypeString).toBe( + 'process_cpu:samples:count:cpu:nanoseconds:delta' + ); + expect(result.current.matchersOnly).toBe('{job="test"}'); + expect(result.current.fullExpression).toBe( + 'process_cpu:samples:count:cpu:nanoseconds:delta{job="test"}' + ); + + // Test without profile type + mockLocation.search = ''; + + const {result: result2} = renderHook(() => useQueryState({}), {wrapper: createWrapper()}); + + expect(result2.current.hasProfileType).toBe(false); + expect(result2.current.profileTypeString).toBe(''); + }); + + it('should force apply view defaults and overwrite existing URL params', async () => { + // Start with existing URL params + mockLocation.search = + '?expression=memory:inuse_space:bytes:space:bytes{}&sum_by=line&group_by=pod'; + + const viewDefaults = { + expression: 'process_cpu:samples:count:cpu:nanoseconds:delta{job="default"}', + sumBy: ['function'], + groupBy: ['namespace'], + }; + + const {result} = renderHook(() => useQueryState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Force apply view defaults - should overwrite existing values + act(() => { + result.current.forceApplyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Should overwrite with view defaults, not preserve existing + expect(params.expression).toBe( + 'process_cpu:samples:count:cpu:nanoseconds:delta{job="default"}' + ); + expect(params.sum_by).toBe('function'); + expect(params.group_by).toBe('namespace'); + }); + }); + + it('should force apply only provided view defaults', async () => { + // Start with existing URL params + mockLocation.search = + '?expression=memory:inuse_space:bytes:space:bytes{}&sum_by=line&group_by=pod'; + + // Only provide expression in viewDefaults + const viewDefaults = { + expression: 'process_cpu:samples:count:cpu:nanoseconds:delta{job="new"}', + }; + + const {result} = renderHook(() => useQueryState({viewDefaults}), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.forceApplyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Should overwrite expression + expect(params.expression).toBe( + 'process_cpu:samples:count:cpu:nanoseconds:delta{job="new"}' + ); + // sum_by and group_by should remain from URL since not in viewDefaults + expect(params.sum_by).toBe('line'); + expect(params.group_by).toBe('pod'); + }); + }); + + it('should force apply view defaults with suffix for comparison mode', async () => { + // Start with existing URL params for side _a + mockLocation.search = '?expression_a=memory:inuse_space:bytes:space:bytes{}&sum_by_a=line'; + + const viewDefaults = { + expression: 'process_cpu:samples:count:cpu:nanoseconds:delta{job="default"}', + sumBy: ['function'], + }; + + const {result} = renderHook(() => useQueryState({suffix: '_a', viewDefaults}), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.forceApplyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Should overwrite _a suffixed params + expect(params.expression_a).toBe( + 'process_cpu:samples:count:cpu:nanoseconds:delta{job="default"}' + ); + expect(params.sum_by_a).toBe('function'); + }); + }); + + it('should not apply anything when viewDefaults is undefined', async () => { + mockLocation.search = '?expression=memory:inuse_space:bytes:space:bytes{}'; + + const {result} = renderHook(() => useQueryState({}), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.forceApplyViewDefaults(); + }); + + // Should not navigate since there are no defaults to apply + expect(mockNavigateTo).not.toHaveBeenCalled(); + }); + + it('should use sharedDefaults for _b suffix when viewDefaults not provided', async () => { + mockLocation.search = '?expression_b=memory:inuse_space:bytes:space:bytes{}&group_by=pod'; + + const sharedDefaults = { + groupBy: ['function_name'], + }; + + const {result} = renderHook(() => useQueryState({suffix: '_b', sharedDefaults}), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.forceApplyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Should overwrite group_by with sharedDefaults value + expect(params.group_by).toBe('function_name'); + }); + }); + + it('should still apply groupBy and sumBy when profile types are loading for matchers-only expression', async () => { + // This test covers the bug where forceApplyViewDefaults would bail out entirely + // when profile types were still loading, even though groupBy and sumBy don't depend on profile types + mockLocation.search = + '?expression=parca_agent:wallclock:nanoseconds{}&group_by=old_value&sum_by=old_sum'; + + // Simulate profile types still loading + setProfileTypesLoading(true); + + const viewDefaults = { + expression: '{namespace="test"}', // Matchers-only expression that needs profile types + sumBy: ['new_sum'], + groupBy: ['new_group', 'another_group'], + }; + + const {result} = renderHook(() => useQueryState({viewDefaults}), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.forceApplyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // Expression should NOT be set (profile types still loading) + // But groupBy and sumBy SHOULD still be applied + expect(params.sum_by).toBe('new_sum'); + expect(params.group_by).toBe('new_group,another_group'); + }); + }); + + it('should still apply groupBy and sumBy when profile types are unavailable for matchers-only expression', async () => { + // Similar to above but for when profile types finished loading with no data + mockLocation.search = + '?expression=process_cpu:samples:count:cpu:nanoseconds:delta{}&group_by=old_value'; + + // Simulate profile types loaded but with empty types array + // Set loading to false and data to undefined to avoid triggering the auto-apply useEffect + // (the auto-apply requires profileTypesData != null) + setProfileTypesLoading(false); + setProfileTypesData(undefined); + + const viewDefaults = { + expression: '{namespace="test"}', // Matchers-only expression that will fail to apply + groupBy: ['function_name', 'labels.cpu'], + }; + + const {result} = renderHook(() => useQueryState({viewDefaults}), { + wrapper: createWrapper(), + }); + + // Clear any calls from initial render + mockNavigateTo.mockClear(); + + act(() => { + result.current.forceApplyViewDefaults(); + }); + + await waitFor(() => { + expect(mockNavigateTo).toHaveBeenCalled(); + // The forceApplyViewDefaults call should set group_by to the new value + const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1]; + // groupBy should still be applied even though expression couldn't be + // (because profileTypesData is undefined, making the matchers-only expression fail) + expect(params.group_by).toBe('function_name,labels.cpu'); + }); + }); + }); }); diff --git a/ui/packages/shared/profile/src/hooks/useQueryState.ts b/ui/packages/shared/profile/src/hooks/useQueryState.ts index bc31fceda9d..3517b960fa1 100644 --- a/ui/packages/shared/profile/src/hooks/useQueryState.ts +++ b/ui/packages/shared/profile/src/hooks/useQueryState.ts @@ -13,15 +13,23 @@ import {useCallback, useEffect, useMemo, useState} from 'react'; +import {ProfileTypesResponse} from '@parca/client'; import {DateTimeRange, useParcaContext, useURLState, useURLStateBatch} from '@parca/components'; import {Query} from '@parca/parser'; -import {QuerySelection} from '../ProfileSelector'; +import {QuerySelection, useProfileTypes} from '../ProfileSelector'; import {ProfileSelection, ProfileSelectionFromParams, ProfileSource} from '../ProfileSource'; import {useResetFlameGraphState} from '../ProfileView/hooks/useResetFlameGraphState'; import {useResetStateOnProfileTypeChange} from '../ProfileView/hooks/useResetStateOnProfileTypeChange'; import {DEFAULT_EMPTY_SUM_BY, sumByToParam, useSumBy, useSumByFromParams} from '../useSumBy'; +interface ViewDefaults { + expression?: string; + sumBy?: string[]; + groupBy?: string[]; + hasProfileFilters?: boolean; +} + interface UseQueryStateOptions { suffix?: '_a' | '_b'; // For comparison mode defaultExpression?: string; @@ -29,6 +37,9 @@ interface UseQueryStateOptions { defaultFrom?: number; defaultTo?: number; comparing?: boolean; // If true, don't auto-select for delta profiles + viewDefaults?: ViewDefaults; // View-specific defaults that don't overwrite URL params + sharedDefaults?: ViewDefaults; // Shared defaults across both comparison sides + onProfileTypeChange?: () => void; // Called when profile type changes on commit, after reset } interface UseQueryStateReturn { @@ -65,6 +76,43 @@ interface UseQueryStateReturn { // parsed query parsedQuery: Query | null; + + // Parsed expression components + hasProfileType: boolean; + profileTypeString: string; + matchersOnly: string; + fullExpression: string; + + // Methods + applyViewDefaults: () => void; + forceApplyViewDefaults: () => void; + resetQuery: () => void; +} + +/** + * Prepends the first available profile type to a matchers-only expression. + * Returns the original expression if it already has a profile type or if no profile types are available. + */ +function prependProfileTypeToMatchers( + expression: string, + profileTypesData: ProfileTypesResponse | undefined +): string { + if (!expression.trim().startsWith('{')) { + return expression; + } + + if (profileTypesData?.types == null || profileTypesData.types.length === 0) { + return expression; + } + + const firstProfileType = profileTypesData.types[0]; + const profileTypeString = `${firstProfileType.name}:${firstProfileType.sampleType}:${ + firstProfileType.sampleUnit + }:${firstProfileType.periodType}:${firstProfileType.periodUnit}${ + firstProfileType.delta ? ':delta' : '' + }`; + + return `${profileTypeString}${expression}`; } export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryStateReturn => { @@ -76,6 +124,9 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState defaultFrom, defaultTo, comparing = false, + viewDefaults, + sharedDefaults, + onProfileTypeChange, } = options; const batchUpdates = useURLStateBatch(); @@ -101,6 +152,22 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState const [sumByParam, setSumByParam] = useURLState(`sum_by${suffix}`); + const [, setGroupByParam] = useURLState('group_by', { + alwaysReturnArray: true, + }); + + // Separate setters for applying view defaults with preserve-existing strategy + const [, setExpressionWithPreserve] = useURLState(`expression${suffix}`, { + mergeStrategy: 'preserve-existing', + }); + const [, setSumByWithPreserve] = useURLState(`sum_by${suffix}`, { + mergeStrategy: 'preserve-existing', + }); + const [, setGroupByWithPreserve] = useURLState('group_by', { + alwaysReturnArray: true, + mergeStrategy: 'preserve-existing', + }); + const [mergeFrom, setMergeFromState] = useURLState(`merge_from${suffix}`); const [mergeTo, setMergeToState] = useURLState(`merge_to${suffix}`); @@ -110,6 +177,37 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState // Parse sumBy from URL parameter format const sumBy = useSumByFromParams(sumByParam); + // Detect if viewDefaults contain matchers-only expression + const hasMatchersOnlyDefault = useMemo(() => { + const defaults = suffix === '' || suffix === '_a' ? viewDefaults : sharedDefaults; + if (defaults?.expression == null || defaults.expression === '') return false; + return defaults.expression.trim().startsWith('{'); + }, [viewDefaults, sharedDefaults, suffix]); + + // Get time range for profile types query + const timeRangeForProfileTypes = useMemo(() => { + return DateTimeRange.fromRangeKey( + timeSelection ?? defaultTimeSelection, + from !== undefined && from !== '' ? parseInt(from) : defaultFrom, + to !== undefined && to !== '' ? parseInt(to) : defaultTo + ); + }, [timeSelection, from, to, defaultTimeSelection, defaultFrom, defaultTo]); + + // Fetch profile types only when needed + const { + loading: profileTypesLoading, + data: profileTypesData, + error: profileTypesError, + } = useProfileTypes( + queryClient, + hasMatchersOnlyDefault ? timeRangeForProfileTypes.getFromMs() : undefined, + hasMatchersOnlyDefault ? timeRangeForProfileTypes.getToMs() : undefined + ); + + // Track if we need to force-apply view defaults after profile types load + // (when forceApplyViewDefaults was called but skipped due to profile types still loading) + const [pendingForceApply, setPendingForceApply] = useState(false); + // Draft state management const [draftExpression, setDraftExpression] = useState(expression ?? defaultExpression); const [draftFrom, setDraftFrom] = useState(from ?? defaultFrom?.toString() ?? ''); @@ -121,7 +219,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState const draftQuery = useMemo(() => { try { return Query.parse(draftExpression ?? ''); - } catch { + } catch (error) { + console.warn('Failed to parse draft expression', { + expression: draftExpression, + error: error instanceof Error ? error.message : String(error), + }); return Query.parse(''); } }, [draftExpression]); @@ -129,7 +231,11 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState const query = useMemo(() => { try { return Query.parse(expression ?? ''); - } catch { + } catch (error) { + console.warn('Failed to parse expression', { + expression, + error: error instanceof Error ? error.message : String(error), + }); return Query.parse(''); } }, [expression]); @@ -346,6 +452,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState Query.parse(querySelection.expression).profileType().toString() ) { resetStateOnProfileTypeChange(); + onProfileTypeChange?.(); } }); }, @@ -369,6 +476,7 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState setSelectionParam, resetFlameGraphState, resetStateOnProfileTypeChange, + onProfileTypeChange, draftProfileType, querySelection.expression, ] @@ -423,10 +531,130 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState [batchUpdates, setSelectionParam, setMergeFromState, setMergeToState] ); + // Apply view defaults to URL params (only if URL params are empty) + const applyViewDefaults = useCallback(() => { + batchUpdates(() => { + const defaults = suffix === '' || suffix === '_a' ? viewDefaults : sharedDefaults; + if (defaults === undefined) return; + + // Apply expression default using preserve-existing strategy + if (defaults.expression !== undefined) { + const isMatchersOnly = defaults.expression.trim().startsWith('{'); + + if (isMatchersOnly) { + // Skip if profile types are still loading - will be retried via useEffect + const canApplyMatchersOnly = + !profileTypesLoading && + profileTypesError == null && + profileTypesData != null && + profileTypesData.types.length > 0; + + if (canApplyMatchersOnly) { + const fullExpression = prependProfileTypeToMatchers( + defaults.expression, + profileTypesData + ); + setExpressionWithPreserve(fullExpression); + } + } else { + setExpressionWithPreserve(defaults.expression); + } + } + + if (defaults.sumBy !== undefined) { + setSumByWithPreserve(sumByToParam(defaults.sumBy)); + } + + if (defaults.groupBy !== undefined) { + setGroupByWithPreserve(defaults.groupBy); + } + }); + }, [ + batchUpdates, + suffix, + viewDefaults, + sharedDefaults, + setExpressionWithPreserve, + setSumByWithPreserve, + setGroupByWithPreserve, + profileTypesLoading, + profileTypesData, + profileTypesError, + ]); + + // Force apply view defaults to URL params (overwrites existing values) + const forceApplyViewDefaults = useCallback(() => { + batchUpdates(() => { + const defaults = suffix === '' || suffix === '_a' ? viewDefaults : sharedDefaults; + if (defaults === undefined) { + return; + } + + if (defaults.expression !== undefined) { + const isMatchersOnly = defaults.expression.trim().startsWith('{'); + + if (isMatchersOnly) { + // Skip if profile types not ready - pendingForceApply triggers retry + const canApplyMatchersOnly = + !profileTypesLoading && + profileTypesError == null && + profileTypesData != null && + profileTypesData.types.length > 0; + + if (canApplyMatchersOnly) { + const fullExpression = prependProfileTypeToMatchers( + defaults.expression, + profileTypesData + ); + setExpressionState(fullExpression); + setPendingForceApply(false); + } else { + setPendingForceApply(true); + } + } else { + setExpressionState(defaults.expression); + } + } + + if (defaults.sumBy !== undefined) { + setSumByParam(sumByToParam(defaults.sumBy)); + } + + if (defaults.groupBy !== undefined) { + setGroupByParam(defaults.groupBy); + } + }); + }, [ + batchUpdates, + suffix, + viewDefaults, + sharedDefaults, + setExpressionState, + setSumByParam, + setGroupByParam, + profileTypesLoading, + profileTypesData, + profileTypesError, + setPendingForceApply, + ]); + + // Reset query to default state + const resetQuery = useCallback(() => { + batchUpdates(() => { + setExpressionState(defaultExpression); + setSumByParam(undefined); + setGroupByParam(undefined); + }); + }, [batchUpdates, setExpressionState, defaultExpression, setSumByParam, setGroupByParam]); + const draftParsedQuery = useMemo(() => { try { return Query.parse(draftSelection.expression ?? ''); - } catch { + } catch (error) { + console.warn('Failed to parse draft selection expression', { + expression: draftSelection.expression, + error: error instanceof Error ? error.message : String(error), + }); return Query.parse(''); } }, [draftSelection.expression]); @@ -434,11 +662,74 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState const parsedQuery = useMemo(() => { try { return Query.parse(querySelection.expression ?? ''); - } catch { + } catch (error) { + console.warn('Failed to parse query selection expression', { + expression: querySelection.expression, + error: error instanceof Error ? error.message : String(error), + }); return Query.parse(''); } }, [querySelection.expression]); + const {hasProfileType, profileTypeString, matchersOnly, fullExpression} = useMemo(() => { + if (expression === undefined || expression === '') { + return { + hasProfileType: false, + profileTypeString: '', + matchersOnly: '{}', + fullExpression: '', + }; + } + + const expr = expression ?? defaultExpression; + try { + const parsed = Query.parse(expr); + + const profileType = parsed.profileType(); + const profileTypeStr = profileType.toString(); + const hasProfile = profileTypeStr !== ''; + const matchers = `{${parsed.matchersString()}}`; + + return { + hasProfileType: hasProfile, + profileTypeString: profileTypeStr, + matchersOnly: matchers, + fullExpression: parsed.toString(), + }; + } catch (error) { + console.warn('Failed to parse expression for profile type extraction', { + expression: expr, + error: error instanceof Error ? error.message : String(error), + }); + return { + hasProfileType: false, + profileTypeString: '', + matchersOnly: '{}', + fullExpression: expr, + }; + } + }, [expression, defaultExpression]); + + // Handle pending force apply when profile types finish loading (for matchers-only expressions) + // This effect only handles the async case where forceApplyViewDefaults was called + // but profile types weren't ready yet + useEffect( + () => { + if ( + pendingForceApply && + hasMatchersOnlyDefault && + !profileTypesLoading && + profileTypesData != null && + profileTypesData.types.length > 0 + ) { + setPendingForceApply(false); + forceApplyViewDefaults(); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [hasMatchersOnlyDefault, profileTypesLoading, profileTypesData, pendingForceApply] + ); + return { // Current committed state querySelection, @@ -466,5 +757,15 @@ export const useQueryState = (options: UseQueryStateOptions = {}): UseQueryState draftParsedQuery, parsedQuery, + + // Parsed expression components + hasProfileType, + profileTypeString, + matchersOnly, + fullExpression, + + applyViewDefaults, + forceApplyViewDefaults, + resetQuery, }; }; diff --git a/ui/packages/shared/profile/src/index.tsx b/ui/packages/shared/profile/src/index.tsx index 6a604a1115b..a0a984df4f8 100644 --- a/ui/packages/shared/profile/src/index.tsx +++ b/ui/packages/shared/profile/src/index.tsx @@ -34,6 +34,7 @@ export * from './ProfileSource'; export { convertToProtoFilters, convertFromProtoFilters, + useProfileFilters, } from './ProfileView/components/ProfileFilters/useProfileFilters'; export * from './ProfileView'; export * from './ProfileViewWithData'; diff --git a/ui/ui.go b/ui/ui.go index 33702c0b7a2..9c0d426462d 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,4 +1,4 @@ -// Copyright 2022-2025 The Parca Authors +// Copyright 2022-2026 The Parca Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at