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
5 changes: 5 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ QueryWatch packages use the shared telemetry package, but not every package uses

- The main `KeelMatrix.QueryWatch` library uses activation and heartbeat through its session lifecycle.
- The `qwatch` CLI sends an activation event on normal execution, but does not send heartbeat events.
- `qwatch telemetry status`, `qwatch telemetry disable`, and `qwatch telemetry enable` do not emit telemetry.
- `qwatch telemetry disable` writes a repo-local opt-out only when it can safely create or update a qwatch-managed config.
- `qwatch telemetry enable` only removes or neutralizes qwatch-managed repo-local opt-out state.
- QueryWatch-owned repo-local configs use `managedBy: "qwatch"` as the ownership marker.
- Higher-precedence process environment variables still override repo-local config.

QueryWatch does not add product-specific telemetry fields on top of the shared telemetry package behavior documented above. If that changes in a way that affects privacy, this file will be updated.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,24 @@ If a summary is top-N sampled, budgets are evaluated only over those captured ev

<!-- BEGIN:CLI_FLAGS -->
```
Usage:
qwatch --input file.json [options]
qwatch telemetry <status|disable|enable> [options]

Commands:
telemetry status [--json] Show effective telemetry state and repo-local config status for the current repo.
telemetry disable Write a qwatch-managed repo-local telemetry opt-out.
telemetry enable Remove or neutralize qwatch-managed repo-local telemetry opt-out.

Options:
--input <path> Input JSON summary file. (repeatable)
--max-queries N Fail if total query count exceeds N.
--max-average-ms N Fail if average duration exceeds N ms.
--max-total-ms N Fail if total duration exceeds N ms.
--baseline <path> Baseline summary JSON to compare against.
--baseline-allow-percent P Allow +P% regression vs baseline before failing.
--write-baseline Write current aggregated summary to --baseline.
--budget "<pattern>=<max>" Per-pattern query count budget (repeatable). (repeatable)
--budget "<pattern>=<max>" Per-pattern query count budget. (repeatable)
Pattern supports wildcards (*, ?) or prefix with 'regex:' for raw regex.
--require-full-events Fail if input summaries are top-N sampled.
--help Show this help.
Expand All @@ -232,6 +242,7 @@ Multi-file support:
- repeat `--input` to aggregate summaries from multiple test projects
- compare current results against a baseline summary
- write GitHub Actions step summaries automatically when running in CI
- inspect or manage repo-local telemetry opt-out state with `qwatch telemetry status|disable|enable`

## Troubleshooting

Expand All @@ -249,6 +260,8 @@ See:
- [PRIVACY.md](PRIVACY.md) for the QueryWatch-specific summary
- [KeelMatrix.Telemetry README](https://github.com/KeelMatrix/Telemetry#readme) for the maintained telemetry behavior and opt-out details

For the CLI, `qwatch telemetry disable` writes a repo-local opt-out file and `qwatch telemetry enable` removes or neutralizes only qwatch-managed repo-local opt-out state. QueryWatch-owned files use `managedBy: "qwatch"` as the ownership marker. Higher-precedence process environment variables still win, and existing non-qwatch-managed repo-local config is left untouched.

## License

MIT
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using KeelMatrix.Telemetry;

namespace KeelMatrix.QueryWatch {
internal static class QueryWatchTelemetry {
internal static class TelemetryHost {
private static readonly Client Client = new("QueryWatch", typeof(QueryWatchSession));

internal static void TrackActivation() => Client.TrackActivation();
Expand Down
4 changes: 2 additions & 2 deletions src/KeelMatrix.QueryWatch/QueryWatchSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public QueryWatchSession(QueryWatchOptions? options = null) {
Options = options ?? new QueryWatchOptions();
StartedAt = DateTimeOffset.UtcNow;

QueryWatchTelemetry.TrackActivation();
TelemetryHost.TrackActivation();
}

/// <summary>Options for this session.</summary>
Expand Down Expand Up @@ -131,7 +131,7 @@ private QueryWatchReport StopInternal() {

if (Interlocked.CompareExchange(ref _stopped, 1, 0) == 0) {
StoppedAt = now;
QueryWatchTelemetry.TrackHeartbeat();
TelemetryHost.TrackHeartbeat();
}

List<QueryEvent> snapshot;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ public void Within_Tolerance_ExitCode_Ok() {
string baseline = Path.Combine(AppContext.BaseDirectory, "Fixtures", "baseline.json");

(int code, string? stdout, string? stderr) = CliRunner.Run([
"--input", current,
"--baseline", baseline,
"--baseline-allow-percent", "10"
"--input",
current,
"--baseline",
baseline,
"--baseline-allow-percent",
"10"
]);

_ = code.Should().Be(0, stdout + Environment.NewLine + stderr);
Expand All @@ -25,9 +28,12 @@ public void Beyond_Tolerance_ExitCode_BaselineRegression() {
string baseline = Path.Combine(AppContext.BaseDirectory, "Fixtures", "baseline.json");

(int code, string _, string? stderr) = CliRunner.Run([
"--input", current,
"--baseline", baseline,
"--baseline-allow-percent", "10"
"--input",
current,
"--baseline",
baseline,
"--baseline-allow-percent",
"10"
]);

_ = code.Should().Be(5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ public void WriteBaseline_Creates_File_And_Prints_Message() {
try {
// Act
(int code, string? stdout, string? stderr) = CliRunner.Run([
"--input", f,
"--baseline", baselinePath,
"--input",
f,
"--baseline",
baselinePath,
"--write-baseline"
]);

Expand Down
17 changes: 13 additions & 4 deletions tests/KeelMatrix.QueryWatch.Cli.IntegrationTests/CliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ internal static class CliRunner {
private static string? _publishedDir;
private static string? _publishedDll;

public static (int ExitCode, string StdOut, string StdErr) Run(string[] args, (string Key, string Value)[]? env = null) {
public static (int ExitCode, string StdOut, string StdErr) Run(
string[] args,
(string Key, string? Value)[]? env = null,
string? workingDirectory = null) {
string repoRoot = FindRepoRoot();
EnsurePublished(repoRoot, out string? dllPath, out string? workDir);

Expand All @@ -18,16 +21,20 @@ public static (int ExitCode, string StdOut, string StdErr) Run(string[] args, (s
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = workDir
WorkingDirectory = workingDirectory ?? workDir
};

// Run the published app: dotnet <dll> -- <args>
psi.ArgumentList.Add(dllPath);
foreach (string a in args) psi.ArgumentList.Add(a);

if (env is not null) {
foreach (var (k, v) in env)
psi.Environment[k] = v;
foreach (var (k, v) in env) {
if (v is null)
_ = psi.Environment.Remove(k);
else
psi.Environment[k] = v;
}
}

using var proc = Process.Start(psi)!;
Expand Down Expand Up @@ -126,5 +133,7 @@ private static string FindRepoRoot() {
}
return AppContext.BaseDirectory;
}

internal static string GetRepoRoot() => FindRepoRoot();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ public void LibraryJson_MaxQueries_Pass_Then_Fail() {
var json = CreateLibraryJson(totalEvents: 2, sampleTop: 10, out _);

(int okCode, string? okOut, string? okErr) = CliRunner.Run([
"--input", json,
"--max-queries", "3"
"--input",
json,
"--max-queries",
"3"
]);
_ = okCode.Should().Be(0, okOut + Environment.NewLine + okErr);

(int failCode, string _, string? failErr) = CliRunner.Run([
"--input", json,
"--max-queries", "1"
"--input",
json,
"--max-queries",
"1"
]);
_ = failCode.Should().Be(4);
_ = failErr.Should().Contain("Max queries exceeded");
Expand All @@ -54,15 +58,19 @@ public void LibraryJson_PatternBudget_Pass_Then_Fail() {

// Allow exactly the predictable match
(int okCode, string? okOut, string? okErr) = CliRunner.Run([
"--input", json,
"--budget", "SELECT * FROM Users*=1"
"--input",
json,
"--budget",
"SELECT * FROM Users*=1"
]);
_ = okCode.Should().Be(0, okOut + Environment.NewLine + okErr);

// Now disallow it
(int badCode, string _, string? badErr) = CliRunner.Run([
"--input", json,
"--budget", "SELECT * FROM Users*=0"
"--input",
json,
"--budget",
"SELECT * FROM Users*=0"
]);
_ = badCode.Should().Be(4);
_ = badErr.Should().Contain("Budget violations");
Expand All @@ -74,8 +82,10 @@ public void Baseline_Write_Then_Compare_With_Tolerance() {
var baselinePath = Path.Combine(Path.GetTempPath(), "qwatch-baseline-" + Guid.NewGuid().ToString("N"), "baseline.json");

(int writeCode, string? writeOut, string? writeErr) = CliRunner.Run([
"--input", current1,
"--baseline", baselinePath,
"--input",
current1,
"--baseline",
baselinePath,
"--write-baseline"
]);
_ = writeCode.Should().Be(0, writeOut + Environment.NewLine + writeErr);
Expand All @@ -85,17 +95,23 @@ public void Baseline_Write_Then_Compare_With_Tolerance() {

// Generous tolerance -> pass
(int passCode, string? passOut, string? passErr) = CliRunner.Run([
"--input", current2,
"--baseline", baselinePath,
"--baseline-allow-percent", "80"
"--input",
current2,
"--baseline",
baselinePath,
"--baseline-allow-percent",
"80"
]);
_ = passCode.Should().Be(0, passOut + Environment.NewLine + passErr);

// Tight tolerance -> fail with baseline regression code
(int failCode, string _, string? failErr) = CliRunner.Run([
"--input", current2,
"--baseline", baselinePath,
"--baseline-allow-percent", "10"
"--input",
current2,
"--baseline",
baselinePath,
"--baseline-allow-percent",
"10"
]);
_ = failCode.Should().Be(5);
_ = failErr.Should().Contain("Baseline regressions");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public void Missing_Budget_Value_Shows_Parse_Error() {
string f = Path.Combine(AppContext.BaseDirectory, "Fixtures", "pattern.json");

(int code, string? stdout, string? stderr) = CliRunner.Run([
"--input", f,
"--input",
f,
"--budget" // missing value
]);

Expand All @@ -58,8 +59,10 @@ public void Invalid_Budget_Spec_Returns_InvalidArguments() {
string f = Path.Combine(AppContext.BaseDirectory, "Fixtures", "pattern.json");

(int code, string? stdout, string? stderr) = CliRunner.Run([
"--input", f,
"--budget", "not-a-valid-spec" // lacks = and max
"--input",
f,
"--budget",
"not-a-valid-spec" // lacks = and max
]);

_ = code.Should().Be(1, stdout + Environment.NewLine + stderr); // ExitCodes.InvalidArguments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,24 @@ public void Aggregates_Across_Multiple_Files_And_Respects_MaxQueries() {
string f2 = Path.Combine(AppContext.BaseDirectory, "Fixtures", "agg_b.json");

(int exitOk, string? stdoutOk, string? stderrOk) = CliRunner.Run([
"--input", f1,
"--input", f2,
"--max-queries", "5"
"--input",
f1,
"--input",
f2,
"--max-queries",
"5"
]);
_ = exitOk.Should().Be(0, stdoutOk + Environment.NewLine + stderrOk);
_ = stdoutOk.Should().Contain("files 2");
_ = stdoutOk.Should().Contain("Queries: 5");

(int exitFail, string _, string? stderrFail) = CliRunner.Run([
"--input", f1,
"--input", f2,
"--max-queries", "4"
"--input",
f1,
"--input",
f2,
"--max-queries",
"4"
]);
_ = exitFail.Should().Be(4);
_ = stderrFail.Should().Contain("Budget violations:");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ public void Exceeds_Pattern_Budget_Returns_BudgetExceeded() {
string f = Path.Combine(AppContext.BaseDirectory, "Fixtures", "pattern.json");
(int code, string? stdout, string? stderr) = CliRunner.Run(
[
"--input", f,
"--budget", "SELECT * FROM Users*=1"
"--input",
f,
"--budget",
"SELECT * FROM Users*=1"
]);

// For pattern budgets the CLI returns 4 when the count exceeds the budget.
Expand All @@ -26,8 +28,10 @@ public void Meets_Pattern_Budget_Returns_Ok() {
string f = Path.Combine(AppContext.BaseDirectory, "Fixtures", "pattern.json");
(int code, string? stdout, string? stderr) = CliRunner.Run(
[
"--input", f,
"--budget", "SELECT * FROM Users*=2"
"--input",
f,
"--budget",
"SELECT * FROM Users*=2"
]);

_ = code.Should().Be(0, stdout + Environment.NewLine + stderr);
Expand Down
Loading
Loading