diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5a659e9d..8231e8e2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,11 +16,11 @@ include:
file: 'github-release.yml'
variables:
- MAGENTO_PHP_VERSION: '8.1'
+ MAGENTO_PHP_VERSION: '8.2'
magento-module/test/functional:
variables:
- MIN_PHP_VERSION: '8.1'
+ MIN_PHP_VERSION: '8.2'
DISABLE_DEPENDENCY_AUDIT: "true"
release:
diff --git a/README.md b/README.md
index d80abaa2..b10fc1a1 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,9 @@ Below is a rundown of all configuration options
1) Authentication key: This is used to communicate with tweakwise and determines your navigator instance, it should be the same as the key found in the navigator under `Connectivity > End points`.
2) Send analytics events to Tweakwise: Enable server-side analytics event sending (product views, searches, purchases). Only enable this if you are not using a JavaScript tracking script to send these events to Tweakwise; enabling both will result in duplicate events.
3) Cookie name: Name of the cookie that holds the Tweakwise profile id. This is usually set by the Tweakwise measure script. When analytics is enabled the cookie is managed automatically and this field can be left empty.
-4) Grouped products: Enable this after the grouped export has been enabled and imported in tweakwise. This wil enable filtering based on variant data and enabled the product image to be controlled by tweakwise so that the correct color is shown based on selected filters. This requires the image url in tweakwise to be configured correctly. If this is not enabled and you've exported grouped products the catalog may be empty.
-5) Language: The language used by the store, passed to Tweakwise to determine word conjugations and spelling corrections in search results.
+4) Internal traffic IP addresses: Comma-separated list of IP addresses that should be tagged as internal traffic. When a visitor's IP matches one of the configured addresses, all Tweakwise API requests triggered by that visit will include a `TWN-Source: Internal-Traffic` header. This allows you to filter out internal traffic (e.g. employees, testers) from Tweakwise Analytics reports. See [Tweakwise docs](https://docs.tweakwise.com/reference/identify-requests-as-internal-traffic) for details.
+5) Grouped products: Enable this after the grouped export has been enabled and imported in tweakwise. This wil enable filtering based on variant data and enabled the product image to be controlled by tweakwise so that the correct color is shown based on selected filters. This requires the image url in tweakwise to be configured correctly. If this is not enabled and you've exported grouped products the catalog may be empty.
+6) Language: The language used by the store, passed to Tweakwise to determine word conjugations and spelling corrections in search results.
#### Layered Navigation (All settings depend on Enabled having value yes):
1) Enabled: Use tweakwise results in navigation, if disabled the standard magento navigation is used. Don't disable the anchor tag on main categories, this causes al products to be shown. The anchor tag can be disabled on sub-categories.
diff --git a/src/Model/Catalog/Layer/Url/Strategy/QueryParameterStrategy.php b/src/Model/Catalog/Layer/Url/Strategy/QueryParameterStrategy.php
index ceb0451f..c3ce97d2 100644
--- a/src/Model/Catalog/Layer/Url/Strategy/QueryParameterStrategy.php
+++ b/src/Model/Catalog/Layer/Url/Strategy/QueryParameterStrategy.php
@@ -355,8 +355,7 @@ public function getAttributeSelectUrl(MagentoHttpRequest $request, Item $item):
// @phpstan-ignore-next-line
$values[] = $value;
}
-
- // @phpstan-ignore-next-line
+
foreach ($values as $key => $value) {
$queryUrl = str_replace('__VALUE.' . $key . '__', $value, $queryUrl);
}
diff --git a/src/Model/Client.php b/src/Model/Client.php
index 4f5bc823..86f10f0c 100644
--- a/src/Model/Client.php
+++ b/src/Model/Client.php
@@ -26,6 +26,7 @@
use SimpleXMLElement;
use GuzzleHttp\Client as HttpClient;
use Magento\Framework\UrlInterface;
+use Magento\Framework\HTTP\PhpEnvironment\RemoteAddress;
use GuzzleHttp\Psr7\Uri;
class Client
@@ -79,7 +80,11 @@ public function __construct(
ResponseFactory $responseFactory,
EndpointManager $endpointManager,
Timer $timer,
- private UrlInterface $urlBuilder
+ private UrlInterface $urlBuilder,
+ /**
+ * @var RemoteAddress
+ */
+ private readonly RemoteAddress $remoteAddress
) {
$this->config = $config;
$this->log = $log;
@@ -108,6 +113,24 @@ protected function getClient(): HttpClient
return $this->client;
}
+ /**
+ * @return array
+ */
+ protected function buildInternalTrafficHeader(): array
+ {
+ $internalIps = $this->config->getInternalIpAddresses();
+ if (empty($internalIps)) {
+ return [];
+ }
+
+ $visitorIp = $this->remoteAddress->getRemoteAddress();
+ if (!in_array($visitorIp, $internalIps, true)) {
+ return [];
+ }
+
+ return ['TWN-Source' => 'Internal-Traffic'];
+ }
+
/**
* @param Request $tweakwiseRequest
* @return HttpRequest
@@ -125,10 +148,10 @@ protected function createHttpRequest(Request $tweakwiseRequest): HttpRequest
* @param Request $tweakwiseRequest
* @return HttpRequest
*/
- protected function createPostRequest(Request $tweakwiseRequest): HttpRequest
+ public function createPostRequest(Request $tweakwiseRequest): HttpRequest
{
$path = $tweakwiseRequest->getPath();
- $headers = [];
+ $headers = $this->buildInternalTrafficHeader();
$headers['Content-Type'] = 'application/json';
$headers['Instance-Key'] = $this->config->getGeneralAuthenticationKey();
@@ -144,12 +167,12 @@ protected function createPostRequest(Request $tweakwiseRequest): HttpRequest
* @param Request $tweakwiseRequest
* @return HttpRequest
*/
- protected function createGetRequest(Request $tweakwiseRequest): HttpRequest
+ public function createGetRequest(Request $tweakwiseRequest): HttpRequest
{
$path = $tweakwiseRequest->getPath();
$pathSuffix = $tweakwiseRequest->getPathSuffix();
- $headers = [];
+ $headers = $this->buildInternalTrafficHeader();
if ($path === 'recommendations/featured') {
if ($this->config->getRecommendationsFeaturedCategory()) {
diff --git a/src/Model/Config.php b/src/Model/Config.php
index b7cfa4a6..2981b196 100644
--- a/src/Model/Config.php
+++ b/src/Model/Config.php
@@ -74,6 +74,9 @@ class Config
private const PRODUCT_CARD_LIFETIME_XML_PATH =
'tweakwise/merchandising_builder/personal_merchandising/product_card_lifetime';
+ private const INTERNAL_IP_ADDRESSES_XML_PATH =
+ 'tweakwise/general/internal_ip_addresses';
+
/**
* @var ScopeConfigInterface
*/
@@ -670,4 +673,17 @@ public function getProductCardLifetime(): int
{
return (int) $this->config->getValue(self::PRODUCT_CARD_LIFETIME_XML_PATH, ScopeInterface::SCOPE_STORE);
}
+
+ /**
+ * @return string[]
+ */
+ public function getInternalIpAddresses(): array
+ {
+ $value = $this->getStoreConfig(self::INTERNAL_IP_ADDRESSES_XML_PATH);
+ if (empty($value)) {
+ return [];
+ }
+
+ return array_filter(array_map('trim', explode(',', $value)));
+ }
}
diff --git a/src/etc/adminhtml/system.xml b/src/etc/adminhtml/system.xml
index 3c1c19ff..435792d7 100644
--- a/src/etc/adminhtml/system.xml
+++ b/src/etc/adminhtml/system.xml
@@ -37,6 +37,10 @@
Tweakwise\Magento2Tweakwise\Model\Config\Source\Language
+
+
+ Comma-separated list of IP addresses. Requests from these IPs will be tagged with TWN-Source: Internal-Traffic in Tweakwise API calls, allowing you to filter them from Tweakwise Analytics reports.
+
diff --git a/tests/Functional/InitialTest.php b/tests/Functional/InitialTest.php
deleted file mode 100644
index f71fd2fd..00000000
--- a/tests/Functional/InitialTest.php
+++ /dev/null
@@ -1,24 +0,0 @@
-tester->assertTrue(true);
- }
-}
diff --git a/tests/Unit/Model/ClientTest.php b/tests/Unit/Model/ClientTest.php
new file mode 100644
index 00000000..45a99faf
--- /dev/null
+++ b/tests/Unit/Model/ClientTest.php
@@ -0,0 +1,143 @@
+createMock(Config::class);
+ $config->method('getInternalIpAddresses')->willReturn(['127.0.0.1']);
+ $config->method('getGeneralAuthenticationKey')->willReturn('123456');
+ $config->method('getRecommendationsFeaturedCategory')->willReturn(false);
+
+ $endpointManager = $this->createMock(EndpointManager::class);
+ $endpointManager->method('getServerUrl')->willReturn('https://gateway.tweakwisenavigator.net');
+
+ $remoteAddress = $this->createMock(RemoteAddress::class);
+ $remoteAddress->method('getRemoteAddress')->willReturn('127.0.0.1');
+
+ $client = $this->createClient($config, $endpointManager, $remoteAddress);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getPath')->willReturn('navigation');
+ $request->method('getPathSuffix')->willReturn('');
+ $request->method('getParameters')->willReturn(['tn_ps' => '12']);
+
+ $httpRequest = $client->createGetRequest($request);
+
+ $this->assertSame('Internal-Traffic', $httpRequest->getHeaderLine('TWN-Source'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreateGetRequestSkipsInternalTrafficHeaderForNonMatchingIp(): void
+ {
+ $config = $this->createMock(Config::class);
+ $config->method('getInternalIpAddresses')->willReturn(['10.0.0.99']);
+ $config->method('getGeneralAuthenticationKey')->willReturn('123456');
+ $config->method('getRecommendationsFeaturedCategory')->willReturn(false);
+
+ $endpointManager = $this->createMock(EndpointManager::class);
+ $endpointManager->method('getServerUrl')->willReturn('https://gateway.tweakwisenavigator.net');
+
+ $remoteAddress = $this->createMock(RemoteAddress::class);
+ $remoteAddress->method('getRemoteAddress')->willReturn('127.0.0.1');
+
+ $client = $this->createClient($config, $endpointManager, $remoteAddress);
+
+ $request = $this->createMock(Request::class);
+ $request->method('getPath')->willReturn('navigation');
+ $request->method('getPathSuffix')->willReturn('');
+ $request->method('getParameters')->willReturn(['tn_ps' => '12']);
+
+ $httpRequest = $client->createGetRequest($request);
+
+ $this->assertSame('', $httpRequest->getHeaderLine('TWN-Source'));
+ }
+
+ /**
+ * @return void
+ */
+ public function testCreatePostRequestAddsInternalTrafficHeaderForAnalyticsRequests(): void
+ {
+ $config = $this->createMock(Config::class);
+ $config->method('getInternalIpAddresses')->willReturn(['127.0.0.1']);
+ $config->method('getGeneralAuthenticationKey')->willReturn('123456');
+
+ $urlBuilder = $this->createMock(UrlInterface::class);
+ $urlBuilder->expects($this->once())
+ ->method('getUrl')
+ ->with('https://navigator-analytics.tweakwise.com/analytics')
+ ->willReturn('https://navigator-analytics.tweakwise.com/analytics');
+
+ $remoteAddress = $this->createMock(RemoteAddress::class);
+ $remoteAddress->method('getRemoteAddress')->willReturn('127.0.0.1');
+
+ $client = new Client(
+ $config,
+ $this->createMock(Logger::class),
+ $this->createMock(ResponseFactory::class),
+ $this->createMock(EndpointManager::class),
+ $this->createMock(Timer::class),
+ $urlBuilder,
+ $remoteAddress
+ );
+
+ $request = $this->createMock(AnalyticsRequest::class);
+ $request->method('getPath')->willReturn('analytics');
+ $request->method('getApiUrl')->willReturn('https://navigator-analytics.tweakwise.com');
+ $request->method('getParameters')->willReturn(['event' => 'click']);
+
+ $httpRequest = $client->createPostRequest($request);
+
+ $this->assertSame('Internal-Traffic', $httpRequest->getHeaderLine('TWN-Source'));
+ $this->assertSame('123456', $httpRequest->getHeaderLine('Instance-Key'));
+ }
+
+ /**
+ * @param Config $config
+ * @param EndpointManager $endpointManager
+ * @param RemoteAddress $remoteAddress
+ * @return Client
+ */
+ private function createClient(
+ Config $config,
+ EndpointManager $endpointManager,
+ RemoteAddress $remoteAddress
+ ): Client {
+ return new Client(
+ $config,
+ $this->createMock(Logger::class),
+ $this->createMock(ResponseFactory::class),
+ $endpointManager,
+ $this->createMock(Timer::class),
+ $this->createMock(UrlInterface::class),
+ $remoteAddress
+ );
+ }
+}
diff --git a/tests/Unit/Model/ConfigTest.php b/tests/Unit/Model/ConfigTest.php
new file mode 100644
index 00000000..da7f712f
--- /dev/null
+++ b/tests/Unit/Model/ConfigTest.php
@@ -0,0 +1,75 @@
+createMock(ScopeConfigInterface::class);
+ $scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->with('tweakwise/general/internal_ip_addresses', 'store', null)
+ ->willReturn('');
+
+ $config = $this->createConfig($scopeConfig);
+
+ $this->assertSame([], $config->getInternalIpAddresses());
+ }
+
+ /**
+ * @return void
+ */
+ public function testGetInternalIpAddressesParsesCommaSeparatedValuesWithWhitespace(): void
+ {
+ $scopeConfig = $this->createMock(ScopeConfigInterface::class);
+ $scopeConfig->expects($this->once())
+ ->method('getValue')
+ ->with('tweakwise/general/internal_ip_addresses', 'store', null)
+ ->willReturn('127.0.0.1 , 10.0.0.1 , 192.168.1.1');
+
+ $config = $this->createConfig($scopeConfig);
+
+ $this->assertSame(['127.0.0.1', '10.0.0.1', '192.168.1.1'], $config->getInternalIpAddresses());
+ }
+
+ /**
+ * @param ScopeConfigInterface $scopeConfig
+ * @return Config
+ */
+ private function createConfig(ScopeConfigInterface $scopeConfig): Config
+ {
+ $state = $this->createMock(State::class);
+ $state->method('getAreaCode')->willReturn(Area::AREA_FRONTEND);
+
+ return new Config(
+ $scopeConfig,
+ $this->createMock(Json::class),
+ $this->createMock(RequestInterface::class),
+ $state,
+ $this->createMock(WriterInterface::class),
+ $this->createMock(TypeListInterface::class)
+ );
+ }
+}