Repository: getsentry/sentry-javascript (TypeScript)

Issue:
node-cron and node-schedule fire checkin only on app start
### Is there an existing issue for this?

- [X] I have checked for existing issues https://github.com/getsentry/sentry-javascript/issues
- [X] I have reviewed the documentation https://docs.sentry.io/
- [X] I am using the latest SDK release https://github.com/getsentry/sentry-javascript/releases

### How do you use Sentry?

Sentry Saas (sentry.io)

### Which SDK are you using?

@sentry/node

### SDK Version

7.112.1

### Framework Version

express 4.19.2

### Link to Sentry event

_No response_

### SDK Setup

```js
Sentry.init({
  dsn: env.DSN,
  debug: true,
  tracesSampleRate: 1,
  profilesSampleRate: 1,
  integrations: [new Sentry.Integrations.Express({ app })],
});
```

### Steps to Reproduce

1. Created an example app with `cron`, `node-cron` and `node-schedule`
2. look at the log output

### Expected Result

When a job from `cron` is executed, the Sentry logs are correct. But `node-cron` and `node-schedule` are only logged on app start. Those should also log like `cron`.

### Actual Result

This is the log. `node-schedule` and `node-cron` produce no Sentry logs anymore.

```
Sentry Logger [info]: Sending checkin: node-cron_slug in_progress
Sentry Logger [info]: Sending checkin: node-cron_slug ok
Sentry Logger [info]: Sending checkin: node-schedule_slug in_progress
Sentry Logger [info]: Sending checkin: node-schedule_slug ok
Example app listening on port 3030                           <-- app starts here, but node-cron and node-schedule are already sending checkins
Sentry Logger [info]: Sending checkin: cron_slug in_progress
cron: Job is running every few seconds                       <-- cron job runs and logs correct info about checkin
Sentry Logger [info]: Sending checkin: cron_slug ok
node-cron: Job is running every few seconds                  <-- node-cron job runs, but no logs
node-schedule: Job is running every few seconds              <-- node-schedule job runs, but no logs
```



Fix (patch):
diff --git a/packages/node/src/cron/cron.ts b/packages/node/src/cron/cron.ts
index 8b6fc324a7a6..ce6225ced2fa 100644
--- a/packages/node/src/cron/cron.ts
+++ b/packages/node/src/cron/cron.ts
@@ -1,4 +1,4 @@
-import { withMonitor } from '@sentry/core';
+import { captureException, withMonitor } from '@sentry/core';
 import { replaceCronNames } from './common';
 
 export type CronJobParams = {
@@ -92,11 +92,16 @@ export function instrumentCron<T>(lib: T & CronJobConstructor, monitorSlug: stri
 
       const cronString = replaceCronNames(cronTime);
 
-      function monitoredTick(context: unknown, onComplete?: unknown): void | Promise<void> {
+      async function monitoredTick(context: unknown, onComplete?: unknown): Promise<void> {
         return withMonitor(
           monitorSlug,
-          () => {
-            return onTick(context, onComplete);
+          async () => {
+            try {
+              await onTick(context, onComplete);
+            } catch (e) {
+              captureException(e);
+              throw e;
+            }
           },
           {
             schedule: { type: 'crontab', value: cronString },
@@ -124,11 +129,16 @@ export function instrumentCron<T>(lib: T & CronJobConstructor, monitorSlug: stri
 
           const cronString = replaceCronNames(cronTime);
 
-          param.onTick = (context: unknown, onComplete?: unknown) => {
+          param.onTick = async (context: unknown, onComplete?: unknown) => {
             return withMonitor(
               monitorSlug,
-              () => {
-                return onTick(context, onComplete);
+              async () => {
+                try {
+                  await onTick(context, onComplete);
+                } catch (e) {
+                  captureException(e);
+                  throw e;
+                }
               },
               {
                 schedule: { type: 'crontab', value: cronString },
diff --git a/packages/node/src/cron/node-cron.ts b/packages/node/src/cron/node-cron.ts
index 4495a0b54909..efa7cde2b698 100644
--- a/packages/node/src/cron/node-cron.ts
+++ b/packages/node/src/cron/node-cron.ts
@@ -1,4 +1,4 @@
-import { withMonitor } from '@sentry/core';
+import { captureException, withMonitor } from '@sentry/core';
 import { replaceCronNames } from './common';
 
 export interface NodeCronOptions {
@@ -15,7 +15,7 @@ export interface NodeCron {
  *
  * ```ts
  * import * as Sentry from "@sentry/node";
- * import cron from "node-cron";
+ * import * as cron from "node-cron";
  *
  * const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron);
  *
@@ -35,22 +35,33 @@ export function instrumentNodeCron<T>(lib: Partial<NodeCron> & T): T {
         // When 'get' is called for schedule, return a proxied version of the schedule function
         return new Proxy(target.schedule, {
           apply(target, thisArg, argArray: Parameters<NodeCron['schedule']>) {
-            const [expression, , options] = argArray;
+            const [expression, callback, options] = argArray;
 
             if (!options?.name) {
               throw new Error('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.');
             }
 
-            return withMonitor(
-              options.name,
-              () => {
-                return target.apply(thisArg, argArray);
-              },
-              {
-                schedule: { type: 'crontab', value: replaceCronNames(expression) },
-                timezone: options?.timezone,
-              },
-            );
+            async function monitoredCallback(): Promise<void> {
+              return withMonitor(
+                options.name,
+                async () => {
+                  // We have to manually catch here and capture the exception because node-cron swallows errors
+                  // https://github.com/node-cron/node-cron/issues/399
+                  try {
+                    return await callback();
+                  } catch (e) {
+                    captureException(e);
+                    throw e;
+                  }
+                },
+                {
+                  schedule: { type: 'crontab', value: replaceCronNames(expression) },
+                  timezone: options?.timezone,
+                },
+              );
+            }
+
+            return target.apply(thisArg, [expression, monitoredCallback, options]);
           },
         });
       } else {
diff --git a/packages/node/src/cron/node-schedule.ts b/packages/node/src/cron/node-schedule.ts
index 79ae44a06e52..35db51618b9a 100644
--- a/packages/node/src/cron/node-schedule.ts
+++ b/packages/node/src/cron/node-schedule.ts
@@ -30,9 +30,13 @@ export function instrumentNodeSchedule<T>(lib: T & NodeSchedule): T {
         // eslint-disable-next-line @typescript-eslint/unbound-method
         return new Proxy(target.scheduleJob, {
           apply(target, thisArg, argArray: Parameters<NodeSchedule['scheduleJob']>) {
-            const [nameOrExpression, expressionOrCallback] = argArray;
+            const [nameOrExpression, expressionOrCallback, callback] = argArray;
 
-            if (typeof nameOrExpression !== 'string' || typeof expressionOrCallback !== 'string') {
+            if (
+              typeof nameOrExpression !== 'string' ||
+              typeof expressionOrCallback !== 'string' ||
+              typeof callback !== 'function'
+            ) {
               throw new Error(
                 "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string",
               );
@@ -41,15 +45,19 @@ export function instrumentNodeSchedule<T>(lib: T & NodeSchedule): T {
             const monitorSlug = nameOrExpression;
             const expression = expressionOrCallback;
 
-            return withMonitor(
-              monitorSlug,
-              () => {
-                return target.apply(thisArg, argArray);
-              },
-              {
-                schedule: { type: 'crontab', value: replaceCronNames(expression) },
-              },
-            );
+            async function monitoredCallback(): Promise<void> {
+              return withMonitor(
+                monitorSlug,
+                async () => {
+                  await callback?.();
+                },
+                {
+                  schedule: { type: 'crontab', value: replaceCronNames(expression) },
+                },
+              );
+            }
+
+            return target.apply(thisArg, [monitorSlug, expression, monitoredCallback]);
           },
         });
       }
diff --git a/yarn.lock b/yarn.lock
index 236f8b18b110..12513abe34d4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7945,6 +7945,11 @@
   resolved "https://registry.yarnpkg.com/@types/node-abi/-/node-abi-3.0.3.tgz#a8334d75fe45ccd4cdb2a6c1ae82540a7a76828c"
   integrity sha512-5oos6sivyXcDEuVC5oX3+wLwfgrGZu4NIOn826PGAjPCHsqp2zSPTGU7H1Tv+GZBOiDUY3nBXY1MdaofSEt4fw==
 
+"@types/node-cron@^3.0.11":
+  version "3.0.11"
+  resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344"
+  integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==
+
 "@types/node-fetch@^2.6.0":
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
@@ -7960,6 +7965,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/node-schedule@^2.1.7":
+  version "2.1.7"
+  resolved "https://registry.yarnpkg.com/@types/node-schedule/-/node-schedule-2.1.7.tgz#79a1e61adc7bbf8d8eaabcef307e07d76cb40d82"
+  integrity sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==
+  dependencies:
+    "@types/node" "*"
+
 "@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
   version "17.0.38"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947"
@@ -12709,6 +12721,13 @@ critters@0.0.16:
     postcss "^8.3.7"
     pretty-bytes "^5.3.0"
 
+cron-parser@^4.2.0:
+  version "4.9.0"
+  resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.9.0.tgz#0340694af3e46a0894978c6f52a6dbb5c0f11ad5"
+  integrity sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==
+  dependencies:
+    luxon "^3.2.1"
+
 cron@^3.1.6:
   version "3.1.6"
   resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.6.tgz#e7e1798a468e017c8d31459ecd7c2d088f97346c"
@@ -20338,6 +20357,11 @@ loglevel@^1.6.8:
   resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114"
   integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==
 
+long-timeout@0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514"
+  integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==
+
 long@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
@@ -20440,7 +20464,7 @@ lunr@^2.3.8:
   resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
   integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
 
-luxon@~3.4.0:
+luxon@^3.2.1, luxon@~3.4.0:
   version "3.4.4"
   resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af"
   integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==
@@ -22157,6 +22181,13 @@ node-addon-api@^6.1.0:
   resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
   integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
 
+node-cron@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.3.tgz#c4bc7173dd96d96c50bdb51122c64415458caff2"
+  integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==
+  dependencies:
+    uuid "8.3.2"
+
 node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7:
   version "2.6.7"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
@@ -22291,6 +22322,15 @@ node-releases@^2.0.6:
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
   integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
 
+node-schedule@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-2.1.1.tgz#6958b2c5af8834954f69bb0a7a97c62b97185de3"
+  integrity sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==
+  dependencies:
+    cron-parser "^4.2.0"
+    long-timeout "0.1.1"
+    sorted-array-functions "^1.3.0"
+
 node-source-walk@^4.0.0, node-source-walk@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/node-source-walk/-/node-source-walk-4.2.0.tgz#c2efe731ea8ba9c03c562aa0a9d984e54f27bc2c"
@@ -26755,6 +26795,11 @@ sort-package-json@^1.57.0:
     is-plain-obj "2.1.0"
     sort-object-keys "^1.1.3"
 
+sorted-array-functions@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
+  integrity sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==
+
 source-list-map@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"