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 CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# For toolchains without make (MSVC on Windows). The client itself needs no
# build step: include trace_client.hpp. This only builds and runs the tests.
cmake_minimum_required(VERSION 3.10)
project(trace_client_cpp CXX)
project(trace_client_cpp VERSION 0.2.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand Down
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,33 @@ all three speak the same wire format and make the same promises.
#include "trace_client.hpp"

trace_client::TraceClient trace("https://trace.danielstephenson.dev", "MyGame",
MYGAME_VERSION, // the program's own version, e.g. "1.4.0"
settings.usageReportingKey,
settings.usageReportingEnabled);
trace.report("startup", {{"version", "1.4.0"}});
trace.report("startup");
trace.report("level-complete", 3.0, {{"level", "forest"}});

// on shutdown -- also before a short-lived program exits, so the event is
// sent. The destructor does the same.
trace.close();
```

## Every event carries the program's version

The third constructor argument is the program's own version, and it is
required. Every event the client sends — `startup`, `command`, anything
else — carries it as the tag `version`, so every event can be tied to a
release, not just `startup`. An event that passes its own `version` tag keeps
it, and the tags passed to `report` are never modified. There is no need to
tag `startup` by hand any more.

The version is trimmed. Since nothing in the client throws, a blank one, or
one over 255 bytes, is handled like a blank base URL: the client reports
nothing and `disabledReason()` is `"unavailable"`.

Before 0.2.0, the constructor took no version and only events tagged by hand
carried one. Upgrading is one argument, after the application name.

## What `report` promises

| Property | Meaning |
Expand All @@ -47,13 +64,14 @@ starts no thread and costs nothing; the first that applies is the reason:
program's own setting. Any other value, or an unset variable, leaves that
setting in charge. `trace_client::environmentOptsOut()` answers the same
question on its own.
- **The program's own setting:** `enabled = false` (the fourth constructor
- **The program's own setting:** `enabled = false` (the fifth constructor
argument).
- **No key** (or a blank one).

`client.disabledReason()` says which one applied — `"environment"`, `"config"`
or `"no key"`, or `"unavailable"` when the client cannot send at all (a
browser build, a blank base URL or application name, or the thread could
browser build, a blank base URL, application name or version, a version over
255 bytes, or the thread could
not be started) — and is empty when the client reports, so a program can print it.
A default-constructed `TraceClient` is disabled with reason `"config"`.

Expand Down Expand Up @@ -130,13 +148,13 @@ The executable can be overridden before the include, e.g.

`POST {baseUrl}/api/metrics` with `Authorization: Bearer <key>`,
`Content-Type: application/json; charset=utf-8`,
`User-Agent: trace-client-cpp/0.1.0 (<application>)` and a body of
`User-Agent: trace-client-cpp/0.2.0 (<application>)` and a body of

```json
{"application":"MyGame","name":"startup","tags":{"version":"1.4.0"}}
{"application":"MyGame","name":"command","tags":{"name":"home","version":"1.4.0"}}
```

`value` and `tags` are omitted when not given. The server assigns the
`value` is omitted when not given; `tags` always holds at least `version`. The server assigns the
timestamp. A `201` is success; anything else is passed to the `Logger` and
dropped.

Expand Down
132 changes: 106 additions & 26 deletions test/test_trace_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ static void cleanEnvironment() {

static void reportPostsTheEventToTheMetricsEndpointWithTheKey() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "secret-key");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "secret-key");
CHECK(client.isEnabled());
client.report("startup");
CHECK(server.waitFor(1, 10));
Expand All @@ -257,12 +257,12 @@ static void reportPostsTheEventToTheMetricsEndpointWithTheKey() {
CHECK_EQ(std::string("/api/metrics"), got[0].path);
CHECK_EQ(std::string("Bearer secret-key"), got[0].authorization);
CHECK_EQ(std::string("application/json; charset=utf-8"), got[0].contentType);
CHECK_EQ(std::string("{\"application\":\"MyGame\",\"name\":\"startup\"}"), got[0].body);
CHECK_EQ(std::string("{\"application\":\"MyGame\",\"name\":\"startup\",\"tags\":{\"version\":\"1.2.3\"}}"), got[0].body);
}

static void reportCarriesValueAndTagsWhenGiven() {
StubServer server;
trace_client::TraceClient client(server.baseUrl() + "/", "MyGame", "k");
trace_client::TraceClient client(server.baseUrl() + "/", "MyGame", "1.2.3", "k");
client.report("command", 1.5, {{"name", "home"}, {"version", "1.0"}});
CHECK(server.waitFor(1, 10));
client.close();
Expand All @@ -275,7 +275,7 @@ static void reportCarriesValueAndTagsWhenGiven() {

static void userAgentNamesTheClientVersion() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
client.report("startup");
CHECK(server.waitFor(1, 10));
client.close();
Expand All @@ -287,7 +287,7 @@ static void userAgentNamesTheClientVersion() {
static void reportReturnsBeforeTheServerAnswers() {
StubServer server;
server.hold();
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
for (int i = 0; i < 10; ++i) client.report("startup");
CHECK(secondsSince(start) < 0.1);
Expand All @@ -302,7 +302,7 @@ static void reportDoesNotThrowWhenNothingIsListening() {
port = std::atoi(closed.baseUrl().substr(closed.baseUrl().rfind(':') + 1).c_str());
}
Log log;
trace_client::TraceClient client("http://127.0.0.1:" + std::to_string(port), "MyGame", "k", true, log.logger());
trace_client::TraceClient client("http://127.0.0.1:" + std::to_string(port), "MyGame", "1.2.3", "k", true, log.logger());
client.report("startup");
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
client.close();
Expand All @@ -314,7 +314,7 @@ static void rejectedKeyIsLoggedNotThrown() {
StubServer server;
server.status = 401;
Log log;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "wrong", true, log.logger());
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "wrong", true, log.logger());
client.report("startup");
client.close();
CHECK_EQ(1u, server.received().size());
Expand All @@ -329,7 +329,7 @@ static void missingCurlIsAnUnreachableServer() {
setEnv("PATH", "/nonexistent-trace-client-test");
Log log;
{
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k", true, log.logger());
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k", true, log.logger());
CHECK(client.isEnabled());
client.report("startup");
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
Expand All @@ -345,11 +345,11 @@ static void missingCurlIsAnUnreachableServer() {
static void disabledClientSendsNothing() {
StubServer server;
{
trace_client::TraceClient off(server.baseUrl(), "MyGame", "k", false);
trace_client::TraceClient off(server.baseUrl(), "MyGame", "1.2.3", "k", false);
CHECK(!off.isEnabled());
CHECK_EQ(std::string("config"), off.disabledReason());
off.report("startup");
trace_client::TraceClient keyless(server.baseUrl(), "MyGame", " ");
trace_client::TraceClient keyless(server.baseUrl(), "MyGame", "1.2.3", " ");
CHECK(!keyless.isEnabled());
CHECK_EQ(std::string("no key"), keyless.disabledReason());
keyless.report("startup");
Expand All @@ -358,7 +358,7 @@ static void disabledClientSendsNothing() {
CHECK_EQ(std::string("config"), nothing.disabledReason());
nothing.report("startup");
nothing.close();
trace_client::TraceClient unnamed(server.baseUrl(), " ", "k");
trace_client::TraceClient unnamed(server.baseUrl(), " ", "1.2.3", "k");
CHECK_EQ(std::string("unavailable"), unnamed.disabledReason());
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
Expand All @@ -367,7 +367,7 @@ static void disabledClientSendsNothing() {

static void disabledReasonIsEmptyWhenTheClientReports() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
CHECK(client.disabledReason().empty());
}

Expand All @@ -377,7 +377,7 @@ static void environmentOptOutsDisableForEveryAcceptedValue() {
for (std::size_t i = 0; i < sizeof off / sizeof off[0]; ++i) {
cleanEnvironment();
setEnv("TRACE_USAGE_REPORTING", off[i]);
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
CHECK(!client.isEnabled());
CHECK_EQ(std::string("environment"), client.disabledReason());
client.report("startup");
Expand All @@ -386,7 +386,7 @@ static void environmentOptOutsDisableForEveryAcceptedValue() {
for (std::size_t i = 0; i < sizeof dnt / sizeof dnt[0]; ++i) {
cleanEnvironment();
setEnv("DO_NOT_TRACK", dnt[i]);
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
CHECK(!client.isEnabled());
CHECK_EQ(std::string("environment"), client.disabledReason());
}
Expand All @@ -402,17 +402,17 @@ static void otherEnvironmentValuesLeaveTheProgramSettingInCharge() {
cleanEnvironment();
setEnv("TRACE_USAGE_REPORTING", values[i]);
setEnv("DO_NOT_TRACK", i == 0 ? "0" : "false");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
CHECK(client.isEnabled());
}
cleanEnvironment();
}

static void environmentWinsOverTheConfigFlagAndTheKey() {
setEnv("DO_NOT_TRACK", "1");
trace_client::TraceClient off("http://127.0.0.1:9", "MyGame", "k", false);
trace_client::TraceClient off("http://127.0.0.1:9", "MyGame", "1.2.3", "k", false);
CHECK_EQ(std::string("environment"), off.disabledReason());
trace_client::TraceClient keyless("http://127.0.0.1:9", "MyGame", "");
trace_client::TraceClient keyless("http://127.0.0.1:9", "MyGame", "1.2.3", "");
CHECK_EQ(std::string("environment"), keyless.disabledReason());
CHECK(trace_client::environmentOptsOut());
cleanEnvironment();
Expand All @@ -421,7 +421,7 @@ static void environmentWinsOverTheConfigFlagAndTheKey() {

static void reportIgnoresABlankName() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
client.report("");
client.report(" ");
client.close();
Expand Down Expand Up @@ -469,7 +469,7 @@ static void nothingTheProgramPassesCanChangeWhereOrWhatCurlSends() {
StubServer server;
// A newline in any value would, unescaped, start a new curl option.
std::string sneaky = "My\"Game\nurl = \"http://127.0.0.1:1/\"\n";
trace_client::TraceClient client(server.baseUrl(), sneaky, "k\r\nX-Injected: 1");
trace_client::TraceClient client(server.baseUrl(), sneaky, "1.2.3", "k\r\nX-Injected: 1");
client.report("startup\n", {{"t\\", "\"\n-o /tmp/x"}});
CHECK(server.waitFor(1, 10));
client.close();
Expand All @@ -480,14 +480,14 @@ static void nothingTheProgramPassesCanChangeWhereOrWhatCurlSends() {
CHECK(lower(got[0].headerLines[i]).find("x-injected") != 0);
}
CHECK_EQ(trace_client::detail::json("My\"Game\nurl = \"http://127.0.0.1:1/\"", "startup\n", false, 0,
{{"t\\", "\"\n-o /tmp/x"}}),
{{"t\\", "\"\n-o /tmp/x"}, {"version", "1.2.3"}}),
got[0].body);
}

static void aNewlineInTheBaseUrlCannotAddACurlOption() {
StubServer server;
// Unescaped, the second line would be an option of its own.
trace_client::TraceClient client(server.baseUrl() + "\nheader = X-Injected:1\n#", "MyGame", "k");
trace_client::TraceClient client(server.baseUrl() + "\nheader = X-Injected:1\n#", "MyGame", "1.2.3", "k");
client.report("startup");
client.close();
std::vector<Request> got = server.received();
Expand All @@ -502,7 +502,7 @@ static void queueIsBoundedAndDropsRatherThanGrows() {
StubServer server;
server.hold(); // the sender waits on the first report
Log log;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k", true, log.logger());
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k", true, log.logger());
const std::size_t flood = trace_client::QUEUE_CAPACITY * 3;
for (std::size_t i = 0; i < flood; ++i) client.report("flood");
CHECK(log.count("queue full") >= flood - trace_client::QUEUE_CAPACITY - 1);
Expand All @@ -516,7 +516,7 @@ static void closeSendsWhatWasJustQueuedBeforeStopping() {
// a lost one visible.
StubServer server;
for (int i = 0; i < 20; ++i) {
trace_client::TraceClient client(server.baseUrl(), "MyCli", "k");
trace_client::TraceClient client(server.baseUrl(), "MyCli", "1.2.3", "k");
client.report("startup", {{"run", std::to_string(i)}});
client.close();
}
Expand All @@ -526,7 +526,7 @@ static void closeSendsWhatWasJustQueuedBeforeStopping() {
static void destructorClosesToo() {
StubServer server;
{
trace_client::TraceClient client(server.baseUrl(), "MyCli", "k");
trace_client::TraceClient client(server.baseUrl(), "MyCli", "1.2.3", "k");
client.report("startup");
}
CHECK_EQ(1u, server.received().size());
Expand All @@ -535,7 +535,7 @@ static void destructorClosesToo() {
static void closeReturnsWithinTheTimeoutWhenTheServerHangs() {
StubServer server;
server.hold(); // never answers while held
trace_client::TraceClient client(server.baseUrl(), "MyCli", "k");
trace_client::TraceClient client(server.baseUrl(), "MyCli", "1.2.3", "k");
client.report("startup");
client.report("second");
std::this_thread::sleep_for(std::chrono::milliseconds(200));
Expand All @@ -546,9 +546,85 @@ static void closeReturnsWithinTheTimeoutWhenTheServerHangs() {
server.release();
}

static void reportTagsEveryEventWithTheProgramVersionTrimmed() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", " 2.0.0-SNAPSHOT ", "k");
CHECK(client.isEnabled());
client.report("command", {{"name", "home"}});
CHECK(server.waitFor(1, 10));
client.close();
std::vector<Request> got = server.received();
if (got.empty()) { CHECK(!got.empty()); return; }
CHECK_EQ(std::string("{\"application\":\"MyGame\",\"name\":\"command\","
"\"tags\":{\"name\":\"home\",\"version\":\"2.0.0-SNAPSHOT\"}}"), got[0].body);
// the program's version is a tag; the User-Agent still names the client's
CHECK_EQ(std::string("trace-client-cpp/" TRACE_CLIENT_VERSION " (MyGame)"), got[0].userAgent);
}

static void anEventsOwnVersionTagWinsOverTheProgramVersion() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
trace_client::Tags tags;
tags["version"] = "9.9.9";
client.report("startup", tags);
CHECK(server.waitFor(1, 10));
client.close();
std::vector<Request> got = server.received();
if (got.empty()) { CHECK(!got.empty()); return; }
CHECK_EQ(std::string("{\"application\":\"MyGame\",\"name\":\"startup\",\"tags\":{\"version\":\"9.9.9\"}}"),
got[0].body);
CHECK_EQ(1u, tags.size());
CHECK_EQ(std::string("9.9.9"), tags["version"]);
}

static void theCallersTagsAreNeverModified() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
trace_client::Tags tags;
tags["name"] = "home";
client.report("command", tags);
client.report("command", 1.0, tags);
CHECK(server.waitFor(2, 10));
client.close();
CHECK_EQ(1u, tags.size());
CHECK(tags.find("version") == tags.end());

trace_client::Tags merged = trace_client::detail::withVersion(tags, "1.2.3");
CHECK_EQ(1u, tags.size());
CHECK_EQ(std::string("1.2.3"), merged["version"]);
CHECK_EQ(std::string("home"), merged["name"]);
CHECK_EQ(std::string("1.2.3"), trace_client::detail::withVersion(trace_client::Tags(), "1.2.3")["version"]);
}

static void aBlankOrOverlongVersionIsRejected() {
// Nothing in the client throws: a bad version is a bad argument like a
// blank application name -- the client reports nothing, reason "unavailable".
StubServer server;
{
trace_client::TraceClient empty(server.baseUrl(), "MyGame", "", "k");
CHECK(!empty.isEnabled());
CHECK_EQ(std::string("unavailable"), empty.disabledReason());
empty.report("startup");
trace_client::TraceClient blank(server.baseUrl(), "MyGame", " \t ", "k");
CHECK(!blank.isEnabled());
CHECK_EQ(std::string("unavailable"), blank.disabledReason());
blank.report("startup");
trace_client::TraceClient overlong(server.baseUrl(), "MyGame", std::string(trace_client::MAX_LENGTH + 1, '9'), "k");
CHECK(!overlong.isEnabled());
CHECK_EQ(std::string("unavailable"), overlong.disabledReason());
overlong.report("startup");
// exactly the limit, once trimmed, is accepted
trace_client::TraceClient longest(server.baseUrl(), "MyGame",
" " + std::string(trace_client::MAX_LENGTH, '9') + " ", "k");
CHECK(longest.isEnabled());
longest.close();
}
CHECK_EQ(0u, server.received().size());
}

static void closeIsPromptAndIdempotent() {
StubServer server;
trace_client::TraceClient client(server.baseUrl(), "MyGame", "k");
trace_client::TraceClient client(server.baseUrl(), "MyGame", "1.2.3", "k");
client.report("startup");
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
client.close();
Expand Down Expand Up @@ -588,6 +664,10 @@ int main() {
{"destructorClosesToo", destructorClosesToo},
{"closeReturnsWithinTheTimeoutWhenTheServerHangs", closeReturnsWithinTheTimeoutWhenTheServerHangs},
{"closeIsPromptAndIdempotent", closeIsPromptAndIdempotent},
{"reportTagsEveryEventWithTheProgramVersionTrimmed", reportTagsEveryEventWithTheProgramVersionTrimmed},
{"anEventsOwnVersionTagWinsOverTheProgramVersion", anEventsOwnVersionTagWinsOverTheProgramVersion},
{"theCallersTagsAreNeverModified", theCallersTagsAreNeverModified},
{"aBlankOrOverlongVersionIsRejected", aBlankOrOverlongVersionIsRejected},
};
const std::size_t count = sizeof tests / sizeof tests[0];
for (std::size_t i = 0; i < count; ++i) {
Expand Down
Loading
Loading