@indigoai-us/hq-cli 5.10.1 → 5.12.0

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.
@@ -10,9 +10,40 @@ import {
10
10
  clearAllCache,
11
11
  } from "../utils/secrets-cache.js";
12
12
  import { SECRET_NAME_PATTERN, GROUP_ID_PATTERN } from "./_patterns.js";
13
- import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
13
+ import {
14
+ vaultApiFetch,
15
+ getCompanyUid,
16
+ getEntityUid,
17
+ } from "../utils/vault-api.js";
14
18
  export type { VaultApiOptions } from "../utils/vault-api.js";
15
- export { vaultApiFetch, getCompanyUid };
19
+ export { vaultApiFetch, getCompanyUid, getEntityUid };
20
+
21
+ interface SecretsScopeOpts {
22
+ company?: string;
23
+ personal?: boolean;
24
+ }
25
+
26
+ function scopeOpts(opts: SecretsScopeOpts): {
27
+ personal: boolean;
28
+ companySlug: string | undefined;
29
+ } {
30
+ if (opts.personal && opts.company) {
31
+ console.error(
32
+ chalk.red("Error: --personal cannot be combined with --company."),
33
+ );
34
+ process.exit(1);
35
+ }
36
+ return { personal: !!opts.personal, companySlug: opts.company };
37
+ }
38
+
39
+ function rejectIfPersonal(opts: SecretsScopeOpts, action: string): void {
40
+ if (opts.personal) {
41
+ console.error(
42
+ chalk.red(`Error: ${action} is not supported with --personal.`),
43
+ );
44
+ process.exit(1);
45
+ }
46
+ }
16
47
 
17
48
  function shellSingleQuote(value: string): string {
18
49
  return "'" + value.replace(/'/g, "'\\''") + "'";
@@ -125,7 +156,11 @@ export function registerSecretsCommand(program: Command): void {
125
156
  const secrets = program
126
157
  .command("secrets")
127
158
  .description("Manage secrets in HQ vault (SSM Parameter Store)")
128
- .option("--company <slug>", "Company slug (resolves to companyUid)");
159
+ .option("--company <slug>", "Company slug (resolves to companyUid)")
160
+ .option(
161
+ "--personal",
162
+ "Operate on the caller's personal vault (no sharing)",
163
+ );
129
164
 
130
165
  secrets
131
166
  .command("set <name>")
@@ -164,8 +199,10 @@ export function registerSecretsCommand(program: Command): void {
164
199
  }
165
200
 
166
201
  const token = await ensureCognitoToken();
167
- const companySlug = secrets.opts().company as string | undefined;
168
- const companyUid = await getCompanyUid(token, companySlug);
202
+ const companyUid = await getEntityUid(
203
+ token,
204
+ scopeOpts(secrets.opts()),
205
+ );
169
206
 
170
207
  const res = await vaultApiFetch({
171
208
  token,
@@ -200,8 +237,10 @@ export function registerSecretsCommand(program: Command): void {
200
237
  .action(async (name: string, opts: { reveal?: boolean }) => {
201
238
  try {
202
239
  const token = await ensureCognitoToken();
203
- const companySlug = secrets.opts().company as string | undefined;
204
- const companyUid = await getCompanyUid(token, companySlug);
240
+ const companyUid = await getEntityUid(
241
+ token,
242
+ scopeOpts(secrets.opts()),
243
+ );
205
244
 
206
245
  const query: Record<string, string> = {};
207
246
  if (opts.reveal) {
@@ -283,8 +322,10 @@ export function registerSecretsCommand(program: Command): void {
283
322
  }
284
323
 
285
324
  const token = await ensureCognitoToken();
286
- const companySlug = secrets.opts().company as string | undefined;
287
- const companyUid = await getCompanyUid(token, companySlug);
325
+ const companyUid = await getEntityUid(
326
+ token,
327
+ scopeOpts(secrets.opts()),
328
+ );
288
329
 
289
330
  const query: Record<string, string> = {};
290
331
  if (normalizedPrefix) {
@@ -364,8 +405,10 @@ export function registerSecretsCommand(program: Command): void {
364
405
  }
365
406
 
366
407
  const token = await ensureCognitoToken();
367
- const companySlug = secrets.opts().company as string | undefined;
368
- const companyUid = await getCompanyUid(token, companySlug);
408
+ const companyUid = await getEntityUid(
409
+ token,
410
+ scopeOpts(secrets.opts()),
411
+ );
369
412
 
370
413
  const res = await vaultApiFetch({
371
414
  token,
@@ -427,8 +470,10 @@ export function registerSecretsCommand(program: Command): void {
427
470
  }
428
471
 
429
472
  const token = await ensureCognitoToken();
430
- const companySlug = secrets.opts().company as string | undefined;
431
- const companyUid = await getCompanyUid(token, companySlug);
473
+ const companyUid = await getEntityUid(
474
+ token,
475
+ scopeOpts(secrets.opts()),
476
+ );
432
477
 
433
478
  const revealed = await Promise.all(
434
479
  keys.map(async (key) => {
@@ -518,8 +563,10 @@ export function registerSecretsCommand(program: Command): void {
518
563
  }
519
564
 
520
565
  const token = await ensureCognitoToken();
521
- const companySlug = secrets.opts().company as string | undefined;
522
- const companyUid = await getCompanyUid(token, companySlug);
566
+ const companyUid = await getEntityUid(
567
+ token,
568
+ scopeOpts(secrets.opts()),
569
+ );
523
570
 
524
571
  const revealed = await Promise.all(
525
572
  keys.map(async (key) => {
@@ -568,6 +615,8 @@ export function registerSecretsCommand(program: Command): void {
568
615
  .option("--expires <duration>", "Token expiry duration (e.g. 24h, 2d, 30m)", "24h")
569
616
  .action(async (name: string, opts: { expires: string }) => {
570
617
  try {
618
+ rejectIfPersonal(secrets.opts(), "generate-link");
619
+
571
620
  if (!SECRET_NAME_PATTERN.test(name)) {
572
621
  console.error(chalk.red(`Invalid secret name '${name}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_API_KEY or DEV/MY_KEY)`));
573
622
  process.exit(1);
@@ -586,8 +635,10 @@ export function registerSecretsCommand(program: Command): void {
586
635
  }
587
636
 
588
637
  const token = await ensureCognitoToken();
589
- const companySlug = secrets.opts().company as string | undefined;
590
- const companyUid = await getCompanyUid(token, companySlug);
638
+ const companyUid = await getEntityUid(
639
+ token,
640
+ scopeOpts(secrets.opts()),
641
+ );
591
642
 
592
643
  const res = await vaultApiFetch({
593
644
  token,
@@ -632,6 +683,8 @@ export function registerSecretsCommand(program: Command): void {
632
683
  .requiredOption("--permission <level>", "Permission level: read | write | admin")
633
684
  .action(async (path: string, opts: { with: string; permission: string }) => {
634
685
  try {
686
+ rejectIfPersonal(secrets.opts(), "share");
687
+
635
688
  if (!SECRET_NAME_PATTERN.test(path)) {
636
689
  console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
637
690
  process.exit(1);
@@ -651,8 +704,10 @@ export function registerSecretsCommand(program: Command): void {
651
704
  const granteeId = opts.with;
652
705
 
653
706
  const token = await ensureCognitoToken();
654
- const companySlug = secrets.opts().company as string | undefined;
655
- const companyUid = await getCompanyUid(token, companySlug);
707
+ const companyUid = await getEntityUid(
708
+ token,
709
+ scopeOpts(secrets.opts()),
710
+ );
656
711
 
657
712
  const res = await vaultApiFetch({
658
713
  token,
@@ -695,6 +750,8 @@ export function registerSecretsCommand(program: Command): void {
695
750
  .requiredOption("--from <principal>", "Email address or group id to remove")
696
751
  .action(async (path: string, opts: { from: string }) => {
697
752
  try {
753
+ rejectIfPersonal(secrets.opts(), "unshare");
754
+
698
755
  if (!SECRET_NAME_PATTERN.test(path)) {
699
756
  console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
700
757
  process.exit(1);
@@ -709,8 +766,10 @@ export function registerSecretsCommand(program: Command): void {
709
766
  const granteeId = opts.from;
710
767
 
711
768
  const token = await ensureCognitoToken();
712
- const companySlug = secrets.opts().company as string | undefined;
713
- const companyUid = await getCompanyUid(token, companySlug);
769
+ const companyUid = await getEntityUid(
770
+ token,
771
+ scopeOpts(secrets.opts()),
772
+ );
714
773
 
715
774
  const res = await vaultApiFetch({
716
775
  token,
@@ -751,14 +810,18 @@ export function registerSecretsCommand(program: Command): void {
751
810
  .description("Show the ACL (access control list) for a secret path")
752
811
  .action(async (path: string) => {
753
812
  try {
813
+ rejectIfPersonal(secrets.opts(), "acl");
814
+
754
815
  if (!SECRET_NAME_PATTERN.test(path)) {
755
816
  console.error(chalk.red(`Invalid secret path '${path}': must match ^[A-Z][A-Z0-9_]*(/[A-Z][A-Z0-9_]+)*$ (e.g. MY_KEY or PROD/DB_PASSWORD)`));
756
817
  process.exit(1);
757
818
  }
758
819
 
759
820
  const token = await ensureCognitoToken();
760
- const companySlug = secrets.opts().company as string | undefined;
761
- const companyUid = await getCompanyUid(token, companySlug);
821
+ const companyUid = await getEntityUid(
822
+ token,
823
+ scopeOpts(secrets.opts()),
824
+ );
762
825
 
763
826
  const secretPath = path;
764
827
  const res = await vaultApiFetch({
package/src/index.ts CHANGED
@@ -28,6 +28,8 @@ import { registerRunCommand } from "./commands/run.js";
28
28
  import { registerGroupsCommand } from "./commands/groups.js";
29
29
  import { registerFilesCommand } from "./commands/files.js";
30
30
  import { registerMembersCommand } from "./commands/members.js";
31
+ import { registerFeedbackCommand } from "./commands/feedback.js";
32
+ import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
31
33
 
32
34
  initSentry();
33
35
 
@@ -36,7 +38,7 @@ const program = new Command();
36
38
  program
37
39
  .name("hq")
38
40
  .description("HQ management CLI — modules, packages, and cloud sync")
39
- .version("5.8.6");
41
+ .version("5.12.0");
40
42
 
41
43
  // Module management subcommand group
42
44
  const modulesCmd = program
@@ -109,8 +111,16 @@ registerMembersCommand(program);
109
111
  // Onboarding (top-level — Cognito + vault-service provisioning)
110
112
  registerOnboardCommand(program);
111
113
 
114
+ // Feedback (subcommand group — hq feedback bug|feature)
115
+ registerFeedbackCommand(program);
116
+
112
117
  (async () => {
113
118
  try {
119
+ Sentry.addBreadcrumb({
120
+ category: "command",
121
+ message: sanitizeArgv(process.argv.slice(2)).join(" "),
122
+ level: "info",
123
+ });
114
124
  await program.parseAsync();
115
125
  } catch (err) {
116
126
  Sentry.captureException(err);
package/src/sentry.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as Sentry from "@sentry/node";
2
2
  import { BUNDLED_DSN } from "./sentry-dsn.generated.js";
3
3
  import { beforeSend } from "./sentry-before-send.js";
4
+ import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
4
5
 
5
6
  export function initSentry(): void {
6
7
  const dsn = BUNDLED_DSN || process.env.SENTRY_DSN;
@@ -13,6 +14,7 @@ export function initSentry(): void {
13
14
  tags: { repo: "hq-cli" },
14
15
  },
15
16
  beforeSend,
17
+ beforeBreadcrumb,
16
18
  });
17
19
  }
18
20
 
@@ -0,0 +1,18 @@
1
+ import type { Breadcrumb } from "@sentry/node";
2
+
3
+ const BUFFER_SIZE = 20;
4
+ const _buffer: Breadcrumb[] = [];
5
+
6
+ // Sentry beforeBreadcrumb hook: records every breadcrumb in a ring buffer
7
+ // and returns it unchanged so Sentry still processes it normally.
8
+ export function beforeBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb | null {
9
+ _buffer.push(breadcrumb);
10
+ if (_buffer.length > BUFFER_SIZE) {
11
+ _buffer.shift();
12
+ }
13
+ return breadcrumb;
14
+ }
15
+
16
+ export function getRecentBreadcrumbs(): Breadcrumb[] {
17
+ return [..._buffer];
18
+ }
@@ -0,0 +1,172 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import * as os from "os";
3
+
4
+ vi.mock("child_process", () => ({
5
+ execFileSync: vi.fn(),
6
+ }));
7
+
8
+ vi.mock("./breadcrumb-buffer.js", () => ({
9
+ getRecentBreadcrumbs: vi.fn(() => []),
10
+ }));
11
+
12
+ import { execFileSync } from "child_process";
13
+ import { getRecentBreadcrumbs } from "./breadcrumb-buffer.js";
14
+ import { collectDiagnostics, sanitizeArgv } from "./feedback-diagnostics.js";
15
+ import { CLI_VERSION } from "../cli-version.js";
16
+
17
+ function mockGitSuccess(remoteUrl = "https://github.com/acme/repo.git"): void {
18
+ vi.mocked(execFileSync).mockImplementation(
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ (_cmd: string, args?: any): any => {
21
+ const a: string[] = args ?? [];
22
+ if (a[0] === "rev-parse" && a[1] === "--abbrev-ref") return "main\n";
23
+ if (a[0] === "rev-parse" && a[1] === "--short") return "abc1234\n";
24
+ if (a[0] === "status") return "";
25
+ if (a[0] === "remote") return `${remoteUrl}\n`;
26
+ return "";
27
+ },
28
+ );
29
+ }
30
+
31
+ beforeEach(() => {
32
+ vi.resetAllMocks();
33
+ vi.mocked(getRecentBreadcrumbs).mockReturnValue([]);
34
+ mockGitSuccess();
35
+ });
36
+
37
+ describe("sanitizeArgv", () => {
38
+ it("passes through non-secret args unchanged", () => {
39
+ expect(sanitizeArgv(["feedback", "bug", "--title", "test"])).toEqual([
40
+ "feedback",
41
+ "bug",
42
+ "--title",
43
+ "test",
44
+ ]);
45
+ });
46
+
47
+ it("redacts value following a secret flag", () => {
48
+ expect(sanitizeArgv(["secrets", "set", "NAME", "--token", "abc123"])).toEqual([
49
+ "secrets",
50
+ "set",
51
+ "NAME",
52
+ "--token",
53
+ "***",
54
+ ]);
55
+ });
56
+
57
+ it("redacts --password and --secret flags", () => {
58
+ expect(sanitizeArgv(["--password", "hunter2", "--secret", "s3cr3t"])).toEqual([
59
+ "--password",
60
+ "***",
61
+ "--secret",
62
+ "***",
63
+ ]);
64
+ });
65
+
66
+ it("does not redact a secret flag at the end with no following value", () => {
67
+ expect(sanitizeArgv(["--token"])).toEqual(["--token"]);
68
+ });
69
+
70
+ it("redacts value in --flag=value form for secret flags", () => {
71
+ expect(sanitizeArgv(["--token=abc123", "--password=hunter2"])).toEqual([
72
+ "--token=***",
73
+ "--password=***",
74
+ ]);
75
+ });
76
+
77
+ it("passes non-secret --flag=value forms through unchanged", () => {
78
+ expect(sanitizeArgv(["--title=foo", "--company=acme"])).toEqual([
79
+ "--title=foo",
80
+ "--company=acme",
81
+ ]);
82
+ });
83
+ });
84
+
85
+ describe("collectDiagnostics", () => {
86
+ it("populates cliVersion from the bundled CLI_VERSION constant (not env var)", () => {
87
+ const saved = process.env.npm_package_version;
88
+ delete process.env.npm_package_version;
89
+ const blob = collectDiagnostics();
90
+ expect(blob.cliVersion).toBe(CLI_VERSION);
91
+ if (saved !== undefined) process.env.npm_package_version = saved;
92
+ });
93
+
94
+ it("cliVersion is unaffected by npm_package_version env var", () => {
95
+ process.env.npm_package_version = "99.99.99";
96
+ const blob = collectDiagnostics();
97
+ expect(blob.cliVersion).toBe(CLI_VERSION);
98
+ delete process.env.npm_package_version;
99
+ });
100
+
101
+ it("sanitizes secret flags in the captured command", () => {
102
+ const saved = process.argv;
103
+ process.argv = ["node", "/usr/bin/hq", "secrets", "set", "NAME", "--token", "secret123"];
104
+ const blob = collectDiagnostics();
105
+ expect(blob.command).toEqual(["secrets", "set", "NAME", "--token", "***"]);
106
+ process.argv = saved;
107
+ });
108
+
109
+ it("sanitizes --flag=value secret flags in the captured command", () => {
110
+ const saved = process.argv;
111
+ process.argv = ["node", "/usr/bin/hq", "secrets", "set", "NAME", "--token=secretXYZ"];
112
+ const blob = collectDiagnostics();
113
+ expect(blob.command).toEqual(["secrets", "set", "NAME", "--token=***"]);
114
+ process.argv = saved;
115
+ });
116
+
117
+ it("captures command as process.argv slice from index 2", () => {
118
+ const saved = process.argv;
119
+ process.argv = ["node", "/usr/bin/hq", "feedback", "bug"];
120
+ const blob = collectDiagnostics();
121
+ expect(blob.command).toEqual(["feedback", "bug"]);
122
+ process.argv = saved;
123
+ });
124
+
125
+ it("populates nodeVersion and os fields from process and os module", () => {
126
+ const blob = collectDiagnostics();
127
+ expect(blob.nodeVersion).toBe(process.version);
128
+ expect(blob.os.platform).toBe(os.platform());
129
+ expect(blob.os.release).toBe(os.release());
130
+ expect(blob.os.arch).toBe(os.arch());
131
+ });
132
+
133
+ it("sanitizes credentials embedded in HTTPS remote URLs", () => {
134
+ mockGitSuccess("https://user:s3cr3t@github.com/acme/repo.git");
135
+ const blob = collectDiagnostics();
136
+ expect(blob.git.remoteUrl).toBe("https://***@github.com/acme/repo.git");
137
+ });
138
+
139
+ it("returns null git context when not inside a git repo", () => {
140
+ vi.mocked(execFileSync).mockImplementation(() => {
141
+ throw new Error("not a git repo");
142
+ });
143
+ const blob = collectDiagnostics();
144
+ expect(blob.git.branch).toBeNull();
145
+ expect(blob.git.head).toBeNull();
146
+ expect(blob.git.remoteUrl).toBeNull();
147
+ expect(blob.git.dirty).toBe(false);
148
+ });
149
+
150
+ it("includes recentSentryBreadcrumbs from the ring buffer", () => {
151
+ const crumb = { category: "http", message: "GET /v1/foo", level: "info" as const };
152
+ vi.mocked(getRecentBreadcrumbs).mockReturnValue([crumb]);
153
+ const blob = collectDiagnostics();
154
+ expect(blob.recentSentryBreadcrumbs).toEqual([crumb]);
155
+ });
156
+
157
+ it("reports dirty=true when git status shows uncommitted changes", () => {
158
+ vi.mocked(execFileSync).mockImplementation(
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ (_cmd: string, args?: any): any => {
161
+ const a: string[] = args ?? [];
162
+ if (a[0] === "rev-parse" && a[1] === "--abbrev-ref") return "main\n";
163
+ if (a[0] === "rev-parse" && a[1] === "--short") return "abc1234\n";
164
+ if (a[0] === "status") return " M src/index.ts\n";
165
+ if (a[0] === "remote") return "https://github.com/acme/repo.git\n";
166
+ return "";
167
+ },
168
+ );
169
+ const blob = collectDiagnostics();
170
+ expect(blob.git.dirty).toBe(true);
171
+ });
172
+ });
@@ -0,0 +1,115 @@
1
+ import * as os from "os";
2
+ import { execFileSync } from "child_process";
3
+ import { getRecentBreadcrumbs } from "./breadcrumb-buffer.js";
4
+ import { CLI_VERSION } from "../cli-version.js";
5
+
6
+ export interface GitContext {
7
+ branch: string | null;
8
+ head: string | null;
9
+ dirty: boolean;
10
+ remoteUrl: string | null;
11
+ }
12
+
13
+ export interface DiagnosticsBlob {
14
+ cliVersion: string;
15
+ nodeVersion: string;
16
+ os: { platform: string; release: string; arch: string };
17
+ command: string[];
18
+ cwd: string;
19
+ git: GitContext;
20
+ recentSentryBreadcrumbs: unknown[];
21
+ }
22
+
23
+ const SECRET_FLAGS = new Set([
24
+ "--token",
25
+ "--secret",
26
+ "--password",
27
+ "--key",
28
+ "--api-key",
29
+ "--access-token",
30
+ "--auth-token",
31
+ ]);
32
+
33
+ export function sanitizeArgv(argv: string[]): string[] {
34
+ const result: string[] = [];
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const arg = argv[i];
37
+ if (arg.startsWith("--") && arg.includes("=")) {
38
+ const eqIdx = arg.indexOf("=");
39
+ const flag = arg.slice(0, eqIdx);
40
+ if (SECRET_FLAGS.has(flag)) {
41
+ result.push(`${flag}=***`);
42
+ continue;
43
+ }
44
+ }
45
+ result.push(arg);
46
+ if (SECRET_FLAGS.has(arg) && i + 1 < argv.length) {
47
+ result.push("***");
48
+ i++;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+
54
+ function sanitizeRemoteUrl(url: string): string {
55
+ return url.replace(/https?:\/\/[^@]+@/, "https://***@");
56
+ }
57
+
58
+ function runGit(args: string[]): string {
59
+ return execFileSync("git", args, {
60
+ encoding: "utf-8",
61
+ timeout: 2000,
62
+ stdio: ["ignore", "pipe", "ignore"],
63
+ }).trim();
64
+ }
65
+
66
+ function collectGitContext(): GitContext {
67
+ let branch: string | null = null;
68
+ let head: string | null = null;
69
+ let dirty = false;
70
+ let remoteUrl: string | null = null;
71
+
72
+ try {
73
+ branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"]);
74
+ } catch {
75
+ return { branch: null, head: null, dirty: false, remoteUrl: null };
76
+ }
77
+
78
+ try {
79
+ head = runGit(["rev-parse", "--short", "HEAD"]);
80
+ } catch {
81
+ // best-effort
82
+ }
83
+
84
+ try {
85
+ const statusOut = runGit(["status", "--porcelain"]);
86
+ dirty = statusOut.length > 0;
87
+ } catch {
88
+ // best-effort
89
+ }
90
+
91
+ try {
92
+ const raw = runGit(["remote", "get-url", "origin"]);
93
+ remoteUrl = sanitizeRemoteUrl(raw);
94
+ } catch {
95
+ // no origin remote
96
+ }
97
+
98
+ return { branch, head, dirty, remoteUrl };
99
+ }
100
+
101
+ export function collectDiagnostics(): DiagnosticsBlob {
102
+ return {
103
+ cliVersion: CLI_VERSION,
104
+ nodeVersion: process.version,
105
+ os: {
106
+ platform: os.platform(),
107
+ release: os.release(),
108
+ arch: os.arch(),
109
+ },
110
+ command: sanitizeArgv(process.argv.slice(2)),
111
+ cwd: process.cwd(),
112
+ git: collectGitContext(),
113
+ recentSentryBreadcrumbs: getRecentBreadcrumbs(),
114
+ };
115
+ }