diff --git a/README.md b/README.md index 61d75f8..c837385 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,12 @@ whatever the opt-outs say; passing `installIdFile:` lets the client do it only when reporting is on. A program that already keeps settings can store the ID there instead and pass -it explicitly; `installId:` wins over `installIdFile:`: +it explicitly. `installId:` wins over `installIdFile:`, unless it is null or +blank; then the file is used, if one was given, and otherwise no ID is sent: ```csharp var trace = new TraceClient(url, "my-game", "1.4.0", key: key, - installId: settings.UsageReportingInstallId); // null or blank: none sent + installId: settings.UsageReportingInstallId); ``` An explicit ID is trimmed; one over 255 characters throws `ArgumentException`, @@ -115,12 +116,17 @@ client never generates one, never writes one, and sends nothing. |---|---| | **Returns immediately** | The HTTP call runs on one background thread the client owns. A game loop can report from its main thread and no frame waits on the network. | | **Never throws** | A server that is down, slow, or rejecting the key is a dropped report, not an exception in your program — nor is a logger callback that throws. Drops go to the optional `log` callback, otherwise nowhere. | -| **Bounded** | At most 256 reports wait to be sent; past that, new ones are dropped. A trace server that is unreachable for a week costs a few kilobytes, not your memory. | +| **Bounded** | At most 256 reports wait to be sent; past that, new ones are dropped. A trace server that is unreachable for a week costs those 256 reports, typically tens of kilobytes, not a queue that keeps growing. | | **Within the server's limits** | A report the server would reject for its size — more than 32 tags, or an application, name, tag key or tag value longer than 255 characters — is dropped (and logged) instead of sent. Null or blank tag keys and null tag values are skipped. | | **`Close()` / `Dispose()` drains** | Reports already queued get up to the client timeout (5 s total) to be sent before the thread stops, so a CLI that reports and exits at once does not lose its event. Still bounded: an unreachable server delays exit by at most the timeout plus half a second: at the timeout the in-flight request is cancelled, and the sending thread gets half a second to stop. What is still queued then is dropped, and the `log` callback is told how many. | Each request has a 5 s timeout. +A closed client stays closed: `IsEnabled` is `false`, and `Report` sends nothing +and logs nothing. Closing is not an opt-out, so `DisabledReason` and `InstallId` +keep the values they had. A program that was reporting still sees a `null` +`DisabledReason`, and `IsEnabled` is the way to ask whether reports are going out. + The `log` callback runs on whichever thread hit the problem: the caller's for a report dropped in `Report` and for an installation ID file that fails in the constructor, and the client's background thread for a failed delivery and for diff --git a/src/TraceClient/TraceClient.cs b/src/TraceClient/TraceClient.cs index bb999fe..991299d 100644 --- a/src/TraceClient/TraceClient.cs +++ b/src/TraceClient/TraceClient.cs @@ -131,7 +131,9 @@ public sealed class TraceClient : IDisposable /// longer than characters -- /// programming errors, not runtime ones. /// - /// The trace server, e.g. https://trace.danielstephenson.dev. + /// The trace server, e.g. https://trace.danielstephenson.dev. Only + /// a URL that does not parse is rejected: ftp://host, or localhost:8080 with its + /// scheme left off, makes a client whose every report is a logged drop. /// The program's name, exactly as its key was issued for. Keep it /// ASCII: it is also sent in the User-Agent header, which .NET 5+ refuses to send with /// non-ASCII characters, so every report would be dropped there. @@ -139,14 +141,16 @@ public sealed class TraceClient : IDisposable /// on every event unless the event carries its own. /// The program's write key. Without one the client is a no-op. /// The program's own opt-out. false yields a client that reports nothing. - /// Where dropped reports are mentioned. Optional; treat as debug-level. + /// Where dropped reports and installation ID file failures are mentioned. + /// Optional; treat as debug-level. /// Called on the caller's thread or on the client's background thread, so it must be /// quick and thread-safe. A failed delivery's message includes the report's JSON body. /// The installation's ID, sent as the tag install on every /// event. It should be random -- e.g. a the program stores in its /// own settings -- and never derived from a person, account or address. Trimmed; null - /// or blank means none. Longer than characters throws - /// . Wins over . + /// or blank means none was given, which leaves in charge. + /// Longer than characters throws . + /// Any other value wins over . /// A file, chosen by the program, that holds the installation's /// ID; read or created with , but only when the client is /// enabled, so a disabled client never writes it. Optional; there is no default location. @@ -235,7 +239,9 @@ public static TraceClient Disabled() /// Why sends nothing: null when enabled, otherwise /// , or /// , verbatim, so a program can print - /// "Usage reporting is off (" + reason + ").". Decided once, in the constructor. + /// "Usage reporting is off (" + reason + ").". Decided once, in the constructor: + /// does not change it, so a closed client that was enabled still + /// says null. answers whether reports are going out. /// public string DisabledReason { get; private set; } @@ -333,9 +339,9 @@ public bool IsEnabled /// /// Whether the environment asks for usage reporting to be off, via - /// TRACE_USAGE_REPORTING=off or DO_NOT_TRACK=1. Only the listed - /// values count; anything else (including empty) leaves the program's - /// own setting in charge. + /// or set to one of + /// the values listed there, case-insensitive. Anything else (including empty), or + /// an environment that cannot be read, leaves the program's own setting in charge. /// public static bool EnvironmentDisables() { @@ -379,8 +385,9 @@ private static bool IsYes(string value) /// never modified. A report the server would reject for its size -- more than /// tags, or a string longer than -- /// is dropped here instead of being sent. A null or blank - /// sends nothing and logs nothing. A NaN or infinite is - /// left out of the event, which is still sent. + /// sends nothing and logs nothing, and so does any report on a disabled client or + /// after . A NaN or infinite is left + /// out of the event, which is still sent. /// public void Report(string name, double? value = null, IEnumerable> tags = null) { diff --git a/tests/TraceClient.Tests/TraceClientTest.cs b/tests/TraceClient.Tests/TraceClientTest.cs index f549068..13b23e1 100644 --- a/tests/TraceClient.Tests/TraceClientTest.cs +++ b/tests/TraceClient.Tests/TraceClientTest.cs @@ -1260,7 +1260,8 @@ public void Close_ChangesIsEnabledButNotDisabledReasonOrInstallId() [Fact] public void Close_IsIdempotentAndReportAfterCloseIsANoOp() { - var client = new TraceClient(_server.BaseUrl, "MyCli", "1.2.3", key: "k"); + var logged = new ConcurrentQueue(); + var client = new TraceClient(_server.BaseUrl, "MyCli", "1.2.3", key: "k", log: logged.Enqueue); client.Close(); client.Close(); @@ -1270,6 +1271,7 @@ public void Close_IsIdempotentAndReportAfterCloseIsANoOp() Assert.False(client.IsEnabled); Thread.Sleep(100); Assert.Empty(_server.Received); + Assert.Empty(logged); } } }