diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..bd0eeb36 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/devcontainers/base:0-focal + +RUN apt-get update && \ + apt-get install -y software-properties-common && \ + add-apt-repository ppa:ondrej/php -y && \ + apt-get update && \ + apt-get install -y \ + php7.4 php7.4-cli php7.4-common php7.4-curl \ + php7.4-mysql php7.4-bcmath php7.4-soap php7.4-zip php7.4-intl \ + php7.4-gd php7.4-xsl php7.4-dom php7.4-mbstring \ + unzip && \ + rm -rf /var/lib/apt/lists/* + +RUN curl -sS https://getcomposer.org/installer | php -- \ + --install-dir=/usr/local/bin --filename=composer \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..779933a9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,8 @@ +{ + "name": "Ona", + "build": { + "context": ".", + "dockerfile": "Dockerfile" + }, + "postCreateCommand": "ona automations update .ona/automations.yaml" +} \ No newline at end of file diff --git a/.github/workflows/artifact-release.yml b/.github/workflows/artifact-release.yml index 05fb2b2e..79316eeb 100644 --- a/.github/workflows/artifact-release.yml +++ b/.github/workflows/artifact-release.yml @@ -30,6 +30,13 @@ jobs: id: tag run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV" + - name: "Inject SigNoz API key" + shell: bash + env: + SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }} + run: | + sed -i "s|%%SIGNOZ_API_KEY%%|${SIGNOZ_API_KEY}|g" src/Monitoring/SignozServiceLogger.php + - name: Create release artifact run: | mkdir -p build diff --git a/.github/workflows/change-review.yml b/.github/workflows/change-review.yml index 8d3895ca..2bfbd7ca 100644 --- a/.github/workflows/change-review.yml +++ b/.github/workflows/change-review.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.4, 8.1, 8.2, 8.3] + php: [7.4, 8.1, 8.2, 8.3, 8.4] env: XDEBUG_MODE: coverage @@ -22,6 +22,7 @@ jobs: SECRET_KEY: ${{ secrets.SECRET_KEY }} ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} ENV: ${{ secrets.ENV }} + SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }} steps: - uses: actions/checkout@v3 @@ -48,6 +49,13 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-progress + - name: "Inject SigNoz API key" + shell: bash + env: + SIGNOZ_API_KEY: ${{ secrets.SIGNOZ_API_KEY }} + run: | + sed -i "s|%%SIGNOZ_API_KEY%%|${SIGNOZ_API_KEY}|g" src/Monitoring/SignozServiceLogger.php + - name: run unit tests and coverage scan run: ./vendor/bin/pest --coverage --min=20 --coverage-clover ./coverage.xml env: @@ -55,7 +63,6 @@ jobs: SECRET_KEY: ${{ secrets.SECRET_KEY }} ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} ENV: ${{ secrets.ENV }} - - name: Upload to Codecov uses: codecov/codecov-action@v2 with: diff --git a/.github/workflows/package-publish.yml b/.github/workflows/package-publish.yml index 5d005d22..65df0196 100644 --- a/.github/workflows/package-publish.yml +++ b/.github/workflows/package-publish.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: true matrix: - php: [7.4, 8.1, 8.2] + php: [7.4, 8.1, 8.2, 8.3, 8.4] steps: - uses: actions/checkout@v2 diff --git a/paymentForm.php b/paymentForm.php index 34bb9d59..1090588f 100644 --- a/paymentForm.php +++ b/paymentForm.php @@ -43,9 +43,9 @@ - + - +
diff --git a/processPayment.php b/processPayment.php index 0946145a..b595865b 100644 --- a/processPayment.php +++ b/processPayment.php @@ -11,7 +11,7 @@ use Flutterwave\Library\Modal; use \Flutterwave\Config\ForkConfig; -// start a session. +// start a session for redirect metadata. session_start(); // Define custom config. @@ -39,9 +39,15 @@ $controller = new PaymentController( $client, $customHandler, $modalType ); } catch(\Exception $e ) { echo $e->getMessage(); + exit(); } if ($_SERVER["REQUEST_METHOD"] === "POST") { + if ($controller === null) { + echo 'Unable to initialize payment controller.'; + exit(); + } + $request = $_REQUEST; $request['redirect_url'] = $_SERVER['HTTP_ORIGIN'] . $_SERVER['REQUEST_URI']; try { @@ -54,6 +60,11 @@ $request = $_GET; # Confirming Payment. if(isset($request['tx_ref'])) { + if ($controller === null) { + echo 'Unable to initialize payment controller.'; + exit(); + } + $controller->callback( $request ); } else { diff --git a/setup.php b/setup.php index 3c4ff1d3..9614398d 100644 --- a/setup.php +++ b/setup.php @@ -2,7 +2,6 @@ use Flutterwave\Helper; use Dotenv\Dotenv; -use Flutterwave\Monitoring\SignozServiceLogger; $flutterwave_installation = 'composer'; diff --git a/src/Config/AbstractConfig.php b/src/Config/AbstractConfig.php index 247b9db2..3bb20160 100644 --- a/src/Config/AbstractConfig.php +++ b/src/Config/AbstractConfig.php @@ -15,6 +15,8 @@ use GuzzleHttp\Client; use GuzzleHttp\RequestOptions; use Flutterwave\Helper\EnvVariables; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; abstract class AbstractConfig { @@ -22,7 +24,7 @@ abstract class AbstractConfig public const SECRET_KEY = 'SECRET_KEY'; public const ENCRYPTION_KEY = 'ENCRYPTION_KEY'; public const ENV = 'ENV'; - public const DEFAULT_PREFIX = 'FW|PHP'; + public const DEFAULT_PREFIX = 'FW_PHP'; public const LOG_FILE_NAME = 'flutterwave-php.log'; public Logger $logger; protected string $secret; @@ -55,7 +57,10 @@ protected function __construct(string $secret_key, string $public_key, string $e $log = new Logger('Flutterwave/PHP'); $this->logger = $log; - $this->signoz = new SignozServiceLogger($this->http, $this->getPublicKey(), $this->getEnv(), null, EnvVariables::SDK_VERSION); + $cache = new Psr16Cache(new FilesystemAdapter('flutterwave_signoz')); + $this->signoz = new SignozServiceLogger($this->http, $this->getPublicKey(), $this->getEnv(), $cache, EnvVariables::SDK_VERSION); + // Track app initialization once per lifecycle + $this->signoz->trackAppCreated($this->getPublicKey()); } abstract public static function setUp( diff --git a/src/Controller/PaymentController.php b/src/Controller/PaymentController.php index 00209f8f..b9e861d4 100644 --- a/src/Controller/PaymentController.php +++ b/src/Controller/PaymentController.php @@ -4,10 +4,8 @@ namespace Flutterwave\Controller; -use Flutterwave\EventHandlers\ModalEventHandler; use Flutterwave\EventHandlers\EventHandlerInterface; use Flutterwave\Flutterwave; -use Flutterwave\Entities\Payload; use Flutterwave\Library\Modal; use Flutterwave\Service\Transactions; @@ -43,14 +41,14 @@ private function getRequestMethod(): string public function __call(string $name, array $args) { - if ($this->routes[$name] !== $this->$requestMethod) { + if ($this->routes[$name] !== $this->requestMethod) { // Todo: 404(); echo "Unauthorized page!"; } - call_user_method_array($name, $this, $args); + call_user_func_array([$this, $name], $args); } - private function handleSessionData( array $request ) + private function handleSessionData(array $request): void { $_SESSION['success_url'] = $request['success_url']; $_SESSION['failure_url'] = $request['failure_url']; @@ -61,10 +59,8 @@ private function handleSessionData( array $request ) public function process(array $request) { $this->handleSessionData($request); - - try { - $_SESSION['p'] = $this->client; + try { if('inline' === $this->modalType ) { echo $this->client ->eventHandler($this->handler) @@ -87,23 +83,11 @@ public function callback(array $request) $status = $request['status']; if (empty($tx_ref)) { - session_destroy(); - } - - if (!isset($_SESSION['p'])) { - echo "session expired!. please refresh you browser."; + echo 'Missing transaction reference.'; exit(); } - $payment = $_SESSION['p']; - - // $payment::setUp([ - // 'secret_key' => 'FLWSECK_TEST-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-X', - // 'public_key' => 'FLWPUBK_TEST-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-X', - // 'encryption_key' => 'FLWSECK_XXXXXXXXXXXXXXXX', - // 'environment' => 'staging' - // ]); - + $payment = $this->client; $payment::bootstrap(); if ('cancelled' === $status) { diff --git a/src/EventHandlers/ModalEventHandler.php b/src/EventHandlers/ModalEventHandler.php index ae5fcb88..d53dbd7e 100644 --- a/src/EventHandlers/ModalEventHandler.php +++ b/src/EventHandlers/ModalEventHandler.php @@ -4,8 +4,13 @@ namespace Flutterwave\EventHandlers; +use Flutterwave\Service\Transactions; + class ModalEventHandler implements EventHandlerInterface { + private ?string $success_url = null; + private ?string $failure_url = null; + /** * This is called when the Rave class is initialized * */ @@ -15,6 +20,26 @@ public function onInit($initializationData): void // Save the transaction to your DB. } + public function getSuccessUrl(): ?string + { + return $this->success_url; + } + + public function setSuccessUrl(?string $success_url): void + { + $this->success_url = $success_url; + } + + public function getFailureUrl(): ?string + { + return $this->failure_url; + } + + public function setFailureUrl(?string $failure_url): void + { + $this->failure_url = $failure_url; + } + /** * This is called only when a transaction is successful * */ @@ -31,26 +56,34 @@ public function onSuccessful($transactionData): void // Update the transaction to note that you have given value for the transaction. // You can also redirect to your success page from here. if ($transactionData->status === 'successful') { - $currency = $_SESSION['currency']; - $amount = $_SESSION['amount']; + $currency = $_SESSION['currency'] ?? ($transactionData->currency ?? null); + $amount = $_SESSION['amount'] ?? ($transactionData->amount ?? null); - if ($transactionData->currency === $currency && floatval($transactionData->amount) === floatval($amount)) { - header('Location: ' . $_SESSION['success_url']); - session_destroy(); - } + if ($currency !== null && $amount !== null) { + if ($transactionData->currency === $currency && floatval($transactionData->amount) === floatval($amount)) { + if (!empty($_SESSION['success_url'])) { + header('Location: ' . $_SESSION['success_url']); + } + session_destroy(); + } - if ($transactionData->currency === $currency && floatval($transactionData->amount) < floatval($amount)) { - // TODO: replace this a custom action. - echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface
"; - echo "Partial Payment Made ! replace this with your own action! "; - session_destroy(); - } + if ($transactionData->currency === $currency && floatval($transactionData->amount) < floatval($amount)) { + // TODO: replace this a custom action. + echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface
"; + echo "Partial Payment Made ! replace this with your own action! "; + session_destroy(); + } - if ($transactionData->currency !== $currency && floatval($transactionData->amount) === floatval($amount)) { - // TODO: replace this a custom action. + if ($transactionData->currency !== $currency && floatval($transactionData->amount) === floatval($amount)) { + // TODO: replace this a custom action. + echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface
"; + echo "Currency mismatch. please look into it ! replace this with your own action "; + session_destroy(); + } + } else { + // Fallback when session metadata is unavailable. echo "This Event Handler is an Implementation of " . __NAMESPACE__ . "\EventHandlerInterface
"; - echo "Currency mismatch. please look into it ! replace this with your own action "; - session_destroy(); + echo "Transaction successful."; } } else { $this->onFailure($transactionData); @@ -66,7 +99,9 @@ public function onFailure($transactionData): void // Update the db transaction record (includeing parameters that didn't exist before the transaction is completed. for audit purpose) // You can also redirect to your failure page from here. // TODO: replace this a custom action. - header('Location: ' . $_SESSION['failure_url']); + if (!empty($_SESSION['failure_url'])) { + header('Location: ' . $_SESSION['failure_url']); + } session_destroy(); } @@ -85,8 +120,12 @@ public function onRequeryError($requeryResponse): void { echo "Flutterwave: error querying the transaction."; // trigger webhook notification from Flutterwave. - $service = new Flutterwave\Service\Transaction(); - $service->resendFailedHooks($data->id); + $service = new Transactions(); + $transactionId = is_object($requeryResponse) && isset($requeryResponse->id) + ? (string) $requeryResponse->id + : (string) $requeryResponse; + + $service->resendFailedHooks($transactionId); header('Location: ' . $_SERVER['HTTP_ORIGIN']); } @@ -107,8 +146,8 @@ public function onCancel($transactionReference): void public function onTimeout($transactionReference, $data): void { // trigger webhook notification from Flutterwave. - $service = new Flutterwave\Service\Transaction(); - $service->resendFailedHooks($data->id); + $service = new Transactions(); + $service->resendFailedHooks((string) ($data->id ?? '')); header('Location: ' . $_SERVER['HOST']); } } diff --git a/src/Flutterwave.php b/src/Flutterwave.php index e3e9ef8f..b62ce7fc 100644 --- a/src/Flutterwave.php +++ b/src/Flutterwave.php @@ -28,6 +28,8 @@ class Flutterwave extends AbstractPayment use Configure; use PaymentFactory; + private SignozServiceLogger $signoz; + /** * Flutterwave Construct * @@ -41,6 +43,12 @@ public function __construct() $this->logger = self::$config->getLoggerInstance(); $this->createReferenceNumber(); $this->logger->notice('Main Class Initializes....'); + + if (!method_exists(self::$config, 'getSignoz')) { + $this->signoz = self::getSignoz(); + } else { + $this->signoz = self::$config->getSignoz(); + } } private function checkPageIsSecure() @@ -246,10 +254,9 @@ public function requeryTransaction(string $referenceNumber): object if (isset($this->handler)) { $this->handler->onRequery($this->txref); } - /** @var SignozServiceLogger $signoz */ - $signoz = self::$config->getSignoz(); - $appId = $signoz->getAppId(); - $environment = $signoz->getCurrentEnvironment(); + + $appId = $this->signoz->getAppId(); + $environment = $this->signoz->getCurrentEnvironment(); $data = [ 'id' => (int) $referenceNumber, @@ -264,15 +271,16 @@ public function requeryTransaction(string $referenceNumber): object if ($response->status === 'success') { if ($response->data && $response->data->status === 'successful') { $this->logger->notice('Requeryed a successful transaction....' . json_encode($response->data)); - $signoz->trackRequestSent($appId, $environment, 'GET', $referenceNumber, $url ); // Handle successful. if (isset($this->handler)) { + $final_tx_ref = $response->data->tx_ref; + $this->signoz->trackRequestSent($appId, $environment, 'GET', $referenceNumber, $url ); if( 'production' === $environment ) { $final_currency = $response->data->currency; $final_amount = $response->data->amount; $payment_type = $response->data->payment_type; $final_fee = $response->data->app_fee; - $signoz->trackTransaction($appId,$referenceNumber, $final_currency, (float) $final_amount, $payment_type, (float) $final_fee); + $this->signoz->trackTransaction($appId,$final_tx_ref, $final_currency, (float) $final_amount, $payment_type, (float) $final_fee); } $this->handler->onSuccessful($response->data); } @@ -292,7 +300,7 @@ public function requeryTransaction(string $referenceNumber): object if ($this->requeryCount > 4) { // Now you have to setup a queue by force. We couldn't get a status in 5 requeries. if (isset($this->handler)) { - $signoz->trackError($appId, 'TIMEOUT_ERROR', 'timedout while requerying transaction with id: ' . $referenceNumber); + $this->signoz->trackError($appId, 'TIMEOUT_ERROR', 'timedout while requerying transaction with id: ' . $referenceNumber); $this->handler->onTimeout($this->txref, $response->data); } } else { @@ -304,7 +312,7 @@ public function requeryTransaction(string $referenceNumber): object } } else { // Handle Requery Error. - $signoz->trackError($appId, 'REQUERY_ERROR', 'Failed to requery transaction with id: ' . $referenceNumber); + $this->signoz->trackError($appId, 'REQUERY_ERROR', 'Failed to requery transaction with id: ' . $referenceNumber); if (isset($this->handler)) { $this->handler->onRequeryError($response->data); } @@ -319,12 +327,10 @@ public function initialize(): void { $this->createCheckSum(); - /** @var SignozServiceLogger $signoz */ - $signoz = self::$config->getSignoz(); - $appId = $signoz->getAppId(); - $environment = $signoz->getCurrentEnvironment(); + $appId = $this->signoz->getAppId(); + $environment = $this->signoz->getCurrentEnvironment(); - $signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline'); + $this->signoz->trackRequestSent($appId, $environment, 'GET', $this->txref, '/inline'); $this->logger->info('Rendering Payment Modal..'); diff --git a/src/Helper/Config.php b/src/Helper/Config.php index 54b11418..ecd62762 100644 --- a/src/Helper/Config.php +++ b/src/Helper/Config.php @@ -24,7 +24,7 @@ class Config implements ConfigInterface public const SECRET_KEY = 'SECRET_KEY'; public const ENCRYPTION_KEY = 'ENCRYPTION_KEY'; public const ENV = 'ENV'; - public const DEFAULT_PREFIX = 'FW|PHP'; + public const DEFAULT_PREFIX = 'FW_PHP'; public const LOG_FILE_NAME = 'flutterwave-php.log'; protected Logger $logger; private string $secret; diff --git a/src/Monitoring/SignozServiceLogger.php b/src/Monitoring/SignozServiceLogger.php index 164bcff9..0ae78df3 100644 --- a/src/Monitoring/SignozServiceLogger.php +++ b/src/Monitoring/SignozServiceLogger.php @@ -2,7 +2,9 @@ namespace Flutterwave\Monitoring; +use Flutterwave\Helper\EnvVariables; use GuzzleHttp\ClientInterface; +use GuzzleHttp\Exception\RequestException; use Psr\SimpleCache\CacheInterface; class SignozServiceLogger @@ -18,7 +20,7 @@ class SignozServiceLogger private ?CacheInterface $cache; private string $libraryVersion; - private string $appId; + private ?string $appId = null; private string $publicKey; @@ -45,10 +47,10 @@ public function getAppId() { $merchantId = $this->getMerchantId($this->publicKey); if (!empty($merchantId)) { - $this->appId = $merchantId; + $this->appId = $this->normalizeAppId($merchantId); return $this->appId; } - return $this->publicKey; + return $this->normalizeAppId($this->publicKey); } public function getCurrentEnvironment(): string @@ -58,7 +60,7 @@ public function getCurrentEnvironment(): string public function getMerchantId(string $publicKey) { try { - $response = $this->httpClient->request('GET', self::MERCHANT_INFO . $this->publicKey, [ + $response = $this->httpClient->request('GET', self::MERCHANT_INFO . $publicKey, [ 'headers' => [ 'Content-Type' => 'application/json' ] @@ -76,18 +78,48 @@ public function getMerchantId(string $publicKey) { } public function trackAppCreated( - string $publicKey, - string $merchantId + string $publicKey ): void { + $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', $publicKey)); + if (self::$appCreatedSent) { return; } + + if ($this->cache !== null) { + try { + if ($this->cache->has($cacheKey)) { + self::$appCreatedSent = true; + return; + } + } catch (\Throwable $e) { + // observability must never break payments + } + } + + $merchantId = $this->getMerchantId($publicKey); + + if (empty($merchantId)) { + return; + } + $this->send('app.created', [ - 'app_id' => $merchantId, + 'app_id' => $this->normalizeAppId($merchantId), + 'client_id' => null, 'public_key' => $publicKey, 'library' => self::LIBRARY, 'library_version' => $this->libraryVersion, ]); + + if ($this->cache !== null) { + try { + $this->cache->set($cacheKey, true); + } catch (\Throwable $e) { + // observability must never break payments + } + } + + self::$appCreatedSent = true; } public function trackRequestSent( @@ -97,19 +129,23 @@ public function trackRequestSent( string $reference, string $path ): void { + $safeReference = $this->normalizeReference($reference); + $payload = [ - 'app_id' => $appId, + 'app_id' => $this->normalizeAppId($appId), 'environment' => $environment, - 'api_version' => 'v3', + 'api_version' => EnvVariables::VERSION, 'library_version' => $this->libraryVersion, 'method' => $method, 'path' => $path, - 'reference' => $reference, + 'reference' => $safeReference, ]; + // error_log('Signoz Request Sent reference: ' . $reference); + $cacheKey = sprintf( 'signoz:request_sent:%s', - $reference + $safeReference ); if ($this->cache !== null) { @@ -124,7 +160,7 @@ public function trackRequestSent( // observability must never break payments } } - + $this->send('request.sent', $payload); } @@ -137,7 +173,7 @@ public function trackTransaction( float $fee ): void { $this->send('app.transaction', [ - 'app_id' => $appId, + 'app_id' => $this->normalizeAppId($appId), 'reference' => $reference, 'currency' => $currency, 'amount' => $amount, @@ -152,7 +188,7 @@ public function trackError( string $errorMessage ): void { $this->send('app.error', [ - 'app_id' => $appId, + 'app_id' => $this->normalizeAppId($appId), 'library' => self::LIBRARY, 'library_version' => $this->libraryVersion, 'error_code' => $errorCode, @@ -178,8 +214,35 @@ private function send(string $eventName, array $data): void 'timeout' => 2, 'connect_timeout' => 1, ]); + } catch (RequestException $e) { + $response = $e->getResponse(); + + // if ($response !== null && $response->getStatusCode() === 422) { + // $responseBody = (string) $response->getBody(); + // error_log(sprintf( + // 'Signoz validation error (422) while sending %s: %s', + // $eventName, + // $responseBody + // )); + // } } catch (\Throwable $e) { // observability must never break payments } } + + private function normalizeAppId(string $appId): string + { + return preg_replace('/\s+/', '-', trim($appId)) ?? $appId; + } + + private function normalizeReference(string $reference): string + { + $normalized = preg_replace('/[^A-Za-z0-9_-]+/', '-', trim($reference)); + + if ($normalized === null) { + return $reference; + } + + return trim($normalized, '-'); + } } \ No newline at end of file diff --git a/src/Service/Service.php b/src/Service/Service.php index 0ad3760a..8a3797c8 100644 --- a/src/Service/Service.php +++ b/src/Service/Service.php @@ -12,6 +12,7 @@ use Flutterwave\Factories\PayloadFactory as Payload; use Flutterwave\Helper\Config; use Flutterwave\Helper\EnvVariables; +use Flutterwave\Monitoring\SignozServiceLogger; use Psr\Http\Client\ClientInterface; use InvalidArgumentException; use Psr\Http\Client\ClientExceptionInterface; @@ -27,6 +28,7 @@ class Service implements ServiceInterface public ?FactoryInterface $customer; protected string $baseUrl; protected LoggerInterface $logger; + protected SignozServiceLogger $signoz; protected ConfigInterface $config; protected string $url; protected string $secret; @@ -42,6 +44,7 @@ public function __construct(?ConfigInterface $config = null) $this->config = is_null($config) ? self::$spareConfig : $config; $this->http = $this->config->getHttp(); $this->logger = $this->config->getLoggerInstance(); + $this->signoz = $this->config->getSignoz(); $this->secret = $this->config->getSecretKey(); $this->url = EnvVariables::BASE_URL . '/'; $this->baseUrl = EnvVariables::BASE_URL; @@ -68,57 +71,70 @@ public function request( $secret = $this->config->getSecretKey(); $url = $this->getUrl($overrideUrl, $additionalurl); + $reference = $this->resolveSignozReference($data, $additionalurl, $verb); switch ($verb) { - case 'POST': - $response = $this->http->request( - 'POST', $url, [ - 'debug' => false, // TODO: turn to false on release. - 'headers' => [ - 'Authorization' => "Bearer $secret", - 'Content-Type' => 'application/json', - ], - 'json' => $data, + case 'POST': + $response = $this->http->request( + 'POST', + $url, + [ + 'debug' => false, // TODO: turn to false on release. + 'headers' => [ + 'Authorization' => "Bearer $secret", + 'Content-Type' => 'application/json', + ], + 'json' => $data, ] - ); - break; - case 'PUT': - $response = $this->http->request( - 'PUT', $url, [ - 'debug' => false, // TODO: turn to false on release. - 'headers' => [ - 'Authorization' => "Bearer $secret", - 'Content-Type' => 'application/json', - ], - 'json' => $data ?? [], + ); + break; + case 'PUT': + $response = $this->http->request( + 'PUT', + $url, + [ + 'debug' => false, // TODO: turn to false on release. + 'headers' => [ + 'Authorization' => "Bearer $secret", + 'Content-Type' => 'application/json', + ], + 'json' => $data ?? [], ] - ); - break; - case 'DELETE': - $response = $this->http->request( - 'DELETE', $url, [ - 'debug' => false, - 'headers' => [ - 'Authorization' => "Bearer $secret", - 'Content-Type' => 'application/json', - ], + ); + break; + case 'DELETE': + $response = $this->http->request( + 'DELETE', + $url, + [ + 'debug' => false, + 'headers' => [ + 'Authorization' => "Bearer $secret", + 'Content-Type' => 'application/json', + ], ] - ); - break; - default: - $response = $this->http->request( - 'GET', $url, [ - 'debug' => false, - 'headers' => [ - 'Authorization' => "Bearer $secret", - 'Content-Type' => 'application/json', - ], + ); + break; + default: + $response = $this->http->request( + 'GET', + $url, + [ + 'debug' => false, + 'headers' => [ + 'Authorization' => "Bearer $secret", + 'Content-Type' => 'application/json', + ], ] - ); - break; + ); + break; } $body = $response->getBody()->getContents(); + $appId = $this->signoz->getAppId(); + $environment = $this->signoz->getCurrentEnvironment(); + $this->signoz->trackRequestSent($appId, $environment, $verb, $reference, $additionalurl); + return json_decode($body); } @@ -127,7 +143,7 @@ protected function checkTransactionId($transactionId): void $pattern = '/([0-9]){7}/'; $is_valid = preg_match_all($pattern, $transactionId); - if (! $is_valid) { + if (!$is_valid) { $this->logger->warning('Transaction Service::cannot verify invalid transaction id. '); throw new InvalidArgumentException('cannot verify invalid transaction id.'); } @@ -166,4 +182,31 @@ private function getUrl(bool $overrideUrl, string $additionalurl): string return $this->url . $additionalurl; } + + private function resolveSignozReference(?array $data, string $additionalurl): string + { + if (!is_null($data) && isset($data['tx_ref'])) { + return (string) $data['tx_ref']; + } + + foreach (['reference', 'order_ref', 'batch_id', 'id'] as $key) { + if (!empty($data[$key])) { + return (string) $data[$key]; + } + } + + $segments = array_values(array_filter(explode('/', trim($additionalurl, '/')))); + + $segmentCount = count($segments); + + if ($segmentCount === 4) { + return $segments[2]; + } + + if ($segmentCount === 3) { + return $segments[1]; + } + + return implode('-', $segments); + } } diff --git a/src/Traits/Setup/Configure.php b/src/Traits/Setup/Configure.php index dcc7cf11..eb4df8dc 100644 --- a/src/Traits/Setup/Configure.php +++ b/src/Traits/Setup/Configure.php @@ -8,6 +8,10 @@ use Flutterwave\Helper\Config; use Flutterwave\Config\PackageConfig; use Flutterwave\Config\ForkConfig; +use Flutterwave\Helper\EnvVariables; +use Flutterwave\Monitoring\SignozServiceLogger; +use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Cache\Psr16Cache; trait Configure { @@ -41,4 +45,14 @@ public static function bootstrap(?ConfigInterface $config = null): void self::$methods = include __DIR__ . '/../../Util/methods.php'; } + + public static function getSignoz(): SignozServiceLogger + { + $https = self::$config->getHttp(); + $env = self::$config->getEnv(); + $publicKey = self::$config->getPublicKey(); + $cache = new Psr16Cache(new FilesystemAdapter('flutterwave_signoz')); + + return new SignozServiceLogger($https, $publicKey, $env, $cache, EnvVariables::SDK_VERSION); + } } diff --git a/tests/Resources/Setup/Config.php b/tests/Resources/Setup/Config.php index eea6a86a..f628ca0d 100644 --- a/tests/Resources/Setup/Config.php +++ b/tests/Resources/Setup/Config.php @@ -20,7 +20,7 @@ class Config implements ConfigInterface public const SECRET_KEY = 'SECRET_KEY'; public const ENCRYPTION_KEY = 'ENCRYPTION_KEY'; public const ENV = 'ENV'; - public const DEFAULT_PREFIX = 'FW|PHP'; + public const DEFAULT_PREFIX = 'FW_PHP'; public const LOG_FILE_NAME = 'flutterwave-php.log'; protected Logger $logger; private string $secret; diff --git a/tests/Unit/Monitoring/SignozServiceLoggerTest.php b/tests/Unit/Monitoring/SignozServiceLoggerTest.php new file mode 100644 index 00000000..99291c50 --- /dev/null +++ b/tests/Unit/Monitoring/SignozServiceLoggerTest.php @@ -0,0 +1,97 @@ +resetAppCreatedFlag(); + } + + // public function testAppCreatedIsSentOnlyOncePerPublicKey(): void + // { + // $publicKey = getEnv('PUBLIC_KEY'); + // $firstHttpClient = $this->createMock(ClientInterface::class); + // $secondHttpClient = $this->createMock(ClientInterface::class); + // $cache = $this->createMock(CacheInterface::class); + + // $cacheKey = sprintf('signoz:app_created:%s', hash('sha256', $publicKey)); + + // $cache->expects($this->exactly(2)) + // ->method('has') + // ->with($cacheKey) + // ->willReturnOnConsecutiveCalls(false, true); + + // $cache->expects($this->once()) + // ->method('set') + // ->with($cacheKey, true); + + // $firstHttpClient->expects($this->exactly(2)) + // ->method('request') + // ->withConsecutive( + // [ + // 'GET', + // 'https://api.ravepay.co/flwv3-pug/getpaidx/api/mercinfo?PBFPubKey=' . $publicKey, + // $this->callback(static function (array $options): bool { + // return isset($options['headers']['Content-Type']) && $options['headers']['Content-Type'] === 'application/json'; + // }), + // ], + // [ + // 'POST', + // 'https://signozservice-prod.f4b-flutterwave.com/events', + // $this->callback(static function (array $options): bool { + // if (!isset($options['json']['name'], $options['json']['data']['public_key'])) { + // return false; + // } + + // return $options['json']['name'] === 'app.created' + // && $options['json']['data']['public_key'] === $publicKey; + // }), + // ] + // ) + // ->willReturnOnConsecutiveCalls( + // new Response(200, [], json_encode(['mn' => 'Bajoski Software Developement'])), + // new Response(200) + // ); + + // $logger = new SignozServiceLogger($firstHttpClient, $publicKey, 'sandbox', $cache, '1.0.7'); + // $logger->trackAppCreated($publicKey); + + // $this->resetAppCreatedFlag(); + + // $secondHttpClient->expects($this->never()) + // ->method('request') + // ->with($this->anything(), $this->anything(), $this->anything()); + + // $cache->expects($this->once()) + // ->method('has') + // ->with($cacheKey) + // ->willReturn(true); + + // $cache->expects($this->never()) + // ->method('set'); + + // $secondLogger = new SignozServiceLogger($secondHttpClient, $publicKey, 'sandbox', $cache, '1.0.7'); + // $secondLogger->trackAppCreated($publicKey); + // } + + private function resetAppCreatedFlag(): void + { + $reflection = new ReflectionClass(SignozServiceLogger::class); + $property = $reflection->getProperty('appCreatedSent'); + $property->setAccessible(true); + $property->setValue(false); + } +}