Skip to content

Commit 7250ff5

Browse files
author
sakshamg1304
committed
feat: send Network calls synchornously if set in init
1 parent 5129662 commit 7250ff5

File tree

11 files changed

+346
-61
lines changed

11 files changed

+346
-61
lines changed

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.13.0] - 2025-10-10
9+
10+
### Added
11+
12+
- Introduced option to send network calls synchronously with the introduction of a new parameter in init, `shouldWaitForTrackingCalls`.
13+
- Added `retryConfig` init parameter to configure retries: `shouldRetry`, `maxRetries`, `initialDelay`, `backoffMultiplier`.
14+
15+
Example:
16+
17+
```php
18+
$vwoClient = VWO::init([
19+
'accountId' => '123456',
20+
'sdkKey' => '32-alpha-numeric-sdk-key',
21+
'shouldWaitForTrackingCalls' => true, // switch to synchronous (cURL) tracking
22+
'retryConfig' => [
23+
'shouldRetry' => true, // default: true
24+
'maxRetries' => 3, // default: 3
25+
'initialDelay' => 2, // seconds; default: 2
26+
'backoffMultiplier' => 2, // delays: 2s, 4s, 8s; default: 2
27+
],
28+
]);
29+
30+
// If you want synchronous calls without retries
31+
$vwoClient = VWO::init([
32+
'accountId' => '123456',
33+
'sdkKey' => '32-alpha-numeric-sdk-key',
34+
'shouldWaitForTrackingCalls' => true,
35+
'retryConfig' => [
36+
'shouldRetry' => false,
37+
],
38+
]);
39+
```
40+
841
## [1.12.0] - 2025-09-25
942

1043
### Added

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,62 @@ $vwoClient = VWO::init([
209209

210210
Refer to the [Gateway Documentation](https://developers.vwo.com/v2/docs/gateway-service) for further details.
211211

212+
### Synchronous network calls
213+
214+
Synchronous network calls differ from the default (asynchronous or fire-and-forget) tracking behavior by waiting for the tracking request to return a response from the VWO server before proceeding with the rest of your application code. By default, the SDK sends tracking network calls in a way that does not block your application, providing maximum throughput and lowest latency for user actions.
215+
216+
**Why is it so?**
217+
- The default asynchronous approach ensures minimal impact on user experience or application response times. Tracking calls are dispatched without waiting for a server response.
218+
- With synchronous calls enabled (`shouldWaitForTrackingCalls => true`), your code will block and wait for the network call to complete, allowing you to detect network errors, retry, and ensure delivery before execution continues.
219+
220+
**When should you use synchronous calls?**
221+
- Use synchronous tracking when tracking data integrity or acknowledgment is critical before user flow continues (e.g., checkout flows, conversion confirmations, or compliance events).
222+
- It is recommended when you need to guarantee that server-side events are delivered, and are willing to trade a slight increase in user-facing latency for this assurance.
223+
224+
225+
226+
You can opt-in to perform network calls synchronously by passing an init parameter.
227+
228+
```php
229+
$vwoClient = VWO::init([
230+
'sdkKey' => '32-alpha-numeric-sdk-key',
231+
'accountId' => '123456',
232+
'shouldWaitForTrackingCalls' => true,
233+
]);
234+
```
235+
236+
Notes:
237+
- In PHP, the default transport for tracking is a non-blocking socket-based call (fire-and-forget).
238+
- When you enable `shouldWaitForTrackingCalls`, the SDK switches to a blocking cURL-based call so your code waits for the response.
239+
- For synchronous (cURL) calls, retry is enabled by default. You can override via `retryConfig` in init:
240+
241+
```php
242+
$vwoClient = VWO::init([
243+
'sdkKey' => '32-alpha-numeric-sdk-key',
244+
'accountId' => '123456',
245+
'shouldWaitForTrackingCalls' => true,
246+
'retryConfig' => [
247+
'shouldRetry' => true, // default: true
248+
'maxRetries' => 3, // default: 3
249+
'initialDelay' => 2, // seconds; default: 2
250+
'backoffMultiplier' => 2, // delays: 2s, 4s, 8s; default: 2
251+
],
252+
]);
253+
```
254+
255+
If you want synchronous calls but without retries, set `shouldRetry` to `false`:
256+
257+
```php
258+
$vwoClient = VWO::init([
259+
'sdkKey' => '32-alpha-numeric-sdk-key',
260+
'accountId' => '123456',
261+
'shouldWaitForTrackingCalls' => true,
262+
'retryConfig' => [
263+
'shouldRetry' => false,
264+
],
265+
]);
266+
```
267+
212268
### Storage
213269

214270
The SDK operates in a stateless mode by default, meaning each `getFlag` call triggers a fresh evaluation of the flag against the current user context.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vwo/vwo-fme-php-sdk",
33

4-
"version": "1.12.0",
4+
"version": "1.13.0",
55
"keywords": ["vwo", "fme", "sdk"],
66
"license": "Apache-2.0",
77
"authors": [{

src/Constants/Constants.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class Constants {
4040
const DEFAULT_EVENTS_PER_REQUEST = 100;
4141
const SDK_NAME = 'vwo-fme-php-sdk';
4242

43-
const SDK_VERSION = '1.12.0';
43+
const SDK_VERSION = '1.13.0';
4444
const AP = 'server';
4545

4646
const SETTINGS = 'settings';
@@ -57,6 +57,20 @@ class Constants {
5757

5858
const PRODUCT = 'product';
5959
const FME = 'fme';
60+
61+
// Retry configuration keys
62+
const RETRY_SHOULD_RETRY = 'shouldRetry';
63+
const RETRY_MAX_RETRIES = 'maxRetries';
64+
const RETRY_INITIAL_DELAY = 'initialDelay';
65+
const RETRY_BACKOFF_MULTIPLIER = 'backoffMultiplier';
66+
67+
// Retry configuration defaults
68+
const DEFAULT_RETRY_CONFIG = [
69+
self::RETRY_SHOULD_RETRY => true,
70+
self::RETRY_MAX_RETRIES => 3,
71+
self::RETRY_INITIAL_DELAY => 2,
72+
self::RETRY_BACKOFF_MULTIPLIER => 2,
73+
];
6074
}
6175

6276
?>

src/Packages/NetworkLayer/Client/NetworkClient.php

Lines changed: 130 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,69 +20,129 @@
2020

2121
use vwo\Packages\NetworkLayer\Models\ResponseModel;
2222
use vwo\Packages\NetworkLayer\Models\RequestModel;
23+
use vwo\Services\LoggerService;
24+
use vwo\Enums\LogLevelEnum;
25+
use vwo\Constants\Constants;
2326

2427
class NetworkClient implements NetworkClientInterface
2528
{
2629
const HTTPS = 'HTTPS';
2730
const DEFAULT_TIMEOUT = 5; // seconds
2831
private $isGatewayUrlNotSecure = false; // Flag to store the value
32+
private $shouldWaitForTrackingCalls = false;
33+
private $retryConfig = Constants::DEFAULT_RETRY_CONFIG;
2934

3035
// Constructor to accept options and store the flag
3136
public function __construct($options = []) {
3237
if (isset($options['isGatewayUrlNotSecure'])) {
3338
$this->isGatewayUrlNotSecure = $options['isGatewayUrlNotSecure'];
3439
}
40+
if (isset($options['shouldWaitForTrackingCalls'])) {
41+
$this->shouldWaitForTrackingCalls = $options['shouldWaitForTrackingCalls'];
42+
}
43+
if(isset($options['retryConfig'])) {
44+
$this->retryConfig = $options['retryConfig'];
45+
}
3546
}
3647

3748
private function shouldUseCurl($networkOptions)
3849
{
39-
return $this->isGatewayUrlNotSecure;
50+
if ($this->isGatewayUrlNotSecure || $this->shouldWaitForTrackingCalls) {
51+
return true;
52+
}
53+
return false;
4054
}
4155

42-
private function makeCurlRequest($url, $method, $headers, $body = null, $timeout = 5)
56+
private function makeCurlRequest($url, $method, $headers, $body = null, $timeout = 5, $retryConfig = [])
4357
{
44-
$ch = curl_init();
58+
// Merge with defaults to ensure all keys are present
59+
$retryConfig = array_merge(Constants::DEFAULT_RETRY_CONFIG, $retryConfig);
4560

46-
curl_setopt_array($ch, [
47-
CURLOPT_URL => $url,
48-
CURLOPT_RETURNTRANSFER => true,
49-
CURLOPT_TIMEOUT => $timeout,
50-
CURLOPT_CUSTOMREQUEST => $method,
51-
CURLOPT_SSL_VERIFYPEER => false,
52-
CURLOPT_SSL_VERIFYHOST => false,
53-
]);
54-
55-
if (!isset($headers['Content-Type'])) {
56-
$headers['Content-Type'] = 'application/json';
61+
$maxRetries = $retryConfig[Constants::RETRY_MAX_RETRIES];
62+
$baseDelay = $retryConfig[Constants::RETRY_INITIAL_DELAY];
63+
$multiplier = $retryConfig[Constants::RETRY_BACKOFF_MULTIPLIER];
64+
$shouldRetry = $retryConfig[Constants::RETRY_SHOULD_RETRY];
65+
66+
$retryDelays = [];
67+
for ($i = 0; $i < $maxRetries; $i++) {
68+
$retryDelays[$i] = $baseDelay * ($i === 0 ? 1 : pow($multiplier, $i));
5769
}
70+
$lastError = null;
5871

59-
// Set headers
60-
if (!empty($headers)) {
61-
$curlHeaders = [];
62-
63-
foreach ($headers as $key => $value) {
64-
$curlHeaders[] = "$key: $value";
72+
for ($attempt = 0; $attempt < $maxRetries && $shouldRetry === true; $attempt++) {
73+
$ch = curl_init();
74+
75+
curl_setopt_array($ch, [
76+
CURLOPT_URL => $url,
77+
CURLOPT_RETURNTRANSFER => true,
78+
CURLOPT_TIMEOUT => $timeout,
79+
CURLOPT_CUSTOMREQUEST => $method,
80+
CURLOPT_SSL_VERIFYPEER => false,
81+
CURLOPT_SSL_VERIFYHOST => false,
82+
CURLOPT_FOLLOWLOCATION => true,
83+
CURLOPT_MAXREDIRS => 5,
84+
]);
85+
86+
if (!isset($headers['Content-Type'])) {
87+
$headers['Content-Type'] = 'application/json';
6588
}
66-
}
67-
curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders);
6889

69-
// Set body for POST requests
70-
if ($method === 'POST' && $body) {
71-
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
72-
}
90+
// Set headers
91+
$curlHeaders = [];
92+
if (!empty($headers)) {
93+
foreach ($headers as $key => $value) {
94+
$curlHeaders[] = "$key: $value";
95+
}
96+
}
97+
curl_setopt($ch, CURLOPT_HTTPHEADER, $curlHeaders);
7398

74-
$response = curl_exec($ch);
99+
// Set body for POST requests
100+
if ($method === 'POST' && $body) {
101+
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
102+
if (defined('CURLOPT_POSTREDIR')) {
103+
curl_setopt($ch, CURLOPT_POSTREDIR, 3); // keep POST on 301/302 redirects
104+
}
105+
}
75106

76-
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
77-
$error = curl_error($ch);
78-
79-
curl_close($ch);
107+
$response = curl_exec($ch);
108+
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
109+
$error = curl_error($ch);
110+
111+
curl_close($ch);
112+
113+
// Check if request was successful
114+
if (!$error && $httpCode >= 200 && $httpCode < 300) {
115+
return [
116+
'body' => $response,
117+
'status_code' => $httpCode
118+
];
119+
}
80120

81-
if ($error) {
82-
throw new \Exception("cURL error: $error");
121+
// Store the error for potential re-throwing
122+
$lastError = $error ? "cURL error: $error" : "HTTP error: $httpCode";
123+
124+
// If this is not the last attempt, wait before retrying
125+
if ($attempt < $maxRetries ) {
126+
$delay = $retryDelays[$attempt] ?? $baseDelay;
127+
128+
LoggerService::error('NETWORK_CALL_RETRY_ATTEMPT', [
129+
'endPoint' => $url,
130+
'err' => $lastError,
131+
'delay' => $delay,
132+
'attempt' => $attempt + 1,
133+
'maxRetries' => $maxRetries
134+
]);
135+
sleep($delay);
136+
} else {
137+
LoggerService::error('NETWORK_CALL_RETRY_FAILED', [
138+
'endPoint' => $url,
139+
'err' => $lastError
140+
]);
141+
}
83142
}
84143

85-
return $response;
144+
// If we get here, all retries failed
145+
throw new \Exception($lastError);
86146
}
87147

88148
private function createSocketConnection($url, $timeout)
@@ -205,13 +265,15 @@ public function GET($request)
205265
try {
206266
// Check if we should use cURL instead of socket
207267
if ($this->shouldUseCurl($networkOptions)) {
208-
$rawResponse = $this->makeCurlRequest(
268+
$curlResponse = $this->makeCurlRequest(
209269
$networkOptions['url'],
210270
'GET',
211271
$networkOptions['headers'],
212272
null,
213273
$networkOptions['timeout'] / 1000
214274
);
275+
$rawResponse = $curlResponse['body'];
276+
$responseModel->setStatusCode($curlResponse['status_code']);
215277
} else {
216278
// Use socket connection (existing logic)
217279
$socket = $this->createSocketConnection(
@@ -246,23 +308,37 @@ public function POST($request)
246308
$networkOptions = $request->getOptions();
247309
$responseModel = new ResponseModel();
248310

311+
// NetworkManager ensures retryConfig is set and validated in options
312+
$retryConfig = $request->getRetryConfig();
313+
249314
try {
250-
// Check if we should use cURL instead of socket
315+
// Check if we should use cURL (either for synchronous or gateway fallback)
251316
if ($this->shouldUseCurl($networkOptions)) {
252-
$rawResponse = $this->makeCurlRequest(
317+
$curlResponse = $this->makeCurlRequest(
253318
$networkOptions['url'],
254319
'POST',
255320
$networkOptions['headers'],
256321
$networkOptions['body'],
257-
$networkOptions['timeout'] / 1000
322+
$networkOptions['timeout'] / 1000,
323+
$retryConfig
258324
);
259325

260-
$parsedData = json_decode($rawResponse, false);
261-
if ($parsedData === null && json_last_error() !== JSON_ERROR_NONE) {
262-
throw new \Exception("Failed to parse JSON response: " . json_last_error_msg());
263-
}
326+
$rawResponse = $curlResponse['body'];
327+
$responseModel->setStatusCode($curlResponse['status_code']);
264328

265-
$responseModel->setData($parsedData);
329+
// If body is empty or whitespace, don't attempt JSON parse
330+
if (is_string($rawResponse) && trim($rawResponse) === '') {
331+
$responseModel->setData(null);
332+
} else {
333+
$parsedData = json_decode($rawResponse, false);
334+
if ($parsedData === null && json_last_error() !== JSON_ERROR_NONE) {
335+
// Non-JSON response; keep status code and set error, but do not throw
336+
$responseModel->setError('Non-JSON response body');
337+
$responseModel->setData(null);
338+
} else {
339+
$responseModel->setData($parsedData);
340+
}
341+
}
266342
} else {
267343
// Use socket connection (existing logic)
268344
$socket = $this->createSocketConnection(
@@ -277,12 +353,18 @@ public function POST($request)
277353
$rawResponse = $this->readResponse($socket);
278354
fclose($socket);
279355

280-
$parsedData = json_decode($rawResponse['body'], false);
281-
if ($parsedData === null && json_last_error() !== JSON_ERROR_NONE) {
282-
throw new \Exception("Failed to parse JSON response: " . json_last_error_msg());
356+
$parsedBody = $rawResponse['body'];
357+
if (is_string($parsedBody) && trim($parsedBody) === '') {
358+
$responseModel->setData(null);
359+
} else {
360+
$parsedData = json_decode($parsedBody, false);
361+
if ($parsedData === null && json_last_error() !== JSON_ERROR_NONE) {
362+
$responseModel->setError('Non-JSON response body');
363+
$responseModel->setData(null);
364+
} else {
365+
$responseModel->setData($parsedData);
366+
}
283367
}
284-
285-
$responseModel->setData($parsedData);
286368
}
287369

288370
} catch (\Exception $e) {

0 commit comments

Comments
 (0)