Skip to content

Commit

Permalink
Bug fixes for v3 api release (#286)
Browse files Browse the repository at this point in the history
* Bug fixes for v3 api release

* updates changelog

* runs pre-commit on all files

* fixes bug directly adding profiles to list

* version bump
  • Loading branch information
cykolln authored Jan 31, 2024
1 parent 8b3ed62 commit 7eb2261
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 47 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- BEGIN RELEASE NOTES -->
### [Unreleased]

### [4.1.2] - 2024-01-31

#### Fixed
- Updated customer.js to correctly handle promise returned from isIdentified
- Updated KlaviyoV3Api to handle new response patterns returned from V3 APIs
- Fixed issue with Added to Cart events not syncing for multi-site configurations
- Fixed Added to Cart observer to check for private key instead of public key
- Fixed error handling from V3 APIs, logs out the error message instead of the stack trace on retries.

### [4.1.1] - 2023-12-12

Expand Down Expand Up @@ -266,7 +272,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- END RELEASE NOTES -->
<!-- BEGIN LINKS -->
[Unreleased]: https://github.com/klaviyo/magento2-klaviyo/compare/4.1.1...HEAD
[Unreleased]: https://github.com/klaviyo/magento2-klaviyo/compare/4.1.2...HEAD
[4.1.2]: https://github.com/klaviyo/magento2-klaviyo/compare/4.1.1...4.1.2
[4.1.1]: https://github.com/klaviyo/magento2-klaviyo/compare/4.1.0...4.1.1
[4.1.0]: https://github.com/klaviyo/magento2-klaviyo/compare/4.0.12...4.1.0
[4.0.12]: https://github.com/klaviyo/magento2-klaviyo/compare/4.0.11...4.0.12
Expand Down
33 changes: 18 additions & 15 deletions Cron/KlSyncs.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,13 @@ private function sendUpdatesToApp(array $groupedRows, bool $isRetry = false)
$webhookTopics = ['product/save']; //List of topics that use webhooks
$trackApiTopics = ['Added To Cart']; //List of topics that use the Track API

$responseManifest = ['1' => [], '0' => []];
$responseManifest = ['success' => [], 'failure' => []];
$failedRows = [];
$syncedRows = [];

foreach ($groupedRows as $topic => $rows) {
if (in_array($topic, $webhookTopics) && !empty($rows)) {
// This block is not currently used
foreach ($rows as $row) {
$response = $this->_webhookHelper->makeWebhookRequest(
$row['topic'],
Expand All @@ -132,7 +135,7 @@ private function sendUpdatesToApp(array $groupedRows, bool $isRetry = false)
if (is_null($decodedPayload)) {
// payload was likely truncated, default to failed response value for the row
$this->_klaviyoLogger->log(sprintf("[sendUpdatesToApp] Truncated Payload - Unable to process and sync row %d", $row['id']));
array_push($responseManifest["0"], $row['id']);
$failedRows[] = $row['id'];
continue;
}

Expand All @@ -146,44 +149,44 @@ private function sendUpdatesToApp(array $groupedRows, bool $isRetry = false)
unset($decodedPayload['StoreId']);
}

// Call the Track API, which returns 0/1 for failed/successful requests
$response = $this->_dataHelper->klaviyoTrackEvent(
$row['topic'],
json_decode($row['user_properties'], true),
$decodedPayload,
$eventTime,
$storeId
);
if (!$response) {
$response = '0';
}

// Add the response value to the manifest, all statuses will be updated after all rows have been processed.
array_push($responseManifest["$response"], $row['id']);
if (isset($response['errors'])) {
$failedRows[] = $row['id'];
} else {
$syncedRows[] = $row['id'];
}
} catch (\Exception $e) {
// Catch an exception raised while processing or sending the event
// defaults to a failed response and allows the other rows to continue syncing
$this->_klaviyoLogger->log(sprintf("[sendUpdatesToApp] Unable to process and sync row %d: %s", $row['id'], $e));
array_push($responseManifest["0"], $row['id']);
$this->_klaviyoLogger->log(sprintf("[sendUpdatesToApp] Unable to process and sync row %d: %s", $row['id'], $e->getMessage()));
$failedRows[] = $row['id'];
continue;
}
}
}
}
$this->updateRowStatuses($responseManifest, $isRetry);
$this->updateRowStatuses($syncedRows, $failedRows, $isRetry);
}

/**
* Update statues of rows to SYNCED, RETRY and FAILED based on response and if Retry cron run
* @param array $responseManifest
* @param array $syncedRows
* @param array $failedRows
* @param bool $isRetry
*/
private function updateRowStatuses(array $responseManifest, bool $isRetry)
private function updateRowStatuses(array $syncedRows, array $failedRows, bool $isRetry)
{
$syncCollection = $this->_syncCollectionFactory->create();
$syncCollection->updateRowStatus($responseManifest['1'], 'SYNCED');
$syncCollection->updateRowStatus($syncedRows, 'SYNCED');

$syncCollection->updateRowStatus($responseManifest['0'], $isRetry ? 'FAILED' : 'RETRY');
$syncCollection->updateRowStatus($failedRows, $isRetry ? 'FAILED' : 'RETRY');
}

/**
Expand Down
31 changes: 18 additions & 13 deletions Helper/Data.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ public function __construct(
$this->_klaviyoLogger = $klaviyoLogger;
$this->_klaviyoScopeSetting = $klaviyoScopeSetting;
$this->observerAtcPayload = null;
$this->api = new KlaviyoV3Api($this->_klaviyoScopeSetting->getPublicApiKey(), $this->_klaviyoScopeSetting->getPrivateApiKey(), $klaviyoScopeSetting);
}

public function getObserverAtcPayload()
Expand All @@ -65,7 +64,8 @@ public function unsetObserverAtcPayload()
public function getKlaviyoLists()
{
try {
$lists_response = $this->api->getLists();
$api = new KlaviyoV3Api($this->_klaviyoScopeSetting->getPublicApiKey(), $this->_klaviyoScopeSetting->getPrivateApiKey(), $this->_klaviyoScopeSetting, $this->_klaviyoLogger);
$lists_response = $api->getLists();
$lists = array();

foreach ($lists_response as $list) {
Expand Down Expand Up @@ -109,6 +109,8 @@ public function subscribeEmailToKlaviyoList($email, $firstName = null, $lastName
$properties['last_name'] = $lastName;
}

$api = new KlaviyoV3Api($this->_klaviyoScopeSetting->getPublicApiKey(), $this->_klaviyoScopeSetting->getPrivateApiKey(), $this->_klaviyoScopeSetting, $this->_klaviyoLogger);

try {
if ($optInSetting == ScopeSetting::API_SUBSCRIBE) {
// Subscribe profile using the profile creation endpoint for lists
Expand All @@ -124,18 +126,18 @@ public function subscribeEmailToKlaviyoList($email, $firstName = null, $lastName
)
);

$response = $this->api->subscribeMembersToList($listId, array($consent_profile_object));
$response = $api->subscribeMembersToList($listId, array($consent_profile_object));
} else {
// Search for profile by email using the api/profiles endpoint
$response = $this->api->searchProfileByEmail($email);
$profile_id = $response["profile_id"];
// If the profile exists, use the ID to add to a list
// If the profile does not exist, create
if ($profile_id) {
$this->api->addProfileToList($listId, $profile_id);
$existing_profile = $api->searchProfileByEmail($email);
if (!$existing_profile) {
// If the profile exists, use the ID to add to a list
// If the profile does not exist, create
$new_profile = $api->createProfile($properties);
$api->addProfileToList($listId, $new_profile["profile_id"]);
} else {
$new_profile = $this->api->createProfile($properties);
$this->api->addProfileToList($listId, $new_profile["profile_id"]);
$profile_id = $existing_profile["profile_id"];
$api->addProfileToList($listId, $profile_id);
}
}
} catch (\Exception $e) {
Expand All @@ -152,9 +154,10 @@ public function subscribeEmailToKlaviyoList($email, $firstName = null, $lastName
*/
public function unsubscribeEmailFromKlaviyoList($email)
{
$api = new KlaviyoV3Api($this->_klaviyoScopeSetting->getPublicApiKey(), $this->_klaviyoScopeSetting->getPrivateApiKey(), $this->_klaviyoScopeSetting, $this->_klaviyoLogger);
$listId = $this->_klaviyoScopeSetting->getNewsletter();
try {
$response = $this->api->unsubscribeEmailFromKlaviyoList($email, $listId);
$response = $api->unsubscribeEmailFromKlaviyoList($email, $listId);
} catch (\Exception $e) {
$this->_klaviyoLogger->log(sprintf('Unable to unsubscribe %s from list %s: %s', $email, $listId, $e));
$response = false;
Expand All @@ -181,6 +184,8 @@ public function klaviyoTrackEvent($event, $customer_properties = [], $properties
if (!is_null($timestamp)) {
$params['time'] = $timestamp;
}
return $this->api->track($params);

$api = new KlaviyoV3Api($this->_klaviyoScopeSetting->getPublicApiKey($storeId), $this->_klaviyoScopeSetting->getPrivateApiKey($storeId), $this->_klaviyoScopeSetting, $this->_klaviyoLogger);
return $api->track($params);
}
}
31 changes: 19 additions & 12 deletions KlaviyoV3Sdk/KlaviyoV3Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class KlaviyoV3Api
/**
* Error messages
*/
const ERROR_INVALID_API_KEY = 'The Private Klaviyo API Key you have set is invalid.';
const ERROR_FORBIDDEN = 'Private API key has invalid permissions.';
const ERROR_NOT_AUTHORIZED = 'Authentication key missing from request or is invalid. Check that Klaviyo Private API key is correctly set for scope.';
const ERROR_NON_200_STATUS = 'Request Failed with HTTP Status Code: %s';
const ERROR_API_CALL_FAILED = 'Request could be completed at this time, API call failed';
const ERROR_MALFORMED_RESPONSE_BODY = 'Response from API could not be decoded from JSON, check response body';
Expand Down Expand Up @@ -190,8 +191,10 @@ public function addProfileToList($list_id, $profile_id)
{
$body = array(
self::DATA_KEY_PAYLOAD => array(
self::TYPE_KEY_PAYLOAD => self::PROFILE_KEY_PAYLOAD,
self::ID_KEY_PAYLOAD => $profile_id
array(
self::TYPE_KEY_PAYLOAD => self::PROFILE_KEY_PAYLOAD,
self::ID_KEY_PAYLOAD => $profile_id
)
)
);

Expand All @@ -209,14 +212,16 @@ public function addProfileToList($list_id, $profile_id)
public function createProfile($profile_properties)
{
$body = array(
self::DATA_KEY_PAYLOAD => array(
self::TYPE_KEY_PAYLOAD => self::PROFILE_KEY_PAYLOAD,
self::ATTRIBUTE_KEY_PAYLOAD => $profile_properties
)
self::DATA_KEY_PAYLOAD =>
array(
self::TYPE_KEY_PAYLOAD => self::PROFILE_KEY_PAYLOAD,
self::ATTRIBUTE_KEY_PAYLOAD => $profile_properties
)

);

$response_body = $this->requestV3('api/profiles/', self::HTTP_POST, $body);
$id = $response_body[self::DATA_KEY_PAYLOAD][0][self::ID_KEY_PAYLOAD];
$id = $response_body[self::DATA_KEY_PAYLOAD][self::ID_KEY_PAYLOAD];
return [
'data' => $response_body,
'profile_id' => $id
Expand Down Expand Up @@ -364,7 +369,7 @@ protected function requestV3($path, $method = null, $body = null, $attempt = 0)
sleep(1);
$this->requestV3($path, $method, $body, $attempt + 1);
} else {
throw new KlaviyoApiException(self::ERROR_API_CALL_FAILED);
$this->handleAPIResponse($response, $statusCode);
}
}
curl_close($curl);
Expand Down Expand Up @@ -463,14 +468,16 @@ protected function getDefaultCurlOptions($method)
protected function handleAPIResponse($response, $statusCode)
{
$decoded_response = $this->decodeJsonResponse($response);
if ($statusCode == 403) {
throw new KlaviyoAuthenticationException(self::ERROR_INVALID_API_KEY, $statusCode);
if ($statusCode == 401) {
throw new KlaviyoAuthenticationException(self::ERROR_NOT_AUTHORIZED, $statusCode);
} elseif ($statusCode == 403) {
throw new KlaviyoAuthenticationException(self::ERROR_FORBIDDEN, $statusCode);
} elseif ($statusCode == 429) {
throw new KlaviyoRateLimitException(
self::ERROR_RATE_LIMIT_EXCEEDED
);
} elseif ($statusCode < 200 || $statusCode >= 300) {
throw new KlaviyoApiException(isset($decoded_response['detail']) ? $decoded_response['detail'] : sprintf(self::ERROR_NON_200_STATUS, $statusCode), $statusCode);
throw new KlaviyoApiException(isset($decoded_response['errors']) ? $decoded_response['errors'][0]['detail'] : sprintf(self::ERROR_NON_200_STATUS, $statusCode), $statusCode);
}

return $decoded_response;
Expand Down
5 changes: 2 additions & 3 deletions Observer/SalesQuoteSaveAfter.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,9 @@ public function execute(Observer $observer)
$kl_decoded_cookie = json_decode(base64_decode($_COOKIE['__kla_id']), true);

// Get the custom variable set in the DataHelper object via the SalesQuoteProductAddAfter observer.
// Check if the public key and Added to Cart payload are set
$public_key = $this->_scopeSetting->getPublicApiKey();
// Check if the private key and Added to Cart payload are set
$klAddedToCartPayload = $this->_dataHelper->getObserverAtcPayload();
if (!isset($klAddedToCartPayload) or !isset($public_key)) {
if (!isset($klAddedToCartPayload) or empty($this->_scopeSetting->getPrivateApiKey())) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion composer.dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "klaviyo/magento2-extension-dev",
"description": "The local development composer file. This is used for local and continuous integration setup/testing.",
"type": "magento2-module",
"version": "4.1.1",
"version": "4.1.2",
"autoload": {
"psr-4": {
"Klaviyo\\Reclaim\\": ""
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "klaviyo/magento2-extension",
"description": "Klaviyo extension for Magento 2. Allows pushing newsletters to Klaviyo's platform and more.",
"type": "magento2-module",
"version": "4.1.1",
"version": "4.1.2",
"autoload": {
"files": [
"registration.php"
Expand Down
2 changes: 1 addition & 1 deletion etc/module.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Klaviyo_Reclaim" setup_version="4.1.1" schema_version="4.1.1">
<module name="Klaviyo_Reclaim" setup_version="4.1.2" schema_version="4.1.2">
<sequence>
<module name="Magento_Customer"/>
<module name="Magento_Checkout"/>
Expand Down

0 comments on commit 7eb2261

Please sign in to comment.