Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 0 additions & 4 deletions include/scarf/event_logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <variant>
#include <vector>

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
52 changes: 3 additions & 49 deletions src/event_logger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<std::string>(value_))
return std::get<std::string>(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<char>(c);
} else {
out << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(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;
Expand Down Expand Up @@ -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";
Expand Down
16 changes: 7 additions & 9 deletions tests/event_logger_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CapturingTransport>();
scarf::LoggerOptions options;
options.endpoint_url = "https://example.test/collect?existing=1";
Expand All @@ -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");
Expand Down Expand Up @@ -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();
Expand Down
Loading