diff --git a/README.md b/README.md index e553a9d..8b3578f 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ int main() { } ``` -`log_event` sends a `POST` request to the configured endpoint with properties encoded as query parameters, matching the Scarf Go SDK behavior. String values are sent as strings; numbers, booleans, arrays, and objects are encoded as JSON values. +`log_event` sends a `POST` request to the configured endpoint with `Content-Type: application/json`. Event properties are encoded as a JSON request body so strings, numbers, booleans, arrays, objects, and nulls round-trip without query-string encoding. ## Configuration diff --git a/include/scarf/event_logger.hpp b/include/scarf/event_logger.hpp index fd72c27..86d471c 100644 --- a/include/scarf/event_logger.hpp +++ b/include/scarf/event_logger.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -37,7 +36,6 @@ class PropertyValue { PropertyValue(Object value); const Value &value() const noexcept; - std::string to_query_value() const; std::string to_json() const; private: @@ -98,8 +96,6 @@ class EventLogger { }; std::string build_user_agent(); -std::string url_encode(std::string_view value); -std::string append_query_parameters(std::string endpoint_url, const PropertyMap &properties); bool env_truthy(const char *name); } // namespace scarf diff --git a/src/event_logger.cpp b/src/event_logger.cpp index a2e33b6..0bf6679 100644 --- a/src/event_logger.cpp +++ b/src/event_logger.cpp @@ -115,11 +115,6 @@ std::string cpp_standard_name() { return "pre11"; } -bool is_unreserved(unsigned char c) { - return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || - c == '.' || c == '~'; -} - } // namespace PropertyValue::PropertyValue() : value_(nullptr) { @@ -198,49 +193,6 @@ std::string PropertyValue::to_json() const { return std::visit(Visitor{}, value_); } -std::string PropertyValue::to_query_value() const { - if (std::holds_alternative(value_)) - return std::get(value_); - return to_json(); -} - -std::string url_encode(std::string_view value) { - std::ostringstream out; - out << std::uppercase << std::hex; - for (unsigned char c : value) { - if (is_unreserved(c)) { - out << static_cast(c); - } else { - out << '%' << std::setw(2) << std::setfill('0') << static_cast(c) << std::setfill(' '); - } - } - return out.str(); -} - -std::string append_query_parameters(std::string endpoint_url, const PropertyMap &properties) { - if (properties.empty()) - return endpoint_url; - - const bool has_query = endpoint_url.find('?') != std::string::npos; - const bool needs_separator = !endpoint_url.empty() && endpoint_url.back() != '?' && endpoint_url.back() != '&'; - if (!has_query) { - endpoint_url += '?'; - } else if (needs_separator) { - endpoint_url += '&'; - } - - bool wrote = false; - for (const auto &[key, value] : properties) { - if (wrote) - endpoint_url += '&'; - wrote = true; - endpoint_url += url_encode(key); - endpoint_url += '='; - endpoint_url += url_encode(value.to_query_value()); - } - return endpoint_url; -} - bool env_truthy(const char *name) { if (name == nullptr) return false; @@ -302,9 +254,11 @@ LogResult EventLogger::log_event(const PropertyMap &properties, std::chrono::mil HttpRequest request; request.method = "POST"; - request.url = append_query_parameters(options_.endpoint_url, properties); + request.url = options_.endpoint_url; + request.body = PropertyValue(PropertyValue::Object(properties)).to_json(); request.timeout = timeout; request.headers.emplace("User-Agent", user_agent_); + request.headers.emplace("Content-Type", "application/json"); if (options_.verbose) std::cerr << "[scarf] sending event to " << request.url << "\n"; diff --git a/tests/event_logger_test.cpp b/tests/event_logger_test.cpp index f6d166a..a7f3ed3 100644 --- a/tests/event_logger_test.cpp +++ b/tests/event_logger_test.cpp @@ -40,7 +40,7 @@ void set_env(const char *name, const char *value) { #endif } -void test_successful_post_with_query_and_user_agent() { +void test_successful_post_with_json_body_and_user_agent() { auto transport = std::make_shared(); scarf::LoggerOptions options; options.endpoint_url = "https://example.test/collect?existing=1"; @@ -56,13 +56,11 @@ void test_successful_post_with_query_and_user_agent() { require(transport->calls == 1, "expected one request"); require(transport->last_request.method == "POST", "expected POST"); require(transport->last_request.timeout == std::chrono::milliseconds(1500), "expected custom timeout"); - require(transport->last_request.url.find("existing=1") != std::string::npos, - "expected existing query to be preserved"); - require(transport->last_request.url.find("event=package_download") != std::string::npos, - "expected event query parameter"); - require(transport->last_request.url.find("count=42") != std::string::npos, "expected numeric query parameter"); - require(transport->last_request.url.find("nested=%7B%22a%22%3A%22b%22%7D") != std::string::npos, - "expected JSON object query parameter"); + require(transport->last_request.url == "https://example.test/collect?existing=1", + "expected endpoint URL to be preserved without telemetry query parameters"); + require(transport->last_request.body == "{\"count\":42,\"event\":\"package_download\",\"nested\":{\"a\":\"b\"}}", + "expected telemetry properties as JSON request body"); + require(transport->last_request.headers.at("Content-Type") == "application/json", "expected JSON content type"); const auto ua = transport->last_request.headers.at("User-Agent"); require(ua.rfind("scarf-cpp/", 0) == 0, "expected scarf-cpp user-agent prefix"); require(ua.find("platform=") != std::string::npos, "expected platform in user-agent"); @@ -132,7 +130,7 @@ void test_blank_endpoint_is_error() { int main() { try { - test_successful_post_with_query_and_user_agent(); + test_successful_post_with_json_body_and_user_agent(); test_disabled_env_does_not_send(); test_non_success_status(); test_transport_exception_is_captured();