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);
}
}
}