diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f5a9fe9f8..caa1220ac 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -7,7 +7,7 @@ parameters: - message: "#^Offset 'host' does not exist on array\\{scheme\\: 'http'\\|'https', host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\.$#" - count: 1 + count: 2 path: src/Dsn.php - @@ -200,6 +200,11 @@ parameters: count: 1 path: src/Options.php + - + message: "#^Method Sentry\\\\Options\\:\\:getOrgId\\(\\) should return int\\|null but returns mixed\\.$#" + count: 1 + path: src/Options.php + - message: "#^Method Sentry\\\\Options\\:\\:getPrefixes\\(\\) should return array\\ but returns mixed\\.$#" count: 1 diff --git a/src/Dsn.php b/src/Dsn.php index 37aaca162..b6f952297 100644 --- a/src/Dsn.php +++ b/src/Dsn.php @@ -12,6 +12,12 @@ */ final class Dsn implements \Stringable { + /** + * @var string Regex to match the organization ID in the host. + * This only applies to Sentry SaaS DSNs that contain the organization ID. + */ + private const SENTRY_ORG_ID_REGEX = '/^o(\d+)\./'; + /** * @var string The protocol to be used to access the resource */ @@ -42,6 +48,11 @@ final class Dsn implements \Stringable */ private $path; + /** + * @var int|null + */ + private $orgId; + /** * Class constructor. * @@ -51,15 +62,17 @@ final class Dsn implements \Stringable * @param string $projectId The ID of the resource to access * @param string $path The specific resource that the web client wants to access * @param string $publicKey The public key to authenticate the SDK + * @param ?int $orgId The org ID */ - private function __construct(string $scheme, string $host, int $port, string $projectId, string $path, string $publicKey) + private function __construct(string $scheme, string $host, int $port, string $projectId, string $path, string $publicKey, ?int $orgId = null) { $this->scheme = $scheme; $this->host = $host; $this->port = $port; - $this->publicKey = $publicKey; - $this->path = $path; $this->projectId = $projectId; + $this->path = $path; + $this->publicKey = $publicKey; + $this->orgId = $orgId; } /** @@ -94,13 +107,19 @@ public static function createFromString(string $value): self $path = substr($parsedDsn['path'], 0, $lastSlashPosition); } + $orgId = null; + if (preg_match(self::SENTRY_ORG_ID_REGEX, $parsedDsn['host'], $matches) == 1) { + $orgId = (int) $matches[1]; + } + return new self( $parsedDsn['scheme'], $parsedDsn['host'], $parsedDsn['port'] ?? ($parsedDsn['scheme'] === 'http' ? 80 : 443), $projectId, $path, - $parsedDsn['user'] + $parsedDsn['user'], + $orgId ); } @@ -152,6 +171,11 @@ public function getPublicKey(): string return $this->publicKey; } + public function getOrgId(): ?int + { + return $this->orgId; + } + /** * Returns the URL of the API for the envelope endpoint. */ diff --git a/src/Options.php b/src/Options.php index eb4ffb1f1..469bd1d22 100644 --- a/src/Options.php +++ b/src/Options.php @@ -436,6 +436,20 @@ public function getDsn(): ?Dsn return $this->options['dsn']; } + public function getOrgId(): ?int + { + return $this->options['org_id']; + } + + public function setOrgId(int $orgId): self + { + $options = array_merge($this->options, ['org_id' => $orgId]); + + $this->options = $this->resolver->resolve($options); + + return $this; + } + /** * Gets the name of the server the SDK is running on (e.g. the hostname). */ @@ -1146,6 +1160,7 @@ private function configureOptions(OptionsResolver $resolver): void 'spotlight_url' => 'http://localhost:8969', 'release' => $_SERVER['SENTRY_RELEASE'] ?? $_SERVER['AWS_LAMBDA_FUNCTION_VERSION'] ?? null, 'dsn' => $_SERVER['SENTRY_DSN'] ?? null, + 'org_id' => null, 'server_name' => gethostname(), 'ignore_exceptions' => [], 'ignore_transactions' => [], @@ -1206,6 +1221,7 @@ private function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('spotlight_url', 'string'); $resolver->setAllowedTypes('release', ['null', 'string']); $resolver->setAllowedTypes('dsn', ['null', 'string', 'bool', Dsn::class]); + $resolver->setAllowedTypes('org_id', ['null', 'int']); $resolver->setAllowedTypes('server_name', 'string'); $resolver->setAllowedTypes('before_send', ['callable']); $resolver->setAllowedTypes('before_send_transaction', ['callable']); diff --git a/src/Tracing/DynamicSamplingContext.php b/src/Tracing/DynamicSamplingContext.php index 55dafa5a4..bcdf6c959 100644 --- a/src/Tracing/DynamicSamplingContext.php +++ b/src/Tracing/DynamicSamplingContext.php @@ -172,6 +172,9 @@ public static function fromTransaction(Transaction $transaction, HubInterface $h if ($options->getDsn() !== null && $options->getDsn()->getPublicKey() !== null) { $samplingContext->set('public_key', $options->getDsn()->getPublicKey()); } + if ($options->getDsn() !== null && $options->getDsn()->getOrgId() !== null) { + $samplingContext->set('org_id', (string) $options->getDsn()->getOrgId()); + } if ($options->getRelease() !== null) { $samplingContext->set('release', $options->getRelease()); @@ -209,6 +212,10 @@ public static function fromOptions(Options $options, Scope $scope): self $samplingContext->set('public_key', $options->getDsn()->getPublicKey()); } + if ($options->getDsn() !== null && $options->getDsn()->getOrgId() !== null) { + $samplingContext->set('org_id', (string) $options->getDsn()->getOrgId()); + } + if ($options->getRelease() !== null) { $samplingContext->set('release', $options->getRelease()); } diff --git a/tests/DsnTest.php b/tests/DsnTest.php index 724d00538..a6be7a144 100644 --- a/tests/DsnTest.php +++ b/tests/DsnTest.php @@ -22,7 +22,8 @@ public function testCreateFromString( int $expectedPort, string $expectedPublicKey, string $expectedProjectId, - string $expectedPath + string $expectedPath, + ?int $expectedOrgId, ): void { $dsn = Dsn::createFromString($value); @@ -32,6 +33,7 @@ public function testCreateFromString( $this->assertSame($expectedPublicKey, $dsn->getPublicKey()); $this->assertSame($expectedProjectId, $dsn->getProjectId(true)); $this->assertSame($expectedPath, $dsn->getPath()); + $this->assertSame($expectedOrgId, $dsn->getOrgId()); } public static function createFromStringDataProvider(): \Generator @@ -44,6 +46,7 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '/sentry', + null, ]; yield [ @@ -54,6 +57,18 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '', + null, + ]; + + yield [ + 'http://public@o1.example.com/1', + 'http', + 'o1.example.com', + 80, + 'public', + '1', + '', + 1, ]; yield [ @@ -64,6 +79,7 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '', + null, ]; yield [ @@ -74,6 +90,7 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '', + null, ]; yield [ @@ -84,6 +101,7 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '', + null, ]; yield [ @@ -94,6 +112,7 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '', + null, ]; yield [ @@ -104,6 +123,7 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '', + null, ]; yield [ @@ -114,6 +134,7 @@ public static function createFromStringDataProvider(): \Generator 'public', '1', '', + null, ]; } @@ -240,6 +261,7 @@ public static function toStringDataProvider(): array return [ ['http://public@example.com/sentry/1'], ['http://public@example.com/1'], + ['http://public@01.example.com/1'], ['http://public@example.com:8080/sentry/1'], ['https://public@example.com/sentry/1'], ['https://public@example.com:4343/sentry/1'],