diff --git a/.github/workflows/package-and-attach-to-release.yml b/.github/workflows/package-and-attach-to-release.yml index 41c28a80..64f7d90b 100644 --- a/.github/workflows/package-and-attach-to-release.yml +++ b/.github/workflows/package-and-attach-to-release.yml @@ -16,13 +16,26 @@ jobs: - name: Checkout uses: actions/checkout@v5.0.0 + - name: Set environment to prod + run: | + sed -i "s/set_config(CONFKEY::SENTRY_ENV, ENVIRONMENT::DEV, 'local_lbplanner')/set_config(CONFKEY::SENTRY_ENV, ENVIRONMENT::PROD, 'local_lbplanner')/g" "$TARGET_FOLDER/version.php" + - name: Extract version id: version run: | version=$(grep -oP '\$plugin->version\s*=\s*\K[0-9]+' "$TARGET_FOLDER/version.php") echo "version=$version" >> $GITHUB_OUTPUT - + + - name: Setup PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: # see gha.dist.yml + php-version: '8.1' + coverage: none + - name: Get php libs + run: | + composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + - name: Create ZIP file run: | ZIP_NAME="${MOODLE_INSTALL_DIR}_${PACKAGE_NAME}_${VERSION}.zip" diff --git a/.gitignore b/.gitignore index b1203584..b760de7b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ moodle *.zip lbplanner.dart/test/env.dart example -.idea \ No newline at end of file +.idea +lbplanner/vendor \ No newline at end of file diff --git a/.kateproject b/.kateproject index ca3bca28..5f9c799c 100644 --- a/.kateproject +++ b/.kateproject @@ -7,7 +7,7 @@ "settings": { "intelephense": { "environment": { - "phpVersion": "8.0.0" + "phpVersion": "8.1.0" } } } diff --git a/README.md b/README.md index a63646b0..74c7f279 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ # LB Planer Plugin This repository contains the source code for the LB Planer Moodle plugin. Api endpoints are documented [here](https://necodeit.github.io/lb_planner_docs/moodle/index.html) + +## Setup + +### Bundled Dependencies + +Run `composer up` here to download dependencies to the vendor folder. +Note that dependencies already present in moodle are excluded from the tree. + +### Dev Env + +You can do this multiple ways, but we personally have a directory structure like so: + +``` +-root ← top folder. any name works + |-plugin ← this folder + |-moodle ← folder with moodle in it + |-local ← moodle's plugin folder + |-lbplanner ← symlink to the lbplanner folder + |-modcustomfields ← a dependency of ours (get at https://gitlab.com/adapta/moodle-local_modcustomfields/) +``` + +Using [kate](https://kate-editor.org/) with [intelephense](https://intelephense.com/) is recommended. + +## Packaging + +Zip the lbplanner folder. as shrimple as that. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..b031b618 --- /dev/null +++ b/composer.json @@ -0,0 +1,20 @@ +{ + "require": { + "sentry/sentry": "^4.16", + "php": ">=8.1" + }, + "replace": { + "symfony/deprecation-contracts": "*", + "guzzlehttp/psr7": "*", + "psr/http-factory": "*", + "psr/http-message": "*", + "psr/log": "*", + "ralouphie/getallheaders": "*" + }, + "config": { + "vendor-dir": "lbplanner/vendor", + "platform": { + "php": "8.1.0" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 00000000..9a1ffa0b --- /dev/null +++ b/composer.lock @@ -0,0 +1,294 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0c2c42c4c9cc556c76976c609d182626", + "packages": [ + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "sentry/sentry", + "version": "4.16.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "c5b086e4235762da175034bc463b0d31cbb38d2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/c5b086e4235762da175034bc463b0d31cbb38d2e", + "reference": "c5b086e4235762da175034bc463b0d31cbb38d2e", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" + }, + "conflict": { + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "monolog/monolog": "^1.6|^2.0|^3.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^8.5|^9.6", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "profiling", + "sentry", + "tracing" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/4.16.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2025-09-22T13:38:03+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v6.4.25", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d28e7e2db8a73e9511df892d36445f61314bbebe", + "reference": "d28e7e2db8a73e9511df892d36445f61314bbebe", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v6.4.25" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-04T17:06:28+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.1.0" + }, + "plugin-api-version": "2.6.0" +} diff --git a/lbplanner/classes/enums/ENVIRONMENT.php b/lbplanner/classes/enums/ENVIRONMENT.php new file mode 100644 index 00000000..7eda5640 --- /dev/null +++ b/lbplanner/classes/enums/ENVIRONMENT.php @@ -0,0 +1,44 @@ +. + +/** + * software environments + * + * @package local_lbplanner + * @subpackage enums + * @copyright 2025 Pallasys + * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later + */ + +namespace local_lbplanner\enums; + +// TODO: revert to native enums once we migrate to php8. + +use local_lbplanner\polyfill\Enum; + +/** + * Software environments + */ +class ENVIRONMENT extends Enum { + /** + * Key for the production environment. + */ + const PROD = 'production'; + /** + * Key for the development environment. + */ + const DEV = 'development'; +} diff --git a/lbplanner/classes/enums/SETTINGS.php b/lbplanner/classes/enums/SETTINGS.php index 4b4ee540..26414a50 100644 --- a/lbplanner/classes/enums/SETTINGS.php +++ b/lbplanner/classes/enums/SETTINGS.php @@ -30,9 +30,19 @@ use local_lbplanner\polyfill\Enum; /** - * The keys for plugin settings + * The keys for plugin settings/configs */ class SETTINGS extends Enum { + /** + * Key for the release version. + * NOTE: This is a constant! Do not set outside version.php under ANY circumstances! + */ + const V_RELEASE = 'release'; + /** + * Key for the full version number in $plugin->version. + * NOTE: This is a constant! Do not set outside version.php under ANY circumstances! + */ + const V_FULLNUM = 'release_fullnum'; /** * Key for the setting for how many days into the future a student should be able to reserve a slot. */ @@ -41,4 +51,16 @@ class SETTINGS extends Enum { * Key for the setting for how long a course should be expired for until it counts as outdated and gets culled. */ const COURSE_OUTDATERANGE = 'course_outdaterange'; + /** + * Key for the setting for where sentry events should be sent to. + */ + const SENTRY_DSN = 'sentry_dsn'; + /** + * Key for the sentry environment. + */ + const SENTRY_ENV = 'sentry_environment'; + /** + * Key for the custom field category ID. + */ + const CF_CATID = 'categoryid'; } diff --git a/lbplanner/classes/helpers/config_helper.php b/lbplanner/classes/helpers/config_helper.php index 7ea85156..b2b63343 100644 --- a/lbplanner/classes/helpers/config_helper.php +++ b/lbplanner/classes/helpers/config_helper.php @@ -43,7 +43,7 @@ public static function add_customfield(): void { $handler = mod_handler::create(); $categoryid = $handler->create_category('LB Planer'); - set_config('categoryid', $categoryid, 'local_lbplanner'); + set_config(SETTINGS::CF_CATID, $categoryid, 'local_lbplanner'); $categorycontroller = category_controller::create($categoryid, null, $handler); $categorycontroller->save(); @@ -77,7 +77,7 @@ public static function add_customfield(): void { public static function remove_customfield(): void { $handler = mod_handler::create(); $catid = self::get_category_id(); - unset_config('categoryid', 'local_lbplanner'); + unset_config(SETTINGS::CF_CATID, 'local_lbplanner'); if ($catid !== -1) { $catcontroller = category_controller::create($catid, null, $handler); $handler->delete_category($catcontroller); @@ -89,7 +89,7 @@ public static function remove_customfield(): void { * @return int the category id if it is set, -1 otherwise */ public static function get_category_id(): int { - $catid = get_config('local_lbplanner', 'categoryid'); + $catid = get_config('local_lbplanner', SETTINGS::CF_CATID); if ($catid === false) { return -1; } else { @@ -112,4 +112,12 @@ public static function get_slot_futuresight(): int { public static function get_course_outdatedrange(): int { return get_config('local_lbplanner', SETTINGS::COURSE_OUTDATERANGE); } + + /** + * Get the Sentry DSN - for where to send error debugging info to. + * @return string the sentry DSN + */ + public static function get_sentry_dsn(): string { + return get_config('local_lbplanner', SETTINGS::SENTRY_DSN); + } } diff --git a/lbplanner/classes/helpers/course_helper.php b/lbplanner/classes/helpers/course_helper.php index 5e49aa06..6cd9ca63 100644 --- a/lbplanner/classes/helpers/course_helper.php +++ b/lbplanner/classes/helpers/course_helper.php @@ -88,6 +88,10 @@ public static function get_eduplanner_courses(bool $onlyenrolled): array { global $DB, $USER; $userid = $USER->id; + $sentryspan = sentry_helper::span_start(__FUNCTION__, ['onlyenrolled' => $onlyenrolled]); + + // TODO: rewrite this where it asks the DB for all lbp courses, and then for any mdlcourses that aren't in lbpc. + $lbptag = core_tag_tag::get_by_name(core_tag_collection::get_default(), self::EDUPLANNER_TAG, strictness:MUST_EXIST); $courseexpireseconds = config_helper::get_course_outdatedrange(); $courseexpiredate = (new DateTimeImmutable("{$courseexpireseconds} seconds ago"))->getTimestamp(); @@ -177,6 +181,9 @@ public static function get_eduplanner_courses(bool $onlyenrolled): array { $fetchedcourse->set_mdlcourse($mdlcourse); array_push($results, $fetchedcourse); } + + $sentryspan->setData(['count_out' => count($results)]); + sentry_helper::span_end($sentryspan); return $results; } diff --git a/lbplanner/classes/helpers/modules_helper.php b/lbplanner/classes/helpers/modules_helper.php index f121918d..6660d6e6 100644 --- a/lbplanner/classes/helpers/modules_helper.php +++ b/lbplanner/classes/helpers/modules_helper.php @@ -207,6 +207,7 @@ public static function get_module_status(module $module, int $userid, ?int $plan public static function get_all_modules_by_course(int $courseid, bool $ekenabled): array { global $DB; + $sentryspan = sentry_helper::span_start(__FUNCTION__, ["ekenabled" => $ekenabled]); $cmodules = $DB->get_records( self::COURSE_MODULES_TABLE, [ @@ -227,6 +228,7 @@ public static function get_all_modules_by_course(int $courseid, bool $ekenabled) array_push($modules, $module); } + sentry_helper::span_end($sentryspan); return $modules; } } diff --git a/lbplanner/classes/helpers/plan_helper.php b/lbplanner/classes/helpers/plan_helper.php index 50919e75..fa82bb1f 100644 --- a/lbplanner/classes/helpers/plan_helper.php +++ b/lbplanner/classes/helpers/plan_helper.php @@ -146,6 +146,7 @@ public static function check_edit_permissions(int $planid, int $userid): bool { */ public static function get_deadlines(int $planid): array { global $DB; + $sentryspan = sentry_helper::span_start(__FUNCTION__); $dbdeadlines = $DB->get_records(self::DEADLINES_TABLE, ['planid' => $planid]); @@ -159,6 +160,7 @@ public static function get_deadlines(int $planid): array { ]; } + sentry_helper::span_end($sentryspan); return $deadlines; } diff --git a/lbplanner/classes/helpers/sentry_helper.php b/lbplanner/classes/helpers/sentry_helper.php new file mode 100644 index 00000000..a796b407 --- /dev/null +++ b/lbplanner/classes/helpers/sentry_helper.php @@ -0,0 +1,171 @@ +. + +namespace local_lbplanner\helpers; + +use Throwable; +use local_lbplanner\enums\{ENVIRONMENT, SETTINGS}; +use local_lbplanner\helpers\config_helper; +use Sentry\SentrySdk; +use Sentry\Tracing\{Span, SpanContext, Transaction, TransactionContext}; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/local/lbplanner/vendor/autoload.php'); + +/** + * Provides helper methods for sentry logging + * + * @package local_lbplanner + * @subpackage helpers + * @copyright 2025 necodeIT + * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later + */ +class sentry_helper { + /** + * @var array $spans Remembers what spans have been in use. + */ + private static array $spans = []; + + /** + * This cache is needed because the value is read fairly often and determining it is somewhat expensive. + * @var bool $isenabled cache for enabled/disabled state of sentry reporting. + */ + private static ?bool $isenabled = null; + + /** + * Checks if moodle plugin is set to report exceptions to sentry + * @return bool whether sentry is to be used + */ + public static function enabled(): bool { + if (self::$isenabled === null) { + self::$isenabled = strlen(config_helper::get_sentry_dsn()) > 0; + } + return self::$isenabled; + } + /** + * Initializes the sentry library for future use. + */ + public static function init(): void { + if (self::enabled()) { + $env = get_config('local_lbplanner', SETTINGS::SENTRY_ENV); + $release = get_config('local_lbplanner', SETTINGS::V_RELEASE); + if ($env === ENVIRONMENT::DEV) { + $release .= '.' . get_config('local_lbplanner', SETTINGS::V_FULLNUM); + } + $cfg = [ + "dsn" => config_helper::get_sentry_dsn(), + 'in_app_include' => [realpath(__DIR__ . '/../..')], + 'in_app_exclude' => [realpath(__DIR__ . '/../../vendor'), '/'], + "enable_tracing" => true, + "traces_sample_rate" => 0.2, + "attach_stacktrace" => true, + "release" => 'lbplanner@' . $release, + "environment" => $env, + ]; + \Sentry\init($cfg); + } + } + /** + * Does a bunch of setup for measuring transaction duration. + * @param string $name name of the transaction to start + * @param string $op the operation this transaction is for + * @param ?array $data an assocarr of data to record for this transaction, or null + * @return ?Transaction the transaction that got started, or null if disabled + */ + public static function transaction_start(string $name, string $op, ?array $data = null): ?Transaction { + if (self::enabled()) { + if (SentrySdk::getCurrentHub()->getSpan() !== null) { + throw new \coding_exception('tried to start a new sentry transaction when there\'s already a span set'); + } + $ctx = TransactionContext::make() + ->setName($name) + ->setOp($op); + if ($data !== null) { + $ctx = $ctx->setData($data); + } + $transaction = \Sentry\startTransaction($ctx); + SentrySdk::getCurrentHub()->setSpan($transaction); + self::$spans[(string)$transaction->getSpanId()] = $transaction; + return $transaction; + } + return null; + } + /** + * Marks a transaction as ended + * @param ?Transaction $transaction the transaction to end + */ + public static function transaction_end(?Transaction $transaction): void { + self::span_end($transaction); // Transactions are just special spans. + } + /** + * Does a bunch of setup for measuring span duration. + * @param string $op the operation this span is for + * @param ?array $data an assocarr of data to record for this span, or null + * @return ?Span the span that got started, or null if disabled + */ + public static function span_start(string $op, ?array $data = null): ?Span { + if (self::enabled()) { + $ctx = SpanContext::make() + ->setOp($op); + if ($data !== null) { + $ctx = $ctx->setData($data); + } + $parent = SentrySdk::getCurrentHub()->getSpan(); + $span = $parent->startChild($ctx); + self::$spans[(string)$span->getSpanId()] = $span; + return $span; + } + return null; + } + /** + * Marks a span as ended + * @param ?Span $span the span to end + */ + public static function span_end(?Span $span): void { + if ($span !== null) { + $span->finish(); + $parentid = $span->getParentSpanId(); + if ($parentid === null) { + // Probably a transaction. No parent, thus no new active span. + $parent = null; + } else { + // Set currently active span to parent span (may be the transaction). + $parent = self::$spans[(string)$parentid]; + } + SentrySdk::getCurrentHub()->setSpan($parent); + } + } + /** + * Checks if any errors happened since init() and reports the most recent one to sentry if so. + * @return whether any errors were found + */ + public static function report_last_err(): bool { + if (self::enabled()) { + return \Sentry\captureLastError() !== null; + } + return false; + } + /** + * Reports an error to sentry + * @param Throwable $err the error + */ + public static function report_err(Throwable $err): void { + if (self::enabled()) { + \Sentry\captureException($err); + } + } +} diff --git a/lbplanner/classes/helpers/slot_helper.php b/lbplanner/classes/helpers/slot_helper.php index d2f94a7c..8d77d0bc 100644 --- a/lbplanner/classes/helpers/slot_helper.php +++ b/lbplanner/classes/helpers/slot_helper.php @@ -97,6 +97,8 @@ class slot_helper { */ public static function get_all_slots(): array { global $DB; + $sentryspan = sentry_helper::span_start(__FUNCTION__); + $slots = $DB->get_records(self::TABLE_SLOTS, []); $slotsobj = []; @@ -104,6 +106,7 @@ public static function get_all_slots(): array { array_push($slotsobj, slot::from_db($slot)); } + sentry_helper::span_end($sentryspan); return $slotsobj; } @@ -117,6 +120,7 @@ public static function get_all_slots(): array { */ public static function get_vintage_time_slots(string $vintage, int $today, int $range): array { global $DB; + $sentryspan = sentry_helper::span_start(__FUNCTION__, ['range' => $range]); if ($range < 7) { $valid = []; @@ -140,6 +144,7 @@ public static function get_vintage_time_slots(string $vintage, int $today, int $ array_push($slotsobj, slot::from_db($slot)); } + sentry_helper::span_end($sentryspan); return $slotsobj; } @@ -151,6 +156,7 @@ public static function get_vintage_time_slots(string $vintage, int $today, int $ */ public static function get_supervisor_slots(int $supervisorid): array { global $DB; + $sentryspan = sentry_helper::span_start(__FUNCTION__); $slots = $DB->get_records_sql( 'SELECT slot.* FROM {' . self::TABLE_SLOTS . '} as slot ' . @@ -164,6 +170,7 @@ public static function get_supervisor_slots(int $supervisorid): array { array_push($slotsobj, slot::from_db($slot)); } + sentry_helper::span_end($sentryspan); return $slotsobj; } @@ -175,6 +182,8 @@ public static function get_supervisor_slots(int $supervisorid): array { */ public static function get_slots_by_room(string $room): array { global $DB; + $sentryspan = sentry_helper::span_start(__FUNCTION__); + $slots = $DB->get_records(self::TABLE_SLOTS, ['room' => $room]); $slotsobj = []; @@ -182,6 +191,7 @@ public static function get_slots_by_room(string $room): array { array_push($slotsobj, slot::from_db($slot)); } + sentry_helper::span_end($sentryspan); return $slotsobj; } @@ -271,6 +281,8 @@ public static function get_reservations_for_user(int $userid): array { * @return reservation[] reservations that pass */ public static function filter_reservations_for_recency(array $reservations): array { + $sentryspan = sentry_helper::span_start(__FUNCTION__, ['count_in' => count($reservations)]); + $goodeggs = []; foreach ($reservations as $reservation) { if (!$reservation->is_outdated()) { @@ -278,6 +290,8 @@ public static function filter_reservations_for_recency(array $reservations): arr } } + $sentryspan->setData(['count_out' => count($goodeggs)]); + sentry_helper::span_end($sentryspan); return $goodeggs; } @@ -308,6 +322,8 @@ public static function get_filters_for_slot(int $slotid): array { * @return slot[] the filtered slot array */ public static function filter_slots_for_user(array $allslots, \stdClass $user): array { + $sentryspan = sentry_helper::span_start(__FUNCTION__, ['count_in' => count($allslots)]); + $mycourses = course_helper::get_eduplanner_courses(true); $mycourseids = []; foreach ($mycourses as $course) { @@ -332,6 +348,9 @@ public static function filter_slots_for_user(array $allslots, \stdClass $user): break; } } + + $sentryspan->setData(['count_out' => count($slots)]); + sentry_helper::span_end($sentryspan); return $slots; } @@ -342,6 +361,8 @@ public static function filter_slots_for_user(array $allslots, \stdClass $user): * @return slot[] the filtered slot array */ public static function filter_slots_for_time(array $allslots, int $range): array { + $sentryspan = sentry_helper::span_start(__FUNCTION__, ['count_in' => count($allslots)]); + if ($range === 7) { return $allslots; } @@ -357,6 +378,9 @@ public static function filter_slots_for_time(array $allslots, int $range): array array_push($slots, $slot); } } + + $sentryspan->setData(['count_out' => count($slots)]); + sentry_helper::span_end($sentryspan); return $slots; } diff --git a/lbplanner/lib.php b/lbplanner/lib.php new file mode 100644 index 00000000..07eaea0e --- /dev/null +++ b/lbplanner/lib.php @@ -0,0 +1,67 @@ +. + +/** + * Defines callbacks for moodle + * + * @package local_lbplanner + * @copyright 2025 Pallasys + * @license https://creativecommons.org/licenses/by-nc-sa/4.0/ CC-BY-NC-SA 4.0 International or later + */ + +use local_lbplanner\helpers\sentry_helper; + +/** + * Callback for any webservices that get called by external actors. + * We use this to catch whenever anything from us is being called, and do sentry setup and error reporting. + * @param stdClass $externalfunctioninfo external function info {@see external_api::external_function_info()} + * @param array $params the raw(ish) parameters that are going to get passed to the function implementing the API call + * @return mixed Either whatever the API call returned, or false if we don't wish to override anything. + */ +function local_lbplanner_override_webservice_execution(stdClass $externalfunctioninfo, array $params): mixed { + // Only override calling our own functions. + if ($externalfunctioninfo->component === 'local_lbplanner') { + sentry_helper::init(); + // Actually calling the function (since we're overriding this part, duh). + try { + $callable = [$externalfunctioninfo->classname, $externalfunctioninfo->methodname]; + $transaction = sentry_helper::transaction_start($callable[0], $callable[1]); + $result = call_user_func_array($callable, $params); + sentry_helper::transaction_end($transaction); + + // Report if call_user_func_array itself had some kind of issue. + if ($result === false) { + $paramsstring = var_export($params, true); + throw new \coding_exception( + "webservice override: call_user_func_array returned with false at " + . $externalfunctioninfo->classname + . "::" + . $externalfunctioninfo->methodname + . "(" + . $paramsstring + . ");" + ); + } + + return $result; + } catch (\Throwable $e) { + sentry_helper::report_err($e); + throw $e; + } + } + + return false; +} diff --git a/lbplanner/services/modules/get_all_modules.php b/lbplanner/services/modules/get_all_modules.php index dd06e8d9..09de5d57 100644 --- a/lbplanner/services/modules/get_all_modules.php +++ b/lbplanner/services/modules/get_all_modules.php @@ -17,7 +17,7 @@ namespace local_lbplanner_services; use core_external\{external_api, external_function_parameters, external_multiple_structure, external_value}; -use local_lbplanner\helpers\{modules_helper, plan_helper, course_helper}; +use local_lbplanner\helpers\{modules_helper, plan_helper, course_helper, sentry_helper}; use local_lbplanner\model\module; /** @@ -71,7 +71,11 @@ public static function get_all_modules(bool $ekenabled): array { $modules ); } - return array_map(fn(module $m) => $m->prepare_for_api_personal($USER->id, $planid), $modules); + + $sentryspan = sentry_helper::span_start('prepare_module_array_for_api_personal', ['amount' => count($modules)]); + $result = array_map(fn(module $m) => $m->prepare_for_api_personal($USER->id, $planid), $modules); + sentry_helper::span_end($sentryspan); + return $result; } /** diff --git a/lbplanner/settings.php b/lbplanner/settings.php index add724a7..827607f2 100644 --- a/lbplanner/settings.php +++ b/lbplanner/settings.php @@ -57,4 +57,13 @@ ); $outdaterangesett->set_min_duration(0); $settings->add($outdaterangesett); + + $sentrydsnsett = new admin_setting_configtext( + 'local_lbplanner/' . SETTINGS::SENTRY_DSN, + 'Sentry DSN', + 'for where to send error debugging info to.', + '', + PARAM_TEXT + ); + $settings->add($sentrydsnsett); } diff --git a/lbplanner/thirdpartylibs.xml b/lbplanner/thirdpartylibs.xml new file mode 100644 index 00000000..44051736 --- /dev/null +++ b/lbplanner/thirdpartylibs.xml @@ -0,0 +1,15 @@ + + + + vendor + Sentry API SDK + An error reporting library. + 4.16 + MIT + + https://github.com/getsentry/sentry-php + + 2012 Functional Software, Inc. dba Sentry + + + \ No newline at end of file diff --git a/lbplanner/vendor/.gitkeep b/lbplanner/vendor/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lbplanner/version.php b/lbplanner/version.php index e18015ff..89f7924b 100644 --- a/lbplanner/version.php +++ b/lbplanner/version.php @@ -24,6 +24,8 @@ defined('MOODLE_INTERNAL') || die(); +use local_lbplanner\enums\{ENVIRONMENT, SETTINGS}; + $plugin->requires = 2024042200.00; // Require Moodle >=4.4.0. $plugin->maturity = MATURITY_BETA; $plugin->component = 'local_lbplanner'; @@ -34,4 +36,6 @@ 'local_modcustomfields' => 2023110600, ]; -set_config('release', $plugin->release, 'local_lbplanner'); +set_config(SETTINGS::V_RELEASE, $plugin->release, 'local_lbplanner'); +set_config(SETTINGS::V_FULLNUM, $plugin->version, 'local_lbplanner'); +set_config(SETTINGS::SENTRY_ENV, ENVIRONMENT::DEV, 'local_lbplanner'); // NOTE: gets set to 'production' by CI for release.