@speedkit/cli 4.20.5 → 4.22.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +83 -2
  3. package/dist/commands/extension/add.d.ts +9 -0
  4. package/dist/commands/extension/add.js +41 -0
  5. package/dist/commands/extension/list.d.ts +6 -0
  6. package/dist/commands/extension/list.js +27 -0
  7. package/dist/commands/extension/remove.d.ts +9 -0
  8. package/dist/commands/extension/remove.js +38 -0
  9. package/dist/commands/extension/update.d.ts +6 -0
  10. package/dist/commands/extension/update.js +20 -0
  11. package/dist/commands/generate-customer-config.d.ts +2 -0
  12. package/dist/commands/generate-customer-config.js +10 -2
  13. package/dist/helpers/cli-config.d.ts +11 -0
  14. package/dist/helpers/cli-config.js +4 -0
  15. package/dist/helpers/cli-config.spec.js +13 -0
  16. package/dist/hooks/init/first-run.js +1 -0
  17. package/dist/services/customer-config/customer-config-service-context.d.ts +9 -1
  18. package/dist/services/customer-config/customer-config-service-context.js +11 -1
  19. package/dist/services/customer-config/customer-config-service-model.d.ts +6 -0
  20. package/dist/services/customer-config/customer-config-service-model.js +36 -0
  21. package/dist/services/customer-config/customer-config-service.d.ts +8 -3
  22. package/dist/services/customer-config/customer-config-service.js +71 -25
  23. package/dist/services/customer-config/template-source.d.ts +33 -0
  24. package/dist/services/customer-config/template-source.js +49 -0
  25. package/dist/services/integration-api/integration-api-model.js +3 -1
  26. package/dist/services/setup/extension/crx.d.ts +13 -0
  27. package/dist/services/setup/extension/crx.js +48 -0
  28. package/dist/services/setup/extension/crx.spec.d.ts +1 -0
  29. package/dist/services/setup/extension/crx.spec.js +38 -0
  30. package/dist/services/setup/extension/extension-installer.d.ts +31 -0
  31. package/dist/services/setup/extension/extension-installer.js +87 -0
  32. package/dist/services/setup/extension/extension-setup-service.d.ts +40 -0
  33. package/dist/services/setup/extension/extension-setup-service.js +152 -0
  34. package/dist/services/setup/extension/webstore.d.ts +33 -0
  35. package/dist/services/setup/extension/webstore.js +74 -0
  36. package/dist/services/setup/extension/webstore.spec.d.ts +1 -0
  37. package/dist/services/setup/extension/webstore.spec.js +51 -0
  38. package/dist/services/setup/index.d.ts +4 -0
  39. package/dist/services/setup/index.js +4 -0
  40. package/dist/services/setup/setup-service-factory.js +5 -1
  41. package/dist/services/setup/setup-service.d.ts +11 -2
  42. package/dist/services/setup/setup-service.js +18 -3
  43. package/oclif.manifest.json +131 -1
  44. package/package.json +8 -1
  45. package/dist/services/customer-config/templates/config_SpeedKit.js.hbs +0 -548
  46. package/dist/services/customer-config/templates/config_customer.json.hbs +0 -38
  47. package/dist/services/customer-config/templates/config_documentHandler.js.hbs +0 -362
  48. package/dist/services/customer-config/templates/config_dynamicBlocks.es6.hbs +0 -163
  49. package/dist/services/customer-config/templates/config_dynamicStyles.css.hbs +0 -137
  50. package/dist/services/customer-config/templates/config_loadHandler.js.hbs +0 -391
  51. package/dist/services/customer-config/templates/downtimeDetection.js.hbs +0 -209
@@ -1,391 +0,0 @@
1
- import { BaseBundle } from 'rum/BaseBundle';
2
- {{#if hasSoftNavigations}}
3
- import { SpaBundle } from 'rum/SpaBundle';
4
- {{/if}}
5
-
6
- {{#if useGATracking}}
7
- import { GAEcommerceTrackingPlugin } from 'rum/GAEcommerceTrackingPlugin';
8
- {{/if}}
9
- import { ClickPlugin } from "rum/ClickPlugin";
10
- import { HostTrackingPlugin } from "rum/HostTrackingPlugin";
11
- {{#if isPOV}}
12
- import { UserFrictionPlugin } from "rum/UserFrictionPlugin";
13
- {{/if}}
14
-
15
- import { PredictivePreloading } from 'predictive-preloading/predictivePreloading';
16
-
17
- !function() {
18
- if (window !== window.top) {
19
- return;
20
- }
21
- {{#if isOxid}}
22
-
23
- var pageTypeSent = false;
24
- {{/if}}
25
-
26
- var defaultCurrency = 'EUR'; // TODO: verify default currency
27
-
28
- window.SpeedKit = window.SpeedKit || {};
29
- SpeedKit.rumPlugins = SpeedKit.rumPlugins || [];
30
- SpeedKit.rumPlugins.push(
31
- new BaseBundle({{#if isPOV}}
32
- {
33
- resourceTimingsOptions: {
34
- rules: [
35
- // TODO: check if this is the correct endpoint for the main API requests
36
- {
37
- // API Requests
38
- url: ["https://{{production.host}}"],
39
- initiatorTypes: ["fetch", "xmlhttprequest"],
40
- },
41
- // TODO: check if other 3rd party requests need to be added here
42
- {
43
- // Critical resources
44
- onlyTrackRenderBlocking: true,
45
- },
46
- ],
47
- },
48
- }
49
- {{/if}}),
50
- {{#if hasSoftNavigations}}
51
- {{#if convertSoftToHardNavigations}}
52
- new SpaBundle({
53
- enforceHardNavigation: {
54
- /**
55
- * @todo add selectors or urls to filter links that should never be converted
56
- *
57
- * @param url {string}
58
- * @param element {HTMLAnchorElement}
59
- * @returns {boolean}
60
- */
61
- filter: (url, element) => {
62
- // var isMainMenu = element.closest("#MAIN_NAVIGATION_MENU,.burger-menu-body");
63
- // if(isMainMenu) return false
64
-
65
- // checks if the currentSite is enabled and an active site
66
- var isActiveSite =
67
- SpeedKit.isPageTypeEnabled(location.href) ||
68
- (speedKit.activeSites ?? []).some((rule) => SpeedKit.testSpeedKitRule(rule, location, speedKit));
69
- return isActiveSite;
70
- },
71
- },
72
- }), // Soft Navigation Tracking using History API
73
- {{else}}
74
- new SpaBundle(), // Soft Navigation Tracking using History API
75
- {{/if}}
76
- {{/if}}
77
- {{#if useGATracking}}
78
- new GAEcommerceTrackingPlugin({
79
- defaultCurrency: defaultCurrency,
80
- {{#if isShopify}}
81
- trackConfirmation: false, // No ecConversion tracking as it is already handled by the Speed Kit Extension for Shopify
82
- {{/if}}
83
- }),
84
- {{/if}}
85
- new ClickPlugin(),
86
- new HostTrackingPlugin(),
87
- {{#if isPOV}}
88
- new UserFrictionPlugin(),
89
- {{/if}}
90
- {{#if isShopify}}
91
- {
92
- key: "loggedIn",
93
- type: "PiDimension",
94
- on: "window.ShopifyAnalytics",
95
- set: function () {
96
- return !!window.ShopifyAnalytics?.meta?.page?.customerId;
97
- },
98
- },
99
- {
100
- key: "pageType",
101
- type: "PiDimension",
102
- on: "window.ShopifyAnalytics",
103
- set: function () {
104
- return window.ShopifyAnalytics?.meta?.page?.pageType;
105
- },
106
- },
107
- {{else}}
108
- {
109
- key: 'loggedIn',
110
- type: 'PiDimension',
111
- {{#if useGATracking}}
112
- on: 'dataLayer',
113
- set: function(pushedEvent) {
114
- return pushedEvent?.isLoggedIn;
115
- },
116
- {{else}}
117
- // TODO: set proper 'on' event listener
118
- set: function() {
119
- // TODO: return value
120
- },
121
- {{/if}}
122
- },
123
- {
124
- key: 'pageType',
125
- type: 'PiDimension',
126
- {{#if useGATracking}}
127
- on: 'dataLayer',
128
- set: function (pushedEvent) {
129
- // TODO: verify event property
130
- {{#if isOxid}}
131
- var pageType = pushedEvent?.ecomm_pagetype;
132
- if (pageType) {
133
- pageTypeSent = true;
134
- return pageType;
135
- }
136
- {{else}}
137
- return pushedEvent?.pageCategory;
138
- {{/if}}
139
- },
140
- {{else}}
141
- // TODO: set proper 'on' event listener
142
- set: function() {
143
- {{#if isOxid}}
144
- var pageType; // TODO: assign value
145
- if (pageType) {
146
- pageTypeSent = true;
147
- return pageType;
148
- }
149
- {{else}}
150
- // TODO: return value
151
- {{/if}}
152
- },
153
- {{/if}}
154
- },
155
- {{#if isOxid}}
156
- // OXID eShop specific fallback tracking for page type using URL:
157
- {
158
- key: "pageType",
159
- type: "PiDimension",
160
- on: "leavePage",
161
- set: function () {
162
- if (pageTypeSent) return;
163
-
164
- var pageType = "";
165
-
166
- // Use first segment of the url path as page type if pageType was not identified yet
167
- var firstPathnameSegment = location.pathname.match(/^\/([^/]+)/);
168
- if (firstPathnameSegment) {
169
- pageType = firstPathnameSegment[1];
170
- }
171
-
172
- // Extend page type by value of cl query parameter as it also can point to the type of the page
173
- var clParam = new URL(location.href).searchParams.get("cl");
174
- if (clParam) {
175
- pageType += "_" + clParam;
176
- }
177
-
178
- return pageType.toLowerCase();
179
- },
180
- },
181
- {{/if}}
182
- {{/if}}
183
- {{#if addSSR}}
184
- {
185
- on: "nextTick",
186
- track: function (controller) {
187
- controller.trackCustomEvent(
188
- "viewportSize",
189
- JSON.stringify({ innerWidth: window.innerWidth, innerHeight: window.innerHeight }),
190
- 0,
191
- );
192
- },
193
- },
194
- {{/if}}
195
- {{#if useGATracking}}
196
-
197
- // TODO: remove or adapt if events are not properly covered by GAEcommerceTrackingPlugin
198
- // {
199
- // key: 'ecAddToCart',
200
- // on: 'dataLayer',
201
- // append: function (pushedEvent) {
202
- // if (!pushedEvent?.ecommerce?.add?.products) {
203
- // // Ignore, because the passed event was not a 'addToCart' event.
204
- // return;
205
- // }
206
- //
207
- // var aTCProductsArray = [];
208
- // var ecommerce = pushedEvent.ecommerce;
209
- // var currencyCode = ecommerce.currencyCode;
210
- //
211
- // // iterate over all added products
212
- // for (var i = 0; i < ecommerce.add.products.length; i++) {
213
- // aTCProductsArray.push({
214
- // id: ecommerce.add.products[i].id, // optional – remove if no id is available
215
- // currencyCode: currencyCode || defaultCurrency,
216
- // price: Number(ecommerce.add.products[i].price),
217
- // quantity: Number(ecommerce.add.products[i].quantity) || 1,
218
- // });
219
- // }
220
- //
221
- // if (aTCProductsArray.length > 0) {
222
- // return aTCProductsArray;
223
- // }
224
- // },
225
- // },
226
- //
227
- // // Confirmation tracking
228
- // {
229
- // key: 'ecConversion',
230
- // on: 'dataLayer',
231
- // set: function (pushedEvent) {
232
- // if (!pushedEvent?.ecommerce?.purchase) {
233
- // // Ignore, because the passed event was not a 'purchase' event.
234
- // return;
235
- // }
236
- //
237
- // var purchase = pushedEvent.ecommerce.purchase;
238
- // if (!purchase.actionField) {
239
- // return;
240
- // }
241
- //
242
- // var orderId = purchase.actionField.id;
243
- // var orderValue = String(purchase.actionField.revenue).replace(',', '.');
244
- // return {
245
- // id: String(orderId),
246
- // currencyCode: defaultCurrency,
247
- // revenue: Number(orderValue),
248
- // };
249
- // },
250
- // },
251
- {{else}}
252
-
253
- {{#if isShopify}}
254
- // No custom ecAddToCart tracking event needed as it is already handled by the speed-kit-config/ShopifyPlugin
255
- // No custom ecConversion tracking event as it is already handled by the Speed Kit Extension for Shopify
256
- {{else}}
257
- {
258
- key: 'ecAddToCart',
259
- // TODO: set proper 'on' event listener
260
- append: function() {
261
- // var itemId = ...;
262
- // var itemPrice = ...;
263
- // var itemCurrency = ...;
264
- // var itemQuantity = ...;
265
- //
266
- // return {
267
- // id: String(itemId), // optional – remove if no id is available
268
- // price: Number(itemPrice),
269
- // currencyCode: itemCurrency || defaultCurrency,
270
- // quantity: Number(itemQuantity) || 1,
271
- // }
272
- },
273
- },
274
- {
275
- key: 'ecConversion',
276
- // TODO: set proper 'on' event listener
277
- set: function() {
278
- // var orderId = ...;
279
- // var orderValue = ...;
280
- // var orderCurrency = ...;
281
- //
282
- // return {
283
- // id: String(orderId),
284
- // currencyCode: orderCurrency || defaultCurrency,
285
- // revenue: Number(orderValue),
286
- // };
287
- },
288
- },
289
- {{/if}}
290
- {{/if}}
291
- {{#if isPlentymarkets}}
292
- {
293
- on: "domInteractive",
294
- track: function (controller) {
295
- var SSR = document.querySelector('[data-server-rendered="true"]');
296
- controller.trackCustomEvent("serverRendered", !!SSR);
297
- },
298
- },
299
- {{/if}}
300
- );
301
- {{#if isPlentymarkets}}
302
-
303
- /* ==== */
304
-
305
- // Plentymarkets specific:
306
-
307
- // code for dynamic blacklisting upon user logging in
308
- function sendCookiesToSpeedKit() {
309
- try {
310
- if (navigator.serviceWorker?.controller) {
311
- navigator.serviceWorker.controller.postMessage({
312
- type: 'send-cookies',
313
- data: document.cookie,
314
- event: 'domLoaded',
315
- });
316
- }
317
- } catch (e) {}
318
- }
319
-
320
- function setBlacklistCookie() {
321
- var expires = (new Date(Date.now() + 30*24*60*60000));
322
- document.cookie = "baqend-speedkit-blacklist=true; path=/; expires=" + expires.toGMTString();
323
- localStorage.setItem('baqend-speedkit-blacklist', 'true');
324
- }
325
-
326
- function removeBlacklistCookie() {
327
- document.cookie = "baqend-speedkit-blacklist=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC";
328
- localStorage.removeItem('baqend-speedkit-blacklist');
329
- }
330
-
331
- function onDomInteractive(cb) {
332
- if (document.readyState !== 'loading') {
333
- return setTimeout(cb, 0);
334
- }
335
- document.addEventListener('DOMContentLoaded', cb);
336
- }
337
-
338
- function blacklistIfLoggedIn() {
339
- // TODO: replace with customer-specific selector
340
- var loggedIn = document.querySelector('#accountMenuList');
341
- if (!loggedIn) {
342
- removeBlacklistCookie();
343
- } else {
344
- setBlacklistCookie();
345
- }
346
- sendCookiesToSpeedKit();
347
- }
348
-
349
- function onJSdone(cb) {
350
- onDomInteractive(function() {
351
- // resolve if element is already there
352
- // TODO: replace with customer-specific selector
353
- if (document.querySelector('#newsletterSignup')) {
354
- cb();
355
- }
356
-
357
- var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
358
-
359
- // TODO: replace with customer-specific selector
360
- if (!MutationObserver || !document.querySelector('header')) {
361
- return;
362
- }
363
-
364
- var observer = new MutationObserver(function(mutations) {
365
- mutations.forEach(function(mutation) {
366
- // TODO: replace with customer-specific selector
367
- if(mutation.target.id ==="newsletterSignup") {
368
- cb();
369
- }
370
- });
371
- });
372
-
373
- // TODO: replace with customer-specific selector
374
- var target = document.querySelector('header');
375
- observer.observe(target, {
376
- attributes: true,
377
- childList: true,
378
- characterData: true,
379
- subtree:true,
380
- });
381
- });
382
- }
383
-
384
- onJSdone(function() {
385
- blacklistIfLoggedIn();
386
- });
387
- {{/if}}
388
-
389
- // TODO: check if prerendering needs to be scoped to certain pages
390
- new PredictivePreloading({ prerender: true });
391
- }();
@@ -1,209 +0,0 @@
1
- const { message } = require("baqend");
2
-
3
- const pageToCheck = "https://{{production.host}}/"; // TODO change to the actual page to check
4
- const maintenancePageRegex = /maintenance\.{{appName}}/g; // TODO change to the actual regex to identify maintenance mode
5
-
6
- /*
7
- create the job with the following data object:
8
- the slack webhook token can be pulled from this page:
9
- https://api.slack.com/apps/A090W8Q33TR/incoming-webhooks
10
- {
11
- "updateStatus": true,
12
- "slackToken": "<webhook slack token from the downtime detection app>",
13
- "dryRun": true
14
- }
15
- */
16
-
17
- /**
18
- * Checks the status of a given page to determine if it is down or healthy.
19
- *
20
- * @param {Object} db - The database connection or object used for reference or logging.
21
- * @param {string} pageToCheck - The URL or identifier of the page to check the status for.
22
- * @param {RegExp} maintenancePageRegex - A regular expression to identify if the page is in maintenance mode.
23
- * @return {Promise<{ state: string }>} An object representing the state of the page, where "state" is either "healthy" or "downtime".
24
- */
25
- async function checkStatus(db, pageToCheck, maintenancePageRegex) {
26
- const pageDown = await isPageDown(pageToCheck, maintenancePageRegex);
27
-
28
- if (!pageDown) {
29
- return { state: "healthy" };
30
- }
31
-
32
- return { state: "downtime" };
33
- }
34
-
35
- /**
36
- * Hook to test the current status
37
- * @param {baqend} db - the baqend instance
38
- * @return {Promise<void>}
39
- */
40
- async function get(db, req, res) {
41
- if (db.User.me?.key !== "1") throw new Abort("Unauthorized");
42
- res.json(await checkStatus(db, pageToCheck, maintenancePageRegex));
43
- }
44
-
45
- {{{{raw}}}}
46
- /**
47
- * Hook which is executed by the cron job
48
- * @param {baqend} db - the baqend instance
49
- * @param {{status: string, error: Error, data?: { state: "healthy" | "downtime", revId?: string } | null}} status - The latest status of this scheduled run
50
- * @param {{ data: { slackToken?: string, dryRun?: boolean }}} job - The job definition
51
- * @return {Promise<void>}
52
- */
53
- {{{{/raw}}}}
54
- async function run(db, status, job) {
55
- const newStatus = await checkStatus(db, pageToCheck, maintenancePageRegex);
56
- newStatus.revId = status?.data?.revId ?? null;
57
-
58
- db.log.info(`Downtime detection resulted in actual: ${newStatus.state}, previous: ${status.data?.state}`);
59
-
60
- const domain = new URL(pageToCheck).hostname;
61
- if (newStatus.state === "healthy" && status.data?.state === "downtime") {
62
- if (!job.data.dryRun) {
63
- try {
64
- await resumeJob(db, newStatus.revId);
65
- db.log.info(`Downtime detection resumed job ${newStatus.revId} for ${domain}`);
66
- } catch (e) {
67
- db.log.warn(
68
- `Downtime detection failed to resume job ${newStatus.revId} for ${domain}, scheduling a new job instead`,
69
- e,
70
- );
71
- const revId = await scheduleJob(db, domain, "deployment", 0);
72
- db.log.info(`Downtime detection started new job ${revId} for ${domain}`);
73
- }
74
- newStatus.revId = null;
75
- }
76
- sendSlackMessage(job.data.slackToken, `A downtime was resolved on ${domain}, deployment job resumed.`).catch((e) =>
77
- db.log.warn("Can't send slack message", e),
78
- );
79
- }
80
-
81
- if (newStatus.state === "downtime" && status.data?.state === "healthy") {
82
- if (!job.data.dryRun) {
83
- // block refresh while the page is down
84
- newStatus.revId = await scheduleJob(db, domain, "deployment", 12 * 60 * 60 * 1000);
85
- db.log.info(`Downtime detection scheduled new job ${newStatus.revId} for ${domain}`);
86
- }
87
- sendSlackMessage(job.data.slackToken, `A downtime was detected on ${domain}, deployment job scheduled.`).catch(
88
- (e) => db.log.warn("Can't send slack message", e),
89
- );
90
- }
91
-
92
- // persisted by the job itself
93
- status.error = null;
94
- status.data = newStatus;
95
- }
96
-
97
- /**
98
- * Schedules a job to handle downtime detection for a given domain.
99
- *
100
- * @param {Object} db - The database connection or message dispatch object used for sending messages.
101
- * @param {string} domain - The domain for which the downtime job should be scheduled.
102
- * @param {string} jobType - The type of job to be scheduled.
103
- * @param {number} [delay=0] - The delay (in milliseconds) before the job is executed.
104
- * @return {Promise<string>} A promise that resolves to the unique identifier of the scheduled job.
105
- */
106
- async function scheduleJob(db, domain, jobType, delay = 0) {
107
- const data = {
108
- name: `Downtime ${domain}`,
109
- type: jobType,
110
- filter: {
111
- contentTypes: ["document"],
112
- origins: [`https://${domain}`],
113
- },
114
- triggeredBy: "DowntimeDetection",
115
- scheduleIn: delay,
116
- };
117
-
118
- const msg = new message.RevalidateAssets(data);
119
- const response = await db.send(msg);
120
- const jobId = response.entity.statusId;
121
-
122
- return jobId;
123
- }
124
-
125
- /**
126
- * Resumes a job by updating its status to "RUNNING".
127
- *
128
- * @param {baqend} db - The baqend instance
129
- * @param {string} jobId - The unique identifier of the job to be resumed.
130
- * @return {Promise<void>} A promise that resolves once the job status update has been sent.
131
- */
132
- async function resumeJob(db, jobId) {
133
- const msg = new message.EditRevalidationJob(jobId, { status: "RUNNING" });
134
- try {
135
- await db.send(msg);
136
- } catch (e) {
137
- if (e.status === 200) {
138
- return; // there is a bug in the server that it currently returns 200 instead of 204 that confuse the SDK
139
- }
140
- throw e;
141
- }
142
- }
143
-
144
- /**
145
- * Returns whether the landing page is not reachable.
146
- *
147
- * @param {string} landingPage The page to test.
148
- * @param {RegExp} maintenancePageRegex The pattern used to identify redirects to maintenance page.
149
- * @param retry The number of already failed tries to load the page.
150
- * @returns `true` if the landing page is not reachable, `false` otherwise.
151
- */
152
- async function isPageDown(landingPage, maintenancePageRegex, retry = 0) {
153
- const res = await fetch(landingPage, { redirect: "manual" });
154
-
155
- // Valid response
156
- if (res.status < 300) {
157
- return false;
158
- }
159
-
160
- // Redirect
161
- if (res.status < 400) {
162
- return isMaintenanceRedirect(res, maintenancePageRegex);
163
- }
164
-
165
- // Error
166
- if (retry > 3) {
167
- return true;
168
- }
169
-
170
- // Retry after error
171
- await sleep(10000);
172
- return isPageDown(landingPage, maintenancePageRegex, retry + 1);
173
- }
174
-
175
- /**
176
- * Returns whether the response redirects to the maintenance page.
177
- *
178
- * @param res {Response} The fetch result of
179
- * @param maintenancePageRegex The page to identify redirects to maintenance page.
180
- */
181
- async function isMaintenanceRedirect(res, maintenancePageRegex) {
182
- const location = res.headers.get("Location") || "";
183
- return !!location.match(maintenancePageRegex);
184
- }
185
-
186
- async function sleep(milliseconds) {
187
- return new Promise((res) => setTimeout(res, milliseconds));
188
- }
189
-
190
- /**
191
- * Sends a message to a Slack channel using the provided webhook token.
192
- *
193
- * @param {string} token - The Slack webhook URL to which the message will be sent.
194
- * @param {string} message - The message content to be sent to the Slack channel.
195
- * @return {Promise<void>} A promise resolving to the response of the fetch request.
196
- */
197
- async function sendSlackMessage(token, message) {
198
- if (!token) {
199
- return;
200
- }
201
- const body = JSON.stringify({
202
- username: "Downtime Detection",
203
- text: message,
204
- });
205
- await fetch(token, { method: "POST", body });
206
- }
207
-
208
- exports.get = get;
209
- exports.run = run;