@mondaydotcomorg/z2h-cli 0.25.4 → 0.25.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/auth.d.ts +2 -1
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +2 -1
  4. package/dist/commands/__tests__/build.test.d.ts +2 -0
  5. package/dist/commands/__tests__/build.test.d.ts.map +1 -0
  6. package/dist/commands/backend.d.ts.map +1 -1
  7. package/dist/commands/backend.js +7 -1
  8. package/dist/commands/build.d.ts.map +1 -1
  9. package/dist/commands/build.js +10 -0
  10. package/dist/commands/deploy.d.ts.map +1 -1
  11. package/dist/commands/deploy.js +42 -19
  12. package/dist/commands/grant.d.ts.map +1 -1
  13. package/dist/commands/grant.js +8 -7
  14. package/dist/esm/auth.d.ts +2 -1
  15. package/dist/esm/auth.d.ts.map +1 -1
  16. package/dist/esm/auth.mjs +2 -1
  17. package/dist/esm/commands/__tests__/build.test.d.ts +2 -0
  18. package/dist/esm/commands/__tests__/build.test.d.ts.map +1 -0
  19. package/dist/esm/commands/backend.d.ts.map +1 -1
  20. package/dist/esm/commands/backend.mjs +7 -1
  21. package/dist/esm/commands/build.d.ts.map +1 -1
  22. package/dist/esm/commands/build.mjs +10 -0
  23. package/dist/esm/commands/deploy.d.ts.map +1 -1
  24. package/dist/esm/commands/deploy.mjs +43 -20
  25. package/dist/esm/commands/grant.d.ts.map +1 -1
  26. package/dist/esm/commands/grant.mjs +8 -7
  27. package/dist/esm/index.mjs +41 -140
  28. package/dist/esm/types.d.ts +23 -0
  29. package/dist/esm/types.d.ts.map +1 -1
  30. package/dist/esm/util/__tests__/errors.test.d.ts +2 -0
  31. package/dist/esm/util/__tests__/errors.test.d.ts.map +1 -0
  32. package/dist/esm/util/__tests__/run-command.test.d.ts +2 -0
  33. package/dist/esm/util/__tests__/run-command.test.d.ts.map +1 -0
  34. package/dist/esm/util/auth/auth-store.d.ts +0 -4
  35. package/dist/esm/util/auth/auth-store.d.ts.map +1 -1
  36. package/dist/esm/util/auth/auth-store.mjs +2 -9
  37. package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -1
  38. package/dist/esm/util/auth/broker-credential-provider.mjs +16 -16
  39. package/dist/esm/util/broker/app.d.ts.map +1 -1
  40. package/dist/esm/util/broker/app.mjs +17 -9
  41. package/dist/esm/util/errors.d.ts +48 -0
  42. package/dist/esm/util/errors.d.ts.map +1 -0
  43. package/dist/esm/util/errors.mjs +91 -0
  44. package/dist/esm/util/git/repo.d.ts +0 -3
  45. package/dist/esm/util/git/repo.d.ts.map +1 -1
  46. package/dist/esm/util/git/repo.mjs +4 -9
  47. package/dist/esm/util/logger.d.ts +4 -1
  48. package/dist/esm/util/logger.d.ts.map +1 -1
  49. package/dist/esm/util/logger.mjs +11 -3
  50. package/dist/esm/util/run-command.d.ts +16 -0
  51. package/dist/esm/util/run-command.d.ts.map +1 -0
  52. package/dist/esm/util/run-command.mjs +47 -0
  53. package/dist/esm/util/s3/upload.d.ts.map +1 -1
  54. package/dist/esm/util/s3/upload.mjs +22 -16
  55. package/dist/esm/util/tracker.d.ts +9 -2
  56. package/dist/esm/util/tracker.d.ts.map +1 -1
  57. package/dist/esm/util/tracker.mjs +18 -15
  58. package/dist/index.js +40 -139
  59. package/dist/types.d.ts +23 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/util/__tests__/errors.test.d.ts +2 -0
  62. package/dist/util/__tests__/errors.test.d.ts.map +1 -0
  63. package/dist/util/__tests__/run-command.test.d.ts +2 -0
  64. package/dist/util/__tests__/run-command.test.d.ts.map +1 -0
  65. package/dist/util/auth/auth-store.d.ts +0 -4
  66. package/dist/util/auth/auth-store.d.ts.map +1 -1
  67. package/dist/util/auth/auth-store.js +2 -10
  68. package/dist/util/auth/broker-credential-provider.d.ts.map +1 -1
  69. package/dist/util/auth/broker-credential-provider.js +16 -16
  70. package/dist/util/broker/app.d.ts.map +1 -1
  71. package/dist/util/broker/app.js +17 -9
  72. package/dist/util/errors.d.ts +48 -0
  73. package/dist/util/errors.d.ts.map +1 -0
  74. package/dist/util/errors.js +102 -0
  75. package/dist/util/git/repo.d.ts +0 -3
  76. package/dist/util/git/repo.d.ts.map +1 -1
  77. package/dist/util/git/repo.js +4 -10
  78. package/dist/util/logger.d.ts +4 -1
  79. package/dist/util/logger.d.ts.map +1 -1
  80. package/dist/util/logger.js +11 -3
  81. package/dist/util/run-command.d.ts +16 -0
  82. package/dist/util/run-command.d.ts.map +1 -0
  83. package/dist/util/run-command.js +49 -0
  84. package/dist/util/s3/upload.d.ts.map +1 -1
  85. package/dist/util/s3/upload.js +22 -16
  86. package/dist/util/tracker.d.ts +9 -2
  87. package/dist/util/tracker.d.ts.map +1 -1
  88. package/dist/util/tracker.js +19 -14
  89. package/handler-api/backend-runner/channels/channel-map.ts +6 -12
  90. package/handler-api/backend-runner/channels/channel-telemetry.ts +79 -0
  91. package/handler-api/backend-runner/channels/llm-channel/llm.channel.ts +3 -2
  92. package/handler-api/backend-runner/channels/snowflake-channel/run-query.ts +21 -7
  93. package/handler-api/backend-runner/channels/snowflake-channel/snowflake.channel.ts +14 -2
  94. package/handler-api/backend-runner/channels/snowflake-channel/stateless-part-b.ts +1 -4
  95. package/package.json +2 -2
  96. package/src/auth.ts +2 -1
  97. package/src/commands/__tests__/backend-deploy.test.ts +18 -0
  98. package/src/commands/__tests__/build.test.ts +83 -0
  99. package/src/commands/__tests__/deploy.test.ts +107 -0
  100. package/src/commands/backend.ts +6 -1
  101. package/src/commands/build.ts +10 -0
  102. package/src/commands/deploy.ts +42 -20
  103. package/src/commands/grant.ts +8 -7
  104. package/src/index.ts +84 -133
  105. package/src/types.ts +47 -0
  106. package/src/util/__tests__/errors.test.ts +90 -0
  107. package/src/util/__tests__/run-command.test.ts +98 -0
  108. package/src/util/auth/__tests__/auth-store.test.ts +2 -1
  109. package/src/util/auth/__tests__/broker-credential-provider.test.ts +44 -8
  110. package/src/util/auth/auth-store.ts +1 -7
  111. package/src/util/auth/broker-credential-provider.ts +18 -16
  112. package/src/util/broker/__tests__/app.test.ts +23 -20
  113. package/src/util/broker/app.ts +16 -9
  114. package/src/util/errors.ts +98 -0
  115. package/src/util/git/__tests__/repo.test.ts +5 -3
  116. package/src/util/git/repo.ts +3 -9
  117. package/src/util/logger.ts +16 -3
  118. package/src/util/run-command.ts +59 -0
  119. package/src/util/s3/upload.ts +27 -22
  120. package/src/util/tracker.ts +19 -19
@@ -0,0 +1,102 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ /**
4
+ * Base for every error the CLI classifies. runCommand in ./run-command.ts reads `reason`
5
+ * and `data` straight off the error, so the throw site is the only place that needs to know
6
+ * why it failed. Nothing re-derives it from the error type at the top level.
7
+ *
8
+ * `exitCode` lets a throw site override the CLI's --help contract, 1 for user-resolvable
9
+ * and 2 for system failures, per error instead of per command. Nothing sets it yet. The
10
+ * command table still pins exit codes, which keeps today's behavior.
11
+ */
12
+ class Z2hError extends Error {
13
+ reason;
14
+ data;
15
+ exitCode;
16
+ constructor(message, reason, data, exitCode = 1) {
17
+ super(message);
18
+ this.reason = reason;
19
+ this.data = data;
20
+ this.exitCode = exitCode;
21
+ this.name = new.target.name;
22
+ }
23
+ }
24
+ /** Tags a deploy/preview stage failure with a bounded reason code instead of a freeform message. */
25
+ class DeployError extends Z2hError {
26
+ reason;
27
+ constructor(reason, message, data) {
28
+ super(message, reason, data);
29
+ this.reason = reason;
30
+ }
31
+ }
32
+ /**
33
+ * Tags a grant/revoke/transfer-owner failure with a bounded reason code instead of a freeform message.
34
+ * `reason` also accepts a raw HTTP status, which gets mapped to a bounded reason internally.
35
+ */
36
+ class GrantError extends Z2hError {
37
+ constructor(reason, message, data) {
38
+ super(message, typeof reason === 'number' ? grantReasonForStatus(reason) : reason, data);
39
+ }
40
+ }
41
+ /** No z2h JWT found — see auth-store.ts for the discovery order (env var, then ~/.zth/auth.json). */
42
+ class Z2hAuthMissingError extends Z2hError {
43
+ authFilePath;
44
+ constructor(authFilePath) {
45
+ super(`No Z2H deploy token found at ${authFilePath}. Please run 'z2h-cli generate-z2h-token' command.`, 'read_auth_token');
46
+ this.authFilePath = authFilePath;
47
+ }
48
+ }
49
+ /** A git pull diverged or a push was rejected; `reason` distinguishes which stage hit it. */
50
+ class GitConflictError extends Z2hError {
51
+ constructor(message, reason = 'git_error') {
52
+ super(message, reason);
53
+ }
54
+ }
55
+ /** Maps an HTTP status to a bounded grant/revoke/transfer-owner failure reason. */
56
+ function grantReasonForStatus(status) {
57
+ // 401 is an expired/invalid token (re-auth fixes it); 403 is a real permission denial.
58
+ if (status === 401) {
59
+ return 'auth';
60
+ }
61
+ if (status === 403) {
62
+ return 'forbidden';
63
+ }
64
+ if (status === 404) {
65
+ return 'not_found';
66
+ }
67
+ if (status >= 500) {
68
+ return 'server_error';
69
+ }
70
+ return 'unknown';
71
+ }
72
+ /** Tags a failure with `reason`, unless `err` is already classified; `signal` overrides `reason` for a killed child process. */
73
+ function tagDeployError(reason, err) {
74
+ if (err instanceof Z2hError) {
75
+ return err;
76
+ }
77
+ const signal = err instanceof Error ? err.signal : undefined;
78
+ return new DeployError(signal ? 'interrupted' : reason, messageOf(err));
79
+ }
80
+ /** Reads the bounded reason off a classified error; anything unclassified is `unknown`. */
81
+ function reasonOf(err) {
82
+ return err instanceof Z2hError ? err.reason : 'unknown';
83
+ }
84
+ /** Extra telemetry dimensions the throw site attached, if any. */
85
+ function dataOf(err) {
86
+ return err instanceof Z2hError ? err.data ?? {} : {};
87
+ }
88
+ /** Safe message for any thrown value. A non-Error throw must not break the error path. */
89
+ function messageOf(err) {
90
+ return err instanceof Error ? err.message : String(err);
91
+ }
92
+
93
+ exports.DeployError = DeployError;
94
+ exports.GitConflictError = GitConflictError;
95
+ exports.GrantError = GrantError;
96
+ exports.Z2hAuthMissingError = Z2hAuthMissingError;
97
+ exports.Z2hError = Z2hError;
98
+ exports.dataOf = dataOf;
99
+ exports.grantReasonForStatus = grantReasonForStatus;
100
+ exports.messageOf = messageOf;
101
+ exports.reasonOf = reasonOf;
102
+ exports.tagDeployError = tagDeployError;
@@ -1,6 +1,3 @@
1
- export declare class GitConflictError extends Error {
2
- constructor(message: string);
3
- }
4
1
  export interface ConflictInfo {
5
2
  localCommits: string[];
6
3
  remoteCommits: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAkBA,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5G;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7F;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../../src/util/git/repo.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzE;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAI3E;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/D;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5G;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAclE;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B7F;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB3F;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7D;AAED,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG5D"}
@@ -5,6 +5,7 @@ const fs = require('fs-extra');
5
5
  const execa = require('execa');
6
6
  const constants = require('../../constants.js');
7
7
  const util_appName = require('../app-name.js');
8
+ const util_errors = require('../errors.js');
8
9
 
9
10
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
10
11
 
@@ -19,12 +20,6 @@ async function execGitLines(args, dir, env) {
19
20
  .then(r => r.stdout.split('\n').filter(Boolean))
20
21
  .catch(() => []);
21
22
  }
22
- class GitConflictError extends Error {
23
- constructor(message) {
24
- super(message);
25
- this.name = 'GitConflictError';
26
- }
27
- }
28
23
  const REQUIRED_GITIGNORE_ENTRIES = ['node_modules/', '.zth/', 'build/'];
29
24
  const LOCAL_HOOKS_DIR = '.zth/hooks';
30
25
  function defaultS3Remote(app) {
@@ -98,7 +93,7 @@ async function pullAndResolve(dir, env) {
98
93
  }
99
94
  await safeBackupIfDirty(dir);
100
95
  const info = await gatherConflictInfo(dir, gitEnv);
101
- throw new GitConflictError(`Pull rejected: Git conflict detected: ${JSON.stringify(info)}`);
96
+ throw new util_errors.GitConflictError(`Pull rejected: Git conflict detected: ${JSON.stringify(info)}`);
102
97
  }
103
98
  }
104
99
  /**
@@ -126,9 +121,9 @@ async function pushToRemote(dir, env) {
126
121
  }
127
122
  if (isRejected) {
128
123
  const info = await gatherConflictInfo(dir, gitEnv);
129
- throw new GitConflictError(`Push rejected: Git conflict detected: ${JSON.stringify(info)}`);
124
+ throw new util_errors.GitConflictError(`Push rejected: Git conflict detected: ${JSON.stringify(info)}`, 'push_error');
130
125
  }
131
- throw err;
126
+ throw new util_errors.DeployError('push_error', util_errors.messageOf(err));
132
127
  }
133
128
  }
134
129
  async function remoteHead(dir) {
@@ -183,7 +178,6 @@ async function ensureGitignore(dir) {
183
178
  await fs__default.default.outputFile(gitignorePath, content + separator + missing.join('\n') + '\n');
184
179
  }
185
180
 
186
- exports.GitConflictError = GitConflictError;
187
181
  exports.cloneRepo = cloneRepo;
188
182
  exports.defaultS3Remote = defaultS3Remote;
189
183
  exports.getRemoteUrl = getRemoteUrl;
@@ -17,6 +17,9 @@ export declare function output(human: string, data: unknown): void;
17
17
  /**
18
18
  * Emit a structured failure payload (JSON mode) or a red error line (human mode),
19
19
  * then exit with the given code.
20
+ *
21
+ * `track: false` skips the telemetry event but still prints and exits. Commands with no
22
+ * success event pass it, so their failures don't skew per-command error rates.
20
23
  */
21
- export declare function fail(message: string, payload: Record<string, unknown>, exitCode?: number): Promise<never>;
24
+ export declare function fail(message: string, payload: Record<string, unknown>, exitCode?: number, track?: boolean): Promise<never>;
22
25
  //# sourceMappingURL=logger.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/util/logger.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAI1C,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAEpD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI1C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAMzD;AAED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAW1G"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/util/logger.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAI1C,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAEpD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAItC;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI1C;AAED;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAMzD;AAED;;;;;;GAMG;AACH,wBAAsB,IAAI,CACxB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,QAAQ,SAAI,EACZ,KAAK,UAAO,GACX,OAAO,CAAC,KAAK,CAAC,CAehB"}
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const pc = require('picocolors');
4
+ const observability = require('@mondaydotcomorg/z2h-shared-utils/observability');
4
5
  const util_tracker = require('./tracker.js');
5
6
 
6
7
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -49,10 +50,15 @@ function output(human, data) {
49
50
  /**
50
51
  * Emit a structured failure payload (JSON mode) or a red error line (human mode),
51
52
  * then exit with the given code.
53
+ *
54
+ * `track: false` skips the telemetry event but still prints and exits. Commands with no
55
+ * success event pass it, so their failures don't skew per-command error rates.
52
56
  */
53
- async function fail(message, payload, exitCode = 1) {
57
+ async function fail(message, payload, exitCode = 1, track = true) {
58
+ // Canonical fields go last so a payload key can never shadow `error`/`action`.
59
+ const { command, ...rest } = payload;
54
60
  if (mode === 'json') {
55
- process.stdout.write(`${JSON.stringify({ ok: false, error: message, ...payload })}\n`);
61
+ process.stdout.write(`${JSON.stringify({ ok: false, ...payload, error: message })}\n`);
56
62
  }
57
63
  else {
58
64
  process.stderr.write(pc__default.default.red(`[z2h-cli] ${message}\n`));
@@ -60,7 +66,9 @@ async function fail(message, payload, exitCode = 1) {
60
66
  process.stderr.write(pc__default.default.dim(` → ${payload.suggestedCommand}\n`));
61
67
  }
62
68
  }
63
- await util_tracker.trackEvent('z2h_cli_error', { action: payload.command ?? 'unknown', error: message });
69
+ if (track) {
70
+ await util_tracker.trackEvent(observability.Z2hCliEvents.error, { ...rest, action: command ?? 'unknown', error: message });
71
+ }
64
72
  process.exit(exitCode);
65
73
  }
66
74
 
@@ -0,0 +1,16 @@
1
+ import type { Command } from 'commander';
2
+ import type { CommandSpec } from '../types';
3
+ /**
4
+ * Wraps a command body with the whole telemetry and failure contract so no command
5
+ * hand-rolls it. It times the happy path. On a throw it reads `reason` and `data` straight
6
+ * off the error, defined in ./errors, instead of re-deriving them per command.
7
+ *
8
+ * Returns a commander action handler, chainable straight off `.command(...)` — commander always
9
+ * appends the Command instance as the action handler's last argument, so this splits it back off
10
+ * to read the command's own name/parent instead of asking the caller to pass it in.
11
+ *
12
+ * NoInfer keeps the arg types coming from `fn` alone. A `data` or `label` callback that
13
+ * only needs the first arg must not narrow the handler's own signature.
14
+ */
15
+ export declare function runCommand<A extends unknown[]>(spec: CommandSpec<NoInfer<A>>, fn: (...args: A) => Promise<unknown>): (...args: [...A, Command]) => Promise<void>;
16
+ //# sourceMappingURL=run-command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../../src/util/run-command.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,UAAU,CAAC;AAcrD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,OAAO,EAAE,EAC5C,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAC7B,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,GACnC,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAyB7C"}
@@ -0,0 +1,49 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const util_logger = require('./logger.js');
4
+ const util_tracker = require('./tracker.js');
5
+ const util_errors = require('./errors.js');
6
+
7
+ /** A spec field is a constant, a callback over the args, or absent — in which case `fallback` applies. */
8
+ const resolveSpecField = (field, args, fallback) => {
9
+ if (field === undefined) {
10
+ return fallback;
11
+ }
12
+ return typeof field === 'function' ? field(args) : field;
13
+ };
14
+ /**
15
+ * Wraps a command body with the whole telemetry and failure contract so no command
16
+ * hand-rolls it. It times the happy path. On a throw it reads `reason` and `data` straight
17
+ * off the error, defined in ./errors, instead of re-deriving them per command.
18
+ *
19
+ * Returns a commander action handler, chainable straight off `.command(...)` — commander always
20
+ * appends the Command instance as the action handler's last argument, so this splits it back off
21
+ * to read the command's own name/parent instead of asking the caller to pass it in.
22
+ *
23
+ * NoInfer keeps the arg types coming from `fn` alone. A `data` or `label` callback that
24
+ * only needs the first arg must not narrow the handler's own signature.
25
+ */
26
+ function runCommand(spec, fn) {
27
+ return async (...allArgs) => {
28
+ const cmd = allArgs[allArgs.length - 1];
29
+ const args = allArgs.slice(0, -1);
30
+ const name = cmd.parent ? `${cmd.parent.name()} ${cmd.name()}` : cmd.name();
31
+ const resolve = (v, fallback) => resolveSpecField(v, args, fallback);
32
+ const track = resolve(spec.track, true);
33
+ const label = resolve(spec.label, name);
34
+ const event = resolve(spec.event, util_tracker.successEvent(name));
35
+ const data = spec.data?.(args) ?? {};
36
+ try {
37
+ await (track ? util_tracker.trackDuration(event, () => fn(...args), data) : fn(...args));
38
+ }
39
+ catch (err) {
40
+ const reason = util_errors.reasonOf(err);
41
+ // A deploy/preview that failed because the consumer's own build broke isn't a CLI/infra
42
+ // problem — don't report it at all, not even as `unknown`.
43
+ const shouldTrack = track && reason !== 'build_error';
44
+ await util_logger.fail(`${label} failed: ${util_errors.messageOf(err)}`, { command: label, reason, ...data, ...util_errors.dataOf(err) }, spec.exitCode ?? (err instanceof util_errors.Z2hError ? err.exitCode : 1), shouldTrack);
45
+ }
46
+ };
47
+ }
48
+
49
+ exports.runCommand = runCommand;
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/util/s3/upload.ts"],"names":[],"mappings":"AAeA,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,MAA0B,GACjC,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BnB"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/util/s3/upload.ts"],"names":[],"mappings":"AAgBA,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,MAA0B,GACjC,OAAO,CAAC,MAAM,EAAE,CAAC,CA8BnB"}
@@ -7,6 +7,7 @@ const mimeTypes = require('mime-types');
7
7
  const pMap = require('p-map');
8
8
  const clientS3 = require('@aws-sdk/client-s3');
9
9
  const constants = require('../../constants.js');
10
+ const util_errors = require('../errors.js');
10
11
  const util_s3_client = require('./client.js');
11
12
 
12
13
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -19,22 +20,27 @@ const pMap__default = /*#__PURE__*/_interopDefault(pMap);
19
20
  const FOREVER = 'public, immutable';
20
21
  const UPLOAD_CONCURRENCY = 5;
21
22
  async function uploadDir(localDir, keyPrefix, bucket = constants.BUCKET_WRITE_NAME) {
22
- const entries = await promises.readdir(localDir, { recursive: true, withFileTypes: true });
23
- const files = entries.filter(e => e.isFile()).map(e => path__default.default.relative(localDir, path__default.default.join(e.parentPath, e.name)));
24
- const s3 = util_s3_client.getS3Client();
25
- await pMap__default.default(files, async (rel) => {
26
- const body = await fs.readFile(path__default.default.join(localDir, rel));
27
- const contentType = mimeTypes.lookup(rel) || 'application/octet-stream';
28
- const key = `${keyPrefix}/${rel.split(path__default.default.sep).join('/')}`;
29
- await s3.send(new clientS3.PutObjectCommand({
30
- Bucket: bucket,
31
- Key: key,
32
- Body: body,
33
- ContentType: contentType,
34
- CacheControl: FOREVER,
35
- }));
36
- }, { concurrency: UPLOAD_CONCURRENCY });
37
- return files.map(f => f.split(path__default.default.sep).join('/'));
23
+ try {
24
+ const entries = await promises.readdir(localDir, { recursive: true, withFileTypes: true });
25
+ const files = entries.filter(e => e.isFile()).map(e => path__default.default.relative(localDir, path__default.default.join(e.parentPath, e.name)));
26
+ const s3 = util_s3_client.getS3Client();
27
+ await pMap__default.default(files, async (rel) => {
28
+ const body = await fs.readFile(path__default.default.join(localDir, rel));
29
+ const contentType = mimeTypes.lookup(rel) || 'application/octet-stream';
30
+ const key = `${keyPrefix}/${rel.split(path__default.default.sep).join('/')}`;
31
+ await s3.send(new clientS3.PutObjectCommand({
32
+ Bucket: bucket,
33
+ Key: key,
34
+ Body: body,
35
+ ContentType: contentType,
36
+ CacheControl: FOREVER,
37
+ }));
38
+ }, { concurrency: UPLOAD_CONCURRENCY });
39
+ return files.map(f => f.split(path__default.default.sep).join('/'));
40
+ }
41
+ catch (err) {
42
+ throw util_errors.tagDeployError('s3_error', err);
43
+ }
38
44
  }
39
45
 
40
46
  exports.uploadDir = uploadDir;
@@ -1,3 +1,10 @@
1
- export declare function trackEvent(name: string, data: Record<string, unknown>): Promise<void>;
2
- export declare function trackDuration<T>(eventName: string, fn: () => Promise<T>, data?: Record<string, unknown>): Promise<T>;
1
+ export declare const trackEvent: (name: string, data: Record<string, unknown>) => Promise<void>, trackDuration: <T>(name: string, fn: () => Promise<T>, data?: Record<string, unknown>) => Promise<T>;
2
+ /**
3
+ * `z2h_cli_<command>_success` — the success-event name for every command, derived by
4
+ * util/run-command.ts's `runCommand()` so no command has to choose one. Nested commands join
5
+ * their path with `_` (`backend invoke` → `z2h_cli_backend_invoke_success`). The one place a
6
+ * command names its own event, `deploy --preview`, still goes through this function so the
7
+ * convention holds everywhere. See CommandSpec's `event` field in types.ts.
8
+ */
9
+ export declare function successEvent(command: string): string;
3
10
  //# sourceMappingURL=tracker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/util/tracker.ts"],"names":[],"mappings":"AASA,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAS3F;AAED,wBAAsB,aAAa,CAAC,CAAC,EACnC,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,CAAC,CAAC,CAKZ"}
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../src/util/tracker.ts"],"names":[],"mappings":"AAcA,eAAO,MAAQ,UAAU,kEAAE,aAAa,uFAItC,CAAC;AAEH;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpD"}
@@ -1,7 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const os = require('node:os');
4
+ const node_module = require('node:module');
5
+ const observability = require('@mondaydotcomorg/z2h-shared-utils/observability');
4
6
 
7
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
5
8
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
6
9
 
7
10
  const os__default = /*#__PURE__*/_interopDefault(os);
@@ -9,22 +12,24 @@ const os__default = /*#__PURE__*/_interopDefault(os);
9
12
  const DOORMAN_URL = 'https://track-int.bigbrain.me/prod/event';
10
13
  const DOORMAN_URL_STAGING = 'https://track-int.bigbrainstaging.me/stage/event';
11
14
  const username = os__default.default.userInfo().username;
15
+ const { version: cliVersion } = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('util/tracker.js', document.baseURI).href)))('@mondaydotcomorg/z2h-cli/package.json');
12
16
  const isStaging = process.env.NODE_ENV === 'staging';
13
- async function trackEvent(name, data) {
14
- const doormanUrl = isStaging ? DOORMAN_URL_STAGING : DOORMAN_URL;
15
- await fetch(doormanUrl, {
16
- method: 'POST',
17
- headers: { 'Content-Type': 'application/json' },
18
- body: JSON.stringify({ name, source: 'z2h-cli', data: { user_name: username, ...data } }),
19
- signal: AbortSignal.timeout(5000),
20
- }).catch(() => { });
21
- }
22
- async function trackDuration(eventName, fn, data) {
23
- const start = Date.now();
24
- const result = await fn();
25
- await trackEvent(eventName, { ...data, duration_ms: Date.now() - start });
26
- return result;
17
+ const { trackEvent, trackDuration } = observability.createHttpTracker({
18
+ url: isStaging ? DOORMAN_URL_STAGING : DOORMAN_URL,
19
+ source: 'z2h-cli',
20
+ getContext: () => ({ user_name: username, cli_version: cliVersion }),
21
+ });
22
+ /**
23
+ * `z2h_cli_<command>_success` — the success-event name for every command, derived by
24
+ * util/run-command.ts's `runCommand()` so no command has to choose one. Nested commands join
25
+ * their path with `_` (`backend invoke` → `z2h_cli_backend_invoke_success`). The one place a
26
+ * command names its own event, `deploy --preview`, still goes through this function so the
27
+ * convention holds everywhere. See CommandSpec's `event` field in types.ts.
28
+ */
29
+ function successEvent(command) {
30
+ return `z2h_cli_${command.replace(/[\s-]+/g, '_')}_success`;
27
31
  }
28
32
 
33
+ exports.successEvent = successEvent;
29
34
  exports.trackDuration = trackDuration;
30
35
  exports.trackEvent = trackEvent;
@@ -1,5 +1,6 @@
1
- import { logger, metric } from '@mondaydotcomorg/trident-backend-runtime';
1
+ import { logger } from '@mondaydotcomorg/trident-backend-runtime';
2
2
 
3
+ import { withChannelTelemetry } from './channel-telemetry';
3
4
  import { makeDbChannel } from './db-channel/db.channel';
4
5
  import { makeLlmChannel } from './llm-channel/llm.channel';
5
6
  import { makeMondayChannel } from './monday-channel/monday.channel';
@@ -56,8 +57,9 @@ const OAUTH_CHANNELS: Record<string, (caller: Caller) => ChannelMap> = {
56
57
  * `monday_not_connected`, `monday_auth`, … A `4xx` means the handler (or the user's
57
58
  * setup) is wrong and retrying won't help; `502` means the upstream failed.
58
59
  *
59
- * Each key is wrapped with `z2h.channel.<key>` duration + error metrics, tagged by app.
60
- * Failures also reach BI generically at the invoke level — see `runner.service.ts`.
60
+ * Each key emits a unified call counter, duration distribution, and BI event tagged
61
+ * by app, channel, method, and outcome. Uncaught failures also reach BI at the
62
+ * invocation level — see `runner.service.ts`.
61
63
  */
62
64
  export function buildChannels(appName: string, caller: Caller, declaredIntegrations: string[]): ChannelMap {
63
65
  const channels: ChannelMap = {
@@ -78,13 +80,5 @@ export function buildChannels(appName: string, caller: Caller, declaredIntegrati
78
80
  }
79
81
  }
80
82
 
81
- return Object.fromEntries(
82
- Object.entries(channels).map(([key, fn]) => [
83
- key,
84
- metric.wrapFunctionWithMetrics(fn, {
85
- customName: `z2h.channel.${key}`,
86
- tags: { highCardinalityTags: { appName } },
87
- }),
88
- ]),
89
- );
83
+ return Object.fromEntries(Object.entries(channels).map(([key, fn]) => [key, withChannelTelemetry(appName, key, fn)]));
90
84
  }
@@ -0,0 +1,79 @@
1
+ import { metric } from '@mondaydotcomorg/trident-backend-runtime';
2
+ import { BackendRunnerEvents } from '@mondaydotcomorg/z2h-shared-utils/observability';
3
+ import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trident';
4
+
5
+ import { ChannelError } from '../backend-runner.errors';
6
+ import type { ChannelHandler } from '../backend-runner.types';
7
+ import { BI_SOURCE } from '../constants';
8
+
9
+ type ChannelOutcome = 'success' | 'failure';
10
+
11
+ function parseChannelKey(key: string): { channel: string; method: string } {
12
+ const parts = key.split('.');
13
+ if (parts.length !== 2 || parts.some((part) => !part)) {
14
+ throw new Error(`channel key must use <channel>.<method>: ${key}`);
15
+ }
16
+ return { channel: parts[0], method: parts[1] };
17
+ }
18
+
19
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
20
+ return (
21
+ (typeof value === 'object' || typeof value === 'function') &&
22
+ value !== null &&
23
+ typeof (value as PromiseLike<unknown>).then === 'function'
24
+ );
25
+ }
26
+
27
+ function recordChannelCall(
28
+ appName: string,
29
+ channel: string,
30
+ method: string,
31
+ outcome: ChannelOutcome,
32
+ startedAt: number,
33
+ error?: unknown,
34
+ ): void {
35
+ const durationMs = performance.now() - startedAt;
36
+ const dimensions = { appName, channel, method };
37
+
38
+ metric.increment(BackendRunnerEvents.channel.callMetric, { ...dimensions, outcome });
39
+ metric.distribution(BackendRunnerEvents.channel.durationMetric, durationMs, dimensions);
40
+ void trackEvent(
41
+ BackendRunnerEvents.channel.biEvent,
42
+ {
43
+ ...dimensions,
44
+ outcome,
45
+ duration_ms: durationMs,
46
+ ...(error instanceof ChannelError ? { error_code: error.code } : {}),
47
+ },
48
+ BI_SOURCE,
49
+ );
50
+ }
51
+
52
+ export function withChannelTelemetry(appName: string, key: string, handler: ChannelHandler): ChannelHandler {
53
+ const { channel, method } = parseChannelKey(key);
54
+
55
+ return (args) => {
56
+ const startedAt = performance.now();
57
+ try {
58
+ const result = handler(args);
59
+ if (isPromiseLike(result)) {
60
+ return Promise.resolve(result).then(
61
+ (value) => {
62
+ recordChannelCall(appName, channel, method, 'success', startedAt);
63
+ return value;
64
+ },
65
+ (error: unknown) => {
66
+ recordChannelCall(appName, channel, method, 'failure', startedAt, error);
67
+ throw error;
68
+ },
69
+ );
70
+ }
71
+
72
+ recordChannelCall(appName, channel, method, 'success', startedAt);
73
+ return result;
74
+ } catch (error) {
75
+ recordChannelCall(appName, channel, method, 'failure', startedAt, error);
76
+ throw error;
77
+ }
78
+ };
79
+ }
@@ -1,6 +1,7 @@
1
1
  import { logger } from '@mondaydotcomorg/trident-backend-runtime';
2
2
  import { createMondayModel } from '@mondaydotcomorg/kong-ai-gateway-sdk/langchain';
3
- import { BackendRunnerEvents, trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability';
3
+ import { BackendRunnerEvents } from '@mondaydotcomorg/z2h-shared-utils/observability';
4
+ import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trident';
4
5
 
5
6
  import { ChannelError } from '../../backend-runner.errors';
6
7
  import { BI_SOURCE } from '../../constants';
@@ -209,7 +210,7 @@ export function makeLlmChannel(appName: string, caller: Caller) {
209
210
  LLM_TIMEOUT_MS,
210
211
  'LLM gateway',
211
212
  );
212
- trackEvent(BackendRunnerEvents.llm.call, { appName, email: caller.email, modelId: model }, BI_SOURCE);
213
+ void trackEvent(BackendRunnerEvents.llm.call, { appName, email: caller.email, modelId: model }, BI_SOURCE);
213
214
  return { content: extractText(response.content) };
214
215
  } catch (err) {
215
216
  throw toLlmChannelError(err);
@@ -1,7 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
3
  import { context, logger } from '@mondaydotcomorg/trident-backend-runtime';
4
- import { BackendRunnerEvents, trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability';
4
+ import { BackendRunnerEvents } from '@mondaydotcomorg/z2h-shared-utils/observability';
5
+ import { trackEvent } from '@mondaydotcomorg/z2h-shared-utils/observability/trident';
5
6
  import { getDefaultClient } from 'src/utils/elasticache';
6
7
 
7
8
  import { ChannelError } from '../../backend-runner.errors';
@@ -25,6 +26,18 @@ export interface SnowflakeRow {
25
26
  export interface RunQueryOptions {
26
27
  /** Clamped to MAX_TIMEOUT_MS. Defaults to DEFAULT_TIMEOUT_MS. */
27
28
  timeoutMs?: number;
29
+ /** Human-readable label for the pool identifier/logs, e.g. a widget name, instead of the default query-shape hash. Sanitized to `[a-z0-9-_]`, max 64 chars. */
30
+ identifier?: string;
31
+ }
32
+
33
+ const MAX_IDENTIFIER_LENGTH = 64;
34
+
35
+ function sanitizeIdentifier(raw: string): string {
36
+ return raw
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9-_]+/g, '-')
39
+ .replace(/^-+|-+$/g, '')
40
+ .slice(0, MAX_IDENTIFIER_LENGTH);
28
41
  }
29
42
 
30
43
  function hashQuery(boundSql: string, params: Record<string, SnowflakeParamValue>): string {
@@ -109,8 +122,8 @@ function resolveTimeout(timeoutMs: number | undefined): number {
109
122
  */
110
123
  export async function runSnowflakeQuery(
111
124
  sql: string,
125
+ appName: string,
112
126
  params?: Record<string, SnowflakeParamValue> | null,
113
- appName?: string,
114
127
  options?: RunQueryOptions | null,
115
128
  ): Promise<SnowflakeRow[]> {
116
129
  const timeoutInMs = resolveTimeout(options?.timeoutMs);
@@ -132,13 +145,14 @@ export async function runSnowflakeQuery(
132
145
 
133
146
  // Cache key stays parameter-sensitive; the pool identifier deliberately is not.
134
147
  const queryCacheKey = hashQuery(boundSql, params ?? {});
135
- const queryIdentifier = hashQueryShape(sql);
148
+ const sanitizedIdentifier = options?.identifier ? sanitizeIdentifier(options.identifier) : '';
149
+ const queryIdentifier = sanitizedIdentifier || hashQueryShape(sql);
136
150
  const scope = cacheScope(useStateless);
137
151
  const startMs = Date.now();
138
152
  const cached = scope ? await readCache(queryCacheKey, scope) : null;
139
153
  if (cached) {
140
154
  const duration_ms = Date.now() - startMs;
141
- trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: true }, BI_SOURCE);
155
+ void trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: true }, BI_SOURCE);
142
156
  logger.info(
143
157
  {
144
158
  tag: LOG_TAG,
@@ -155,7 +169,7 @@ export async function runSnowflakeQuery(
155
169
 
156
170
  const queryOptions = {
157
171
  query: boundSql,
158
- identifier: `z2h-query:${queryIdentifier}`,
172
+ identifier: `z2h-query:${appName}:${queryIdentifier}`,
159
173
  fallback: [] as SnowflakeRow[],
160
174
  timeoutInMs,
161
175
  convertToSingularResult: false,
@@ -182,7 +196,7 @@ export async function runSnowflakeQuery(
182
196
  );
183
197
  } catch (err) {
184
198
  // Failure BI tracking already happens generically at the invoke level
185
- // (runner.service.ts's z2h_backend_invoke_failed, tagged with this
199
+ // (runner.service.ts's BackendRunnerEvents.invoke.biEvent, tagged with this
186
200
  // ChannelError's `code`) — this stays a log only, for the queryHash/
187
201
  // queryIdentifier that generic event can't know about.
188
202
  const duration_ms = Date.now() - startMs;
@@ -195,7 +209,7 @@ export async function runSnowflakeQuery(
195
209
  await writeCache(queryCacheKey, scope, rows);
196
210
  }
197
211
  const duration_ms = Date.now() - startMs;
198
- trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: false }, BI_SOURCE);
212
+ void trackEvent(BackendRunnerEvents.snowflake.queryExecuted, { appName, duration_ms, cache_hit: false }, BI_SOURCE);
199
213
  logger.info(
200
214
  { tag: LOG_TAG, queryHash: queryCacheKey, queryIdentifier, duration_ms, rowCount: rows.length, cache_hit: false },
201
215
  'query executed',