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
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ speak the same wire format and make the same promises.
```csharp
using StephensonSoftware.Trace;

var trace = new TraceClient("https://trace.danielstephenson.dev", "my-game",
var trace = new TraceClient("https://trace.danielstephenson.dev", "my-game", "1.4.0",
key: settings.UsageReportingKey,
enabled: settings.UsageReportingEnabled,
log: message => Debug.WriteLine(message)); // optional
Expand All @@ -31,13 +31,30 @@ else
Console.WriteLine("Usage reporting is off (" + trace.DisabledReason + ").");
}

trace.Report("startup", tags: new Dictionary<string, string> { { "version", "1.4.0" } });
trace.Report("startup");
trace.Report("level-complete", 42.0);

// on shutdown -- also before a short-lived program exits, so the event is sent
trace.Dispose(); // same as trace.Close()
```

## Every event carries the program's version

The third constructor argument is the program's own version, and it is
required: a null or blank one, or one over 255 characters after trimming,
throws `ArgumentException`. Every event the client sends — `startup`,
`level-complete`, 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 dictionary passed to `Report` is never
modified. There is no need to tag `startup` by hand any more. The `version` tag
counts toward the server's 32-tag limit, so an event may carry 31 of its own.

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 — for
example `Application.version` in Unity, or the assembly's informational
version elsewhere. Pass `key`, `enabled` and `log` by name, as above, so a
key can never land in the version's place.

## What `Report` promises

| Property | Meaning |
Expand Down Expand Up @@ -87,14 +104,15 @@ distribution.
## The wire format

`POST {baseUrl}/api/metrics` with `Authorization: Bearer <key>`,
`User-Agent: trace-client/0.1.0 (<application>)` and a body of
`User-Agent: trace-client/0.2.0 (<application>)` and a body of

```json
{"application":"my-game","name":"startup","tags":{"version":"1.4.0"}}
{"application":"my-game","name":"level-complete","value":42,"tags":{"version":"1.4.0"}}
```

`value` and `tags` are omitted when not given; `value` is written with the
invariant culture, so a German locale still sends `2.5`. The server assigns
`value` is omitted when not given, and `tags` always holds at least
`version`. `value` is written with the invariant culture, so a German locale
still sends `2.5`. The server assigns
the timestamp. A `201` is success; anything else is logged and dropped.

## Keys
Expand Down
78 changes: 65 additions & 13 deletions src/TraceClient/TraceClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* trace-client 0.1.0 -- https://github.com/Stephenson-Software/trace-client-csharp
* trace-client 0.2.0 -- https://github.com/Stephenson-Software/trace-client-csharp
*
* One call to report that a program was used. Copy this file into a project as
* is, or reference the project; either way there is nothing else to add.
Expand Down Expand Up @@ -41,19 +41,23 @@ namespace StephensonSoftware.Trace
/// (<c>TRACE_USAGE_REPORTING=off</c> or <c>DO_NOT_TRACK=1</c>) -- reason
/// <c>environment</c>; the program's own setting, <c>enabled: false</c> --
/// reason <c>config</c>; no key -- reason <c>no key</c>.</para>
/// <para>Every event carries the program's own version as the tag
/// <c>version</c> -- the third constructor argument, required, so a
/// <c>command</c> event can be tied to a release as well as a
/// <c>startup</c> one. An event's own <c>version</c> tag wins over it.</para>
/// <code>
/// var trace = new TraceClient("https://trace.example.org", "my-game",
/// var trace = new TraceClient("https://trace.example.org", "my-game", ProgramVersion,
/// key: settings.UsageReportingKey,
/// enabled: settings.UsageReportingEnabled);
/// trace.Report("startup", tags: new Dictionary&lt;string, string&gt; { { "version", Version } });
/// trace.Report("startup");
/// ...
/// trace.Dispose(); // on shutdown: sends what is queued, bounded by the timeout
/// </code>
/// </remarks>
public sealed class TraceClient : IDisposable
{
/// <summary>The client version, as sent in the User-Agent header.</summary>
public const string Version = "0.1.0";
public const string Version = "0.2.0";

/// <summary>How many reports may wait to be sent before new ones are dropped.</summary>
public const int QueueCapacity = 256;
Expand Down Expand Up @@ -89,6 +93,7 @@ public sealed class TraceClient : IDisposable

private readonly Uri _endpoint;
private readonly string _application;
private readonly string _version;
private readonly string _key;
private readonly Action<string> _log;
private readonly BlockingCollection<string> _queue; // null when disabled
Expand All @@ -98,18 +103,23 @@ public sealed class TraceClient : IDisposable
private int _closed;

/// <summary>
/// A client for the program named <paramref name="application"/>, reporting
/// to the trace server at <paramref name="baseUrl"/>. Throws only for a
/// missing or malformed <paramref name="baseUrl"/> or a missing
/// <paramref name="application"/> -- programming errors, not runtime ones.
/// A client for the program named <paramref name="application"/>, at
/// <paramref name="version"/>, reporting to the trace server at
/// <paramref name="baseUrl"/>. Throws <see cref="ArgumentException"/> only for a
/// missing or malformed <paramref name="baseUrl"/>, a missing
/// <paramref name="application"/>, or a missing <paramref name="version"/> or
/// one longer than <see cref="MaxLength"/> characters -- programming errors,
/// not runtime ones.
/// </summary>
/// <param name="baseUrl">The trace server, e.g. <c>https://trace.danielstephenson.dev</c>.</param>
/// <param name="application">The program's name, exactly as its key was issued for.</param>
/// <param name="version">The program's own version, trimmed. Sent as the tag <c>version</c>
/// on every event unless the event carries its own.</param>
/// <param name="key">The program's write key. Without one the client is a no-op.</param>
/// <param name="enabled">The program's own opt-out. <c>false</c> yields a client that reports nothing.</param>
/// <param name="log">Where dropped reports are mentioned. Optional; treat as debug-level.</param>
public TraceClient(string baseUrl, string application, string key = null, bool enabled = true,
Action<string> log = null)
public TraceClient(string baseUrl, string application, string version, string key = null,
bool enabled = true, Action<string> log = null)
{
if (string.IsNullOrWhiteSpace(baseUrl))
{
Expand All @@ -119,8 +129,17 @@ public TraceClient(string baseUrl, string application, string key = null, bool e
{
throw new ArgumentException("application is required", "application");
}
if (string.IsNullOrWhiteSpace(version))
{
throw new ArgumentException("version is required", "version");
}
if (version.Trim().Length > MaxLength)
{
throw new ArgumentException("version is longer than " + MaxLength + " characters", "version");
}
_endpoint = new Uri(baseUrl.Trim().TrimEnd('/') + "/api/metrics");
_application = application.Trim();
_version = version.Trim();
_key = key == null ? "" : key.Trim();
_log = log;

Expand Down Expand Up @@ -151,7 +170,7 @@ public TraceClient(string baseUrl, string application, string key = null, bool e
/// <summary>A client that reports nothing. Useful as a default before settings are read.</summary>
public static TraceClient Disabled()
{
return new TraceClient("http://disabled.invalid", "disabled", enabled: false);
return new TraceClient("http://disabled.invalid", "disabled", "disabled", enabled: false);
}

/// <summary>
Expand Down Expand Up @@ -209,7 +228,9 @@ private static bool IsYes(string value)
/// <summary>
/// Reports that <paramref name="name"/> happened, with an optional numeric
/// value and optional string tags. Returns immediately and never throws.
/// A report the server would reject for its size -- more than
/// The program's version is added as the tag <c>version</c> unless
/// <paramref name="tags"/> already has one; <paramref name="tags"/> itself is
/// never modified. A report the server would reject for its size -- more than
/// <see cref="MaxTags"/> tags, or a string longer than <see cref="MaxLength"/> --
/// is dropped here instead of being sent.
/// </summary>
Expand All @@ -222,7 +243,7 @@ public void Report(string name, double? value = null, IEnumerable<KeyValuePair<s
try
{
string body;
string problem = Json(_application, name, value, tags, out body);
string problem = Json(_application, name, value, WithVersion(tags, _version), out body);
if (problem != null)
{
Log("dropped " + name + ": " + problem);
Expand Down Expand Up @@ -345,6 +366,37 @@ private void Log(string message)
}
}

/// <summary>
/// The event's own tags plus <c>version</c>, unless the event already
/// carries one. A copy; the caller's collection is never modified.
/// </summary>
internal static List<KeyValuePair<string, string>> WithVersion(
IEnumerable<KeyValuePair<string, string>> tags, string version)
{
var merged = new List<KeyValuePair<string, string>>();
bool hasVersion = false;
if (tags != null)
{
foreach (KeyValuePair<string, string> tag in tags)
{
if (tag.Key == null || tag.Value == null)
{
continue;
}
if (tag.Key == "version")
{
hasVersion = true;
}
merged.Add(tag);
}
}
if (!hasVersion)
{
merged.Add(new KeyValuePair<string, string>("version", version));
}
return merged;
}

// JSON is written by hand so this file has no dependencies. The shape is
// fixed and small -- three scalars and a flat string map. Returns null
// and sets body when the report is sendable, otherwise the reason it is not.
Expand Down
2 changes: 1 addition & 1 deletion src/TraceClient/TraceClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<LangVersion>7.3</LangVersion>
<AssemblyName>StephensonSoftware.Trace</AssemblyName>
<RootNamespace>StephensonSoftware.Trace</RootNamespace>
<Version>0.1.0</Version>
<Version>0.2.0</Version>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
Expand Down
Loading
Loading