neetob 0.5.68 → 0.5.77

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.env +2 -1
  3. data/.neetoci/default.yml +1 -1
  4. data/.ruby-version +1 -1
  5. data/Gemfile.lock +44 -21
  6. data/README.md +11 -0
  7. data/bookmarks.md +113 -113
  8. data/data/github-labels.json +80 -45
  9. data/data/repo-team-leads.json +82 -0
  10. data/exe/neetob +1 -1
  11. data/lib/neetob/cli/base.rb +67 -5
  12. data/lib/neetob/cli/cloudflare/automatic_https_rewrites.rb +34 -0
  13. data/lib/neetob/cli/cloudflare/base.rb +2 -2
  14. data/lib/neetob/cli/cloudflare/commands.rb +7 -0
  15. data/lib/neetob/cli/github/active_record_doctor.rb +1 -1
  16. data/lib/neetob/cli/github/brakeman.rb +1 -1
  17. data/lib/neetob/cli/github/bundle_audit.rb +1 -1
  18. data/lib/neetob/cli/github/issues/helpers.rb +40 -0
  19. data/lib/neetob/cli/github/make_pr/base.rb +1 -1
  20. data/lib/neetob/cli/github/repositories/pull_requests.rb +19 -0
  21. data/lib/neetob/cli/github/repositories/team_leads.rb +34 -0
  22. data/lib/neetob/cli/github/unused_assets_audit.rb +5 -1
  23. data/lib/neetob/cli/monthly_audit/commands.rb +2 -1
  24. data/lib/neetob/cli/monthly_audit/databases/users_unique_email_index.rb +6 -1
  25. data/lib/neetob/cli/monthly_audit/databases/uuid_primary_key.rb +8 -0
  26. data/lib/neetob/cli/monthly_audit/github_issue_creation.rb +75 -0
  27. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/always_use_https_is_enabled.rb +11 -0
  28. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/automatic_https_rewrites_is_enabled.rb +43 -0
  29. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/dns_entry_has_proxy_status.rb +9 -0
  30. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/main.rb +2 -2
  31. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/minimum_tls_version_is_one_point_two.rb +11 -0
  32. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/spf_records_are_valid.rb +9 -0
  33. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/ssl_tls_encryption_mode_set_to_full.rb +9 -0
  34. data/lib/neetob/cli/monthly_audit/instances_and_addons/cronitor/setup_correctly_for_apps.rb +10 -0
  35. data/lib/neetob/cli/monthly_audit/instances_and_addons/cronitor/setup_correctly_for_help_center.rb +12 -0
  36. data/lib/neetob/cli/monthly_audit/instances_and_addons/cronitor/setup_correctly_for_landing_pages.rb +15 -2
  37. data/lib/neetob/cli/monthly_audit/instances_and_addons/honeybadger/setup_correctly_for_apps.rb +28 -29
  38. data/lib/neetob/cli/monthly_audit/instances_and_addons/main.rb +5 -5
  39. data/lib/neetob/cli/monthly_audit/instances_and_addons/neeto_deploy_or_heroku/cloudfront_cdn_enabled.rb +11 -17
  40. data/lib/neetob/cli/monthly_audit/instances_and_addons/neeto_deploy_or_heroku/essential_environment_variables_set.rb +7 -10
  41. data/lib/neetob/cli/monthly_audit/instances_and_addons/neeto_deploy_or_heroku/main.rb +0 -3
  42. data/lib/neetob/cli/monthly_audit/instances_and_addons/neeto_deploy_or_heroku/scheduled_exports_enabled.rb +8 -4
  43. data/lib/neetob/cli/monthly_audit/instances_and_addons/neeto_deploy_or_heroku/ssl_certificates_over_thirty_days_from_expiry.rb +69 -24
  44. data/lib/neetob/cli/monthly_audit/misc/main.rb +1 -1
  45. data/lib/neetob/cli/monthly_audit/misc/redirections_working_correctly.rb +14 -1
  46. data/lib/neetob/cli/monthly_audit/misc/sparkpost_sub_account_used_for_all_apps.rb +24 -18
  47. data/lib/neetob/cli/monthly_audit/perform.rb +7 -2
  48. data/lib/neetob/cli/monthly_audit/security/code/active_record_doctor.rb +10 -5
  49. data/lib/neetob/cli/monthly_audit/security/code/brakeman.rb +10 -2
  50. data/lib/neetob/cli/monthly_audit/security/code/bundle_audit.rb +19 -6
  51. data/lib/neetob/cli/monthly_audit/security/code/checks_for_unused_assets.rb +5 -0
  52. data/lib/neetob/cli/monthly_audit/security/code/fasterer.rb +10 -2
  53. data/lib/neetob/cli/monthly_audit/security/code/yarn_audit.rb +6 -1
  54. data/lib/neetob/cli/monthly_audit/security/github/dependabot_prs_merged.rb +20 -0
  55. data/lib/neetob/cli/monthly_audit/security/github/dependabot_turned_on.rb +25 -21
  56. data/lib/neetob/cli/neeto_deploy/autoscaling_config.rb +1 -1
  57. data/lib/neetob/cli/neeto_deploy/certificates.rb +1 -1
  58. data/lib/neetob/cli/neeto_deploy/commands.rb +7 -0
  59. data/lib/neetob/cli/neeto_deploy/config_vars/list.rb +1 -1
  60. data/lib/neetob/cli/neeto_deploy/config_vars/remove.rb +1 -1
  61. data/lib/neetob/cli/neeto_deploy/config_vars/upsert.rb +1 -1
  62. data/lib/neetob/cli/neeto_deploy/scheduled_exports.rb +1 -1
  63. data/lib/neetob/cli/neeto_deploy/unique_email_domains.rb +165 -0
  64. data/lib/neetob/cli/sre/base.rb +13 -13
  65. data/lib/neetob/cli/sre/check_essential_env.rb +7 -2
  66. data/lib/neetob/cli/sre/checklist.rb +2 -2
  67. data/lib/neetob/version.rb +1 -1
  68. data/neetob.gemspec +1 -1
  69. data/package.json +30 -0
  70. data/playwright.config.ts +39 -0
  71. data/scripts/config/.env.local +17 -0
  72. data/scripts/constants/auditData.ts +402 -0
  73. data/scripts/constants/routes.ts +30 -0
  74. data/scripts/constants/selectors.ts +4 -0
  75. data/scripts/constants/table.ts +30 -0
  76. data/scripts/constants/texts.ts +46 -0
  77. data/scripts/constants/userAgents.ts +14 -0
  78. data/scripts/utils/markdown.ts +23 -0
  79. data/scripts/workflows/dependabot.ts +104 -0
  80. data/scripts/workflows/honeybadger.ts +169 -0
  81. data/scripts/workflows/sparkpost.ts +204 -0
  82. data/tsconfig.json +35 -0
  83. data/yarn.lock +2216 -0
  84. metadata +26 -6
  85. data/lib/neetob/cli/monthly_audit/instances_and_addons/cloudflare/bot_protection_enabled.rb +0 -32
  86. data/lib/neetob/cli/monthly_audit/instances_and_addons/neeto_deploy_or_heroku/auto_scaling_enabled.rb +0 -60
@@ -12,7 +12,7 @@ module Neetob
12
12
  end
13
13
 
14
14
  def run
15
- result = `neetodeploy addon scheduled_exports_enabled -a #{app}`
15
+ result = `bundle exec neetodeploy addon scheduled_exports_enabled -a #{app}`
16
16
  if Thread.current[:audit_mode]
17
17
  result
18
18
  else
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Neetob
6
+ class CLI
7
+ module NeetoDeploy
8
+ class UniqueEmailDomains < CLI::Base
9
+ attr_accessor :app
10
+
11
+ def initialize(app)
12
+ super()
13
+ @app = app
14
+ end
15
+
16
+ def run
17
+ ui.info("Executing for app: #{app}")
18
+ query = get_query_for_app(app)
19
+
20
+ return ui.error("No query defined for app: #{app}") unless query
21
+
22
+ Open3.popen3("neetodeploy exec -a #{app}") do |stdin, stdout, stderr, wait_thr|
23
+ return handle_forbidden_error(stderr) if forbidden?(stderr)
24
+
25
+ send_command(stdin, "bundle exec rails console")
26
+ sleep(2)
27
+ return handle_forbidden_error(stderr) if forbidden?(stderr)
28
+
29
+ stdin.puts query
30
+ sleep(1)
31
+ stdin.puts "exit" if wait_thr.alive?
32
+ sleep(1)
33
+ stdin.puts "exit" if wait_thr.alive?
34
+ stdin.close
35
+
36
+ output = safely_read(stdout, "stdout")
37
+ errors = safely_read(stderr, "stderr")
38
+
39
+ if (exit_status = wait_thr.value.exitstatus) == 0
40
+ email_domains = extract_email_domains(output)
41
+ ui.success("Unique email domains for #{app}:")
42
+
43
+ if email_domains.empty?
44
+ ui.info("No email domains found")
45
+ ui.info("Debug: Raw output length: #{output.length}")
46
+ ui.info("Debug: Output preview: #{output[0..200]}...")
47
+ else
48
+
49
+ email_domains.each { |domain| puts domain }
50
+ end
51
+ else
52
+ ui.error("Command failed with exit status: #{exit_status}")
53
+ ui.error("Error output: #{errors}") unless errors.empty?
54
+ end
55
+
56
+ exit_status == 0
57
+ end
58
+ rescue Errno::EPIPE
59
+ handle_forbidden_error
60
+ rescue => e
61
+ ui.error("Exception: #{e.message}")
62
+ ui.error("Exception class: #{e.class}")
63
+ false
64
+ end
65
+
66
+ private
67
+
68
+ def forbidden?(stderr)
69
+ error_output = stderr.read_nonblock(1024) rescue ""
70
+ error_output.include?('{"error":"Forbidden"}')
71
+ end
72
+
73
+ def handle_forbidden_error(stderr = nil)
74
+ ui.error("Access denied: You don't have permission to access this app or your session is not authenticated")
75
+ ui.error("Please run `neetodeploy login` and ensure you have access to the app: #{app}")
76
+ false
77
+ end
78
+
79
+ def send_command(stdin, command)
80
+ stdin.puts command
81
+ end
82
+
83
+ def safely_read(io, label)
84
+ io.read
85
+ rescue => e
86
+ ui.error("Error reading #{label}: #{e.message}")
87
+ ""
88
+ end
89
+
90
+ def get_query_for_app(app_name)
91
+ six_months_ago = (Time.now.utc - 6 * 30 * 24 * 60 * 60).strftime("%Y-%m-%d %H:%M:%S")
92
+
93
+ {
94
+ "neeto-form-web-production" =>
95
+ "User.joins(:forms).where('forms.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
96
+
97
+ "neeto-record-web-production" =>
98
+ "User.joins(:recordings).where('recordings.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
99
+
100
+ "neeto-kb-web-production" =>
101
+ "User.joins(:articles).where.not(articles: { slug: ['welcome', 'getting-started', 'getting-help', 'feedback'] }).pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
102
+
103
+ "neeto-code-web-production" =>
104
+ "User.joins(:projects).where('projects.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
105
+
106
+ "neeto-playdash-web-production" =>
107
+ "Organization.joins(:users, projects: :runs).where('runs.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
108
+
109
+ "neeto-desk-web-production" =>
110
+ "User.joins(:tickets).where.not(tickets: { number: [1,2,3,4,5] }).where('tickets.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
111
+
112
+ "neeto-invoice-web-production" =>
113
+ "User.joins(:time_entries).where('time_entries.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
114
+
115
+ "neeto-chat-web-production" =>
116
+ "User.joins(:conversations).where('conversations.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
117
+
118
+ "neeto-quiz-web-production" =>
119
+ "User.joins(quizzes: :attempts).where('attempts.updated_at >= ?', '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
120
+
121
+ "neeto-site-web-production" =>
122
+ "User.joins(sites: :pages).where('sites.updated_at >= :date OR pages.updated_at >= :date', date: '#{six_months_ago}').pluck(Arel.sql('DISTINCT SPLIT_PART(users.email, \\'@\\', 2)'))",
123
+
124
+ "neeto-cal-web-production" => <<~RUBY
125
+ result = ActiveRecord::Base.connection.execute("WITH active_users AS (
126
+ SELECT users.email AS email FROM users
127
+ WHERE (
128
+ EXISTS (
129
+ SELECT 1 FROM bookings_users
130
+ JOIN bookings ON bookings.id = bookings_users.booking_id
131
+ WHERE bookings.user_id = users.id AND bookings.updated_at >= '#{six_months_ago}'
132
+ )
133
+ OR EXISTS (
134
+ SELECT 1 FROM meetings_users
135
+ JOIN meetings ON meetings.id = meetings_users.meeting_id
136
+ WHERE meetings_users.user_id = users.id AND meetings.updated_at >= '#{six_months_ago}'
137
+ )
138
+ )
139
+ )
140
+ SELECT array_agg(DISTINCT SPLIT_PART(email, '@', 2)) AS unique_domains FROM active_users;");
141
+ unique_domains = result.first['unique_domains']
142
+ unique_domains = unique_domains[1...-1].split(',') if unique_domains.is_a?(String)
143
+ unique_domains
144
+ RUBY
145
+ }[app_name] || "User.pluck(:email).map { |e| e.split('@').last }.uniq"
146
+ end
147
+
148
+ def extract_email_domains(output)
149
+ domains = []
150
+
151
+ array_match = output.match(/=>\s*\n(\[.*?\])/m)
152
+ return [] unless array_match
153
+
154
+ array_content = array_match[1]
155
+
156
+ array_content.scan(/"([^"]+)"/).flatten.each do |domain|
157
+ domains << domain if domain.match?(/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
158
+ end
159
+
160
+ domains.uniq.sort
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -46,8 +46,6 @@ module Neetob
46
46
  "neeto-replay-web-staging",
47
47
  "neeto-site-web-production",
48
48
  "neeto-site-web-staging",
49
- "neeto-testify-web-production",
50
- "neeto-testify-web-staging",
51
49
  "neeto-tower-web-production",
52
50
  "neeto-tower-web-staging",
53
51
  "neeto-wireframe-web-production",
@@ -57,7 +55,9 @@ module Neetob
57
55
  "neeto-playdash-web-staging",
58
56
  "neeto-playdash-web-production",
59
57
  "neeto-engage-web-staging",
60
- "neeto-engage-web-production"
58
+ "neeto-engage-web-production",
59
+ "neeto-pay-web-production",
60
+ "neeto-pay-web-staging"
61
61
  ]
62
62
  }
63
63
 
@@ -254,16 +254,6 @@ module Neetob
254
254
  app: "neeto-site-web-production"
255
255
  }
256
256
  },
257
- "NeetoTestify": {
258
- "staging": {
259
- dns: "neetotestify.net",
260
- app: "neeto-testify-web-staging"
261
- },
262
- "production": {
263
- dns: "neetotestify.com",
264
- app: "neeto-testify-web-production"
265
- }
266
- },
267
257
  "NeetoTower": {
268
258
  "staging": {
269
259
  dns: "neetotower.net",
@@ -303,6 +293,16 @@ module Neetob
303
293
  dns: "neetoplaydash.com",
304
294
  app: "neeto-playdash-web-production"
305
295
  }
296
+ },
297
+ "NeetoPay": {
298
+ "staging": {
299
+ dns: "neetopay.net",
300
+ app: "neeto-pay-web-staging"
301
+ },
302
+ "production": {
303
+ dns: "neetopay.com",
304
+ app: "neeto-pay-web-production"
305
+ }
306
306
  }
307
307
  }
308
308
 
@@ -15,7 +15,11 @@ module Neetob
15
15
  "HONEYBADGER_JS_API_KEY",
16
16
  "NODE_MODULES_CACHE",
17
17
  "YARN_CACHE",
18
- "YARN_PRODUCTION"
18
+ "YARN_PRODUCTION",
19
+ "AUTH_APP_URL",
20
+ "DEFAULT_FROM_EMAIL",
21
+ "ORGANIZATION_SYNCING",
22
+ "SPARKPOST_DOMAIN"
19
23
  ]
20
24
  REQUIRED_KEYS_HEROKU = [
21
25
  "HEROKU_APP_NAME"
@@ -58,7 +62,7 @@ module Neetob
58
62
  def check_envs_neetodeploy(app)
59
63
  # TODO: Optimize once github.com/bigbinary/neeto-deploy-web/issues/3745 is done
60
64
  begin
61
- env_table = `neetodeploy env list -a #{app}`
65
+ env_table = `bundle exec neetodeploy env list -a #{app}`
62
66
  json_parse_result = JSON.parse(env_table) rescue nil
63
67
  if json_parse_result && json_parse_result["error"] == "Forbidden"
64
68
  if Thread.current[:audit_mode]
@@ -73,6 +77,7 @@ module Neetob
73
77
  match = line.match(/^\| (\w+) +\| (.+?) +\|$/)
74
78
  envs[match[1]] = match[2] if match
75
79
  end
80
+ REQUIRED_KEYS.filter! { |key| key != "AUTH_APP_URL" } if app == "neeto-auth-web-production"
76
81
  required_keys = REQUIRED_KEYS + REQUIRED_KEYS_NEETODEPLOY
77
82
  compare_envs(required_keys, envs, app)
78
83
  rescue => exception
@@ -45,9 +45,9 @@ module Neetob
45
45
  ui.info `neetob heroku autoscaling_config -a #{app_name} | sed 's/^/ /'`
46
46
  else
47
47
  ui.info "NeetoDeploy autoscaling status"
48
- ui.info `neetodeploy autoscaling_config list -a #{app_name} | sed 's/^/ /'`
48
+ ui.info `bundle exec neetodeploy autoscaling_config list -a #{app_name} | sed 's/^/ /'`
49
49
  ui.info "Scheduled exports status"
50
- ui.info `neetodeploy addon scheduled_exports_enabled -a #{app_name} | sed 's/^/ /'`
50
+ ui.info `bundle exec neetodeploy addon scheduled_exports_enabled -a #{app_name} | sed 's/^/ /'`
51
51
  end
52
52
  ui.info "Validating essential envs"
53
53
  ui.info `neetob sre check_essential_env -a #{app_name} | sed 's/^/ /'`
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Neetob
4
- VERSION = "0.5.68"
4
+ VERSION = "0.5.77"
5
5
  end
data/neetob.gemspec CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.require_paths = ["lib"]
31
31
 
32
32
  # Must have deps
33
- spec.add_dependency "thor", "~> 1.3.0" # for cli
33
+ spec.add_dependency "thor", "~> 1.5.0" # for cli
34
34
  spec.add_dependency "octokit", "~> 4.0" # for github client
35
35
  spec.add_dependency "terminal-table", "~> 3.0.2" # for building cli table
36
36
  spec.add_dependency "launchy", "~> 2.5.0" # for opening in browser
data/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "playwright-tests",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "audit:dependabot": "playwright test dependabot.ts",
8
+ "audit:honeybadger": "playwright test honeybadger.ts",
9
+ "audit:sparkpost": "playwright test sparkpost.ts"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "@playwright/test": "1.56.1",
16
+ "@types/node": "22.10.7"
17
+ },
18
+ "dependencies": {
19
+ "@bigbinary/neeto-cist": "1.0.17",
20
+ "@bigbinary/neeto-playwright-commons": "1.22.46",
21
+ "@bigbinary/neeto-playwright-reporter": "2.1.5",
22
+ "@faker-js/faker": "9.9.0",
23
+ "dayjs": "1.11.18",
24
+ "dotenv-expand": "^12.0.3",
25
+ "dotenv-webpack": "^8.1.1",
26
+ "lint-staged": "^16.2.6",
27
+ "puppeteer-extra-plugin-stealth": "2.11.2",
28
+ "ramda": "^0.32.0"
29
+ }
30
+ }
@@ -0,0 +1,39 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+ import dotenv from "dotenv";
3
+ import dotenvExpand from "dotenv-expand";
4
+
5
+ const env = dotenv.config({
6
+ path: `scripts/config/.env.local`,
7
+ });
8
+ dotenvExpand.expand(env);
9
+
10
+ const neetoPlaywrightReporterConfig = {
11
+ apiKey: process.env.PLAYDASH_API_KEY,
12
+ ciBuildId: new Date().toString(),
13
+ projectKey: "9gnVzoWsfAnbcsmZugMrVUMo",
14
+ };
15
+
16
+ export default defineConfig({
17
+ testDir: "scripts",
18
+ fullyParallel: true,
19
+ forbidOnly: !!process.env.CI,
20
+ retries: process.env.CI ? 2 : 0,
21
+ workers: process.env.CI ? 1 : undefined,
22
+ reporter: [
23
+ ["@bigbinary/neeto-playwright-reporter", neetoPlaywrightReporterConfig],
24
+ // ["line"],
25
+ ],
26
+ use: {
27
+ trace: "on",
28
+ video: "on",
29
+ screenshot: "on",
30
+ },
31
+ timeout: 7 * 60_000,
32
+ projects: [
33
+ {
34
+ name: "chromium",
35
+ use: { ...devices["Desktop Chrome"] },
36
+ testMatch: "**/workflows/**/*.ts",
37
+ },
38
+ ],
39
+ });
@@ -0,0 +1,17 @@
1
+ # GitHub credentials for Dependabot audit
2
+ GITHUB_USERNAME=github_username
3
+ GITHUB_PASSWORD=github_password
4
+ GITHUB_2FA_SECRET_KEY=2fa_secret_key
5
+
6
+ # Honeybadger credentials
7
+ HONEYBADGER_EMAIL=honeybadger@example.com
8
+ HONEYBADGER_PASSWORD=honeybadger_password
9
+
10
+ # Sparkpost credentials
11
+ SPARKPOST_EMAIL=sparkpost@example.com
12
+ SPARKPOST_PASSWORD=sparkpost_password
13
+ NEETO_DEPLOY_EMAIL=neeto_deploy@example.com
14
+ NEETO_AUTOMATION_FASTMAIL_API_KEY=neeto_deploy_fast_mail_api_key
15
+
16
+ # Playwright reporter
17
+ PLAYDASH_API_KEY=playdash_api_key