|
15344
|
Meet
Meet
Close tab
New Tab
Customize sidebar
Open Meet
Meet
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
rireroxcalMistorbookmarksProtllesWindowmelprTavsco.sroledey© HubspotWebhoov D Pagination© HubspotPaginat© HubspotSyncStrategyBase.phpy syneermenttes tralC Paginationcontic© ermactivilyservice.phpC) Paginationstate•_ Prospectsearchstr> D Redisv D ServiceTraits© OpportunitySync# SyncermEntities© SyncFieldsTrait./T Writecrmtrait.p>@Utils•Weonook869C) BatchSvncCollectotC) BatchSvncRedisSerl© Client.php© ClosedDealStagesS@DealFieldsService.p© DecorateActivity.ph© FieldDefinitions.php© FieldTypeConvertel© HubspotClientintert(C) HubsnotTokenMan© PayloadBuilder.php• RemoteCrmObjectn© ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivity© WebhookSyncBatclIntegrationApp› Accessors›D ADI• contio> DDTO•D Filters• HlobsProspectSearchStra• ServiceTraits895C) Dataclient.oho©DecorateActivity.pt897 €C LocalSearch.nhn© LocalSearchinterfa‹899© RemoteSearch.php© Service.phpv Ml isteners© ConvertLeadActivit©PurgeLookupCache> Metadata) M Miarationclass Client extends BaseClient implemoubuc tunction 1sunauthonizedexceplessage - surtocowertee cgecrreturn str_contains($message,str_contains($message,str_contains($message,preg_mauchl pattern:)'/\t* Vacloares und rerresnes une acc* Inas ensures cone-runnine proce* achrows soc1alaccountlokenLnvalpublic function ensureValidToken()1t sthis->oauthaccount === nuSnewToken = sthis->tokenmanaqd1+ Snewloken == nulb) <public function getConfig()return $this->config;// returns only active (archived=f9 usagespublic function getOwners(): arrayreturn Sthis->getNewInstance• aparam b00l Sarchived• dreturn arrau<owner>New Tab• Sprint Board • SRD QueueGithub Jiminnv DEV~ Google GeminiGemini© circle Cl8 PRODUS 8:• Meet+ New Tab©= meet.google.com/mie-gawc-dsi?authuser-lukas.kovalik%40jiminny.comGoogle MeetC<40ll O| Daily-Platform-now A 100%C43 &• Mon 11 May 9:45:54kovalkukasaiomail.comPROHi LukasWhere should we start?Create image* Create musicWrite anythingBoost my dayHelp me learnGetting ready...You'll be able to join in just a moment© Enter a prompt for GeminiSummarize page...
|
Firefox
|
Meet — Personal
|
meet.google.com/mie-gawc-dsi?authuser=lukas.kovali meet.google.com/mie-gawc-dsi?authuser=lukas.kovalik%40jiminny.com...
|
15344
|
|
15343
|
Meet
Meet
Close tab
New Tab
Customize sidebar
Fire Meet
Meet
Close tab
New Tab
Customize sidebar
Firefox FileEditViewHistoryBookmarksProfilesToolsWindowHelpla6|| Daily - Platform • nowA100% C47 8• Mon 11 May 9:45:54DEV (docker)• *з.181DOCKERO 81DEV (docker)882APP (-zsh)masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zsh-zsh885screenpipe"0 ₴6DEV...
|
Firefox
|
Meet — Personal
|
meet.google.com/mie-gawc-dsi?authuser=lukas.kovali meet.google.com/mie-gawc-dsi?authuser=lukas.kovalik%40jiminny.com...
|
15343
|
|
15342
|
rireroxcalMistorbookmarksProtllesWindowmelprTavsco rireroxcalMistorbookmarksProtllesWindowmelprTavsco.sroledey© HubspotWebhoov @ Pagination© HubspotPaginat© HubspotSyncStrategyBase.phpy syneermenttesIfaC Paginationcontic© ermactivilyservice.phgC) Paginationstate•_ Prospectsearchstr> D Redisv D ServiceTraits© OpportunitySync+ SyncermEntities© SyncFieldsTrait./T Writecrmtrait.p>@Utils•Weonook869C) BatchSvncCollectotC) BatchSvncRedisSerl© Client.php© ClosedDealStagesS@DealFieldsService.p© DecorateActivity.ph© FieldDefinitions.php© FieldTypeConvertel© HubspotClientintert(C) HubsnotTokenMan© PayloadBuilder.php• RemoteCrmObjectn© ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivity© WebhookSyncBatclIntegrationApp› Accessors›D ADI• contio> DDTO•D Filters• HlobsProspectSearchStra• ServiceTraits895C) Dataclient.oho©DecorateActivity.pt897 CC LocalSearch.nhn© LocalSearchinterfa‹899© RemoteSearch.php© Service.phpv Ml isteners© ConvertLeadActivit©PurgeLookupCache> Metadata) M Miarationclass Client extends BaseClient implemoubuc tunction 1sunauthonizedexcepllessage - surtocowerlee cgetrreturn str_contains($message,str_contains($message,str_contains($message,preg_mauchl pattern:'/N:* Vacloares und rerresnes une acc* Inas ensures cone-runnine proce* achrows soc1alaccountlokenLnvalpublic function ensureValidToken()1t sthis->oauthaccount === nuSnewToken = sthis->tokenmanaqd1+ SnewToken == nulz <public function getConfig()return $this->config;// returns only active (archived=f9 usagespublic function getOwners(): arrayreturn Sthis->getNewInstance)• aparam b00l Sarchived• dreturn arrau<owner>→ Imoort bookmarks.• Sprint Board • SRD QueueGithub & Jiminny DEVAsk Jiminny Report…..New Tab~ Google Gemini— New TabGeminiPROHi LukasWhere should we start?Create image* Create musicWrite anythingBoost my dayHelp me learn© circle Cl8 PROD US 8 Staging ail SentryPull requests • jimin...E Workers | Datadog M Jiminny Mail Dashboards | Datad...FirefoxG Search with Google or enter addressliminniMinbox (1 620)Platform Sarint302-..liminnvliminnylapp© Enter a prompt for GeminiSummarize page30 lihlDally - Platrorm • now100% S2Mon 11 May 9:45:52Usernilot 1EventsJY-20542 addlAJ reports.Mept - Dailv -Plattorm...
|
Firefox
|
Personal — Mozilla Firefox
|
meet.google.com/mie-gawc-dsi?authuser=lukas.kovali meet.google.com/mie-gawc-dsi?authuser=lukas.kovalik%40jiminny.com...
|
15342
|
|
15341
|
Firefox FileEditViewHistoryBookmarksProfilesToolsW Firefox FileEditViewHistoryBookmarksProfilesToolsWindowHelploblDaily - Platform - nowA100% C47 8• Mon 11 May 9:45:50181DEV (docker)• *з.DOCKERO 81DEV (docker)882APP (-zsh)masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zsh-zsh885screenpipe"0 ₴6DEV...
|
Notion Calendar
|
NULL
|
NULL
|
15341
|
|
15340
|
rireroxcalMISTOMrTavsco.sroledey© HubspotWebhoov @ rireroxcalMISTOMrTavsco.sroledey© HubspotWebhoov @ Pagination© HubspotPaginatC Paginationcontig(C) Paginationstate.•_ Prospectsearchstr> 0 Redisv D ServiceTraits# Opportunitysync+ SyncermEntitiesTSyncFieldstrait.T.WriteCrmtrait.p•DUuls•WeonookC) BatchSvncCollectotC) BatchSvncRedisSerlC) Client.ohoC) ClosedDealStadesSG DealFieldsService.p© DecorateActivity.ph© FieldDefinitions.php© FieldTypeConverte(0) HubsnotClientinter© HubspotTokenMan© PayloadBuilder.phpG DomotoCrmOhiontl© ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivit© WebhookSyncBatcl• Ca IntegrationApp› Accessors•D ApI• contio> MDTO•D Filters• Hlobs• ProspectSearchStra• ServiceTiraitsC) Dataclient.ohoC) DecorateActivitv.ohC LocalSearch.nhn(0 Loca|Searchinterfad© RemoteSearch.php© Service.phpv Ml isteners© ConvertLeadActivite Duraol aokunGachalM Motadata• M MiarationbookmarksProtllesWindowHelpv Search with Google or enter address6 circle Cl8 PROD Us 9 Stagina A SentrvPull requests . limin.* Workers | Datadog M Jiminny Mail* Dashboards | Datad...© HubspotSyncStrategyBase.phpy syneermenttesIfa© MatchactivityermData.png© ermactivilyservice.phgclass Cllent extends Baseclient 1mplenoubuc tunction 1sunauthonizedzxceplessage - surtocowertee egecrneturn stn contains(Smessagelstr_contains($message,869str_contains($message,(preg_match( pattern:'/Nt→ Imoort bookmarks…•Sprint Board • SRD QueueGithubNew Tab— New Tab• Google Gemini* Vacloares und rerresnes une acc* Inas ensures cone-runnine proce* achrows soc1alaccountlokenLnvalpublic function ensureValidTokenO1t sthis->oauthaccount === nuFirefoxGSearch with Google or enter addressSnewToken = sthis->tokenmanaqd1+ SnewToken == nulz <Platform Sorint302-..liminniinbox (1 620)liminnvliminnylapp889895897 C)899public function getConfigonetunn Cthic-sconfia.# noturne onlv active (anchivod=d9 usagespublic function getOwners: arrayAck Gemini• aparam b00l Sarchived* dreturn arrau<owner>Summarize page40 lDally - Platrorm • now100% S2Mon 11 May 9:45:48Usernilot 1EventsJY-20542 addlAJ reports.Mept - Dailv -Plattorm...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15340
|
|
15339
|
Firefox FileEditViewHistoryBookmarksProfilesToolsW Firefox FileEditViewHistoryBookmarksProfilesToolsWindowHelpla6|| Daily - Platform • nowA100% C47 8• Mon 11 May 9:45:48DEV (docker)• *з.181DOCKERO 81DEV (docker)882APP (-zsh)masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zsh-zsh885screenpipe"0 ₴6DEV...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15339
|
|
15338
|
New Tab
New Tab
Close tab
New Tab
Customize sideba New Tab
New Tab
Close tab
New Tab
Customize sidebar
Close Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
AI Chat settings
Close
Ask Gemini
Google Account: [EMAIL]
Summarize page
Summarize page
Mozilla Firefox
Search with Google or enter address
Search with Google or enter address
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Open context menu for Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Jiminny
Jiminny
Open context menu for Jiminny
Pipelines - jiminny/app
Pipelines - jiminny/app
Open context menu for Pipelines - jiminny/app
Inbox (1,620) - [EMAIL] - Jiminny Mail
Inbox (1,620) - [EMAIL] - Jiminny Mail
Open context menu for Inbox (1,620) - [EMAIL] - Jiminny Mail
Jiminny
Jiminny
Open context menu for Jiminny
Userpilot | Events
Userpilot | Events
Open context menu for Userpilot | Events
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Open context menu for JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Meet - Daily - Platform
Meet - Daily - Platform
Open context menu for Meet - Daily - Platform
Customize
Customize...
|
Firefox
|
Work — Mozilla Firefox
|
NULL
|
15338
|
|
15337
|
New Tab
New Tab
Close tab
New Tab
Customize sideba New Tab
New Tab
Close tab
New Tab
Customize sidebar
Close Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
AI Chat settings
Close
Ask Gemini
Google Account: [EMAIL]
Summarize page
Summarize page
Mozilla Firefox
Search with Google or enter address
Search with Google or enter address
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Open context menu for Platform Sprint 3 Q2 - Platform Team - Scrum Board - Jira
Jiminny
Jiminny
Open context menu for Jiminny
Pipelines - jiminny/app
Pipelines - jiminny/app
Open context menu for Pipelines - jiminny/app
Inbox (1,620) - [EMAIL] - Jiminny Mail
Inbox (1,620) - [EMAIL] - Jiminny Mail
Open context menu for Inbox (1,620) - [EMAIL] - Jiminny Mail
Jiminny
Jiminny
Open context menu for Jiminny
Userpilot | Events
Userpilot | Events
Open context menu for Userpilot | Events
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Open context menu for JY-20543 add AJ reports User pilot tracking by LakyLak · Pull Request #11932 · jiminny/app
Meet - Daily - Platform
Meet - Daily - Platform
Open context menu for Meet - Daily - Platform
Customize
Customize...
|
Firefox
|
Work — Mozilla Firefox
|
NULL
|
15337
|
|
15336
|
Firefox FileEditViewHistoryBookmarksProfilesToolsW Firefox FileEditViewHistoryBookmarksProfilesToolsWindowHelpla6|| Daily - Platform • nowA100% C47 8• Mon 11 May 9:45:45DEV (docker)• *з.181DOCKERO 81DEV (docker)882APP (-zsh)masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zsh-zsh885screenpipe"0 ₴6DEV...
|
Firefox
|
Work — Mozilla Firefox
|
NULL
|
15336
|
|
15335
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your invoice from Apple. - [EMAIL] - Gmail
Your invoice from Apple. - [EMAIL] - Gmail
Close tab
Location Logger
Location Logger
Finance Hub
Finance Hub
Select: transaction_imports - db - Adminer
Select: transaction_imports - db - Adminer
Claude Code | Claude Platform
Claude Code | Claude Platform
April 2026 spending by category - Claude
April 2026 spending by category - Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Applications - Admin - authentik...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15335
|
|
15334
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your invoice from Apple. - [EMAIL] - Gmail
Your invoice from Apple. - [EMAIL] - Gmail
Close tab
Location Logger
Location Logger
Finance Hub
Finance Hub
Select: transaction_imports - db - Adminer
Select: transaction_imports - db - Adminer
Claude Code | Claude Platform
Claude Code | Claude Platform
April 2026 spending by category - Claude
April 2026 spending by category - Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Applications - Admin - authentik
Applications - Admin - authentik
Електронно банкиране ДСК Директ от Банка ДСК...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15334
|
|
15333
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your invoice from Apple. - [EMAIL] - Gmail
Your invoice from Apple. - [EMAIL] - Gmail
Close tab
Location Logger
Location Logger
Finance Hub
Finance Hub
Select: transaction_imports - db - Adminer
Select: transaction_imports - db - Adminer
Claude Code | Claude Platform
Claude Code | Claude Platform
April 2026 spending by category - Claude
April 2026 spending by category - Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Applications - Admin - authentik
Applications - Admin - authentik
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Location Logger
Location Logger
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
None selected
Skip to content
Skip to content
Using Gmail with screen readers
Using Gmail with screen readers
Main menu
Gmail
Search
Search
Search mail
Advanced search options
Search mail
Support
Settings
Ask Gemini
Google apps
Google Account: Lukáš Koválik ([EMAIL])
Compose
Labels
Labels
Inbox
Inbox
Starred
Starred
Snoozed
Snoozed
Important
Important
Sent
Sent
Drafts 8 unread
Drafts
8
Purchases has menu
Purchases
Social 5206 unread has menu
Social
5,206
Updates 8765 unread has menu
Updates
8,765
Forums 6100 unread has menu
Forums
6,100
Promotions 38763 unread has menu
Promotions
38,763
More labels
More
Labels
Labels
Create new label
Labels
Labels
[Imap]/Nevyžiadaná pošta has menu
[Imap]/Nevyžiadaná pošta
arch has menu
arch
Deleted Items has menu
Deleted Items
Fibank 1229 unread has menu
Fibank
1,229
FL 6 unread has menu
FL
6
Hardware & Software has menu
Hardware & Software
HOSTING 5 unread has menu
HOSTING
5
Infected Items has menu
Infected Items
jiminny-github 7488 unread has menu
jiminny-github
7,488
Junk E-mail 219 unread has menu
Junk E-mail
219
Kontakty has menu
Kontakty
Sent Items has menu
Sent Items
WORK 848 unread has menu
WORK
848
z centra 1274 unread has menu
z centra
1,274
More labels
More
Back to Inbox
Archive
Report spam
Delete
Mark as unread
Move to
More email options
2
of
21,254
Newer
Older
Input tools on/off (Ctrl-Shift-K)
Select input tool
Print all
In new window
Your invoice from Apple.
Your invoice from Apple.
Not important
Search for all messages with label Inbox
Remove label Inbox from this conversation
Order from Apple
Order placed
Order number
MLV4N030JX
View order
Based on
this email
Correct?
Yes
No
Apple Verified sender [EMAIL]
Apple Verified sender [EMAIL]
Apple
Verified sender
[EMAIL]
03:49 (5 hours ago)
03:49 (5 hours ago)
Not starred
Add reaction
Reply
More message options
to
me
Show details
Invoice
Get help with subscriptions and purchases.
Visit Apple Support
Visit Apple Support
. Learn how to
manage [PASSWORD_FIELD]
manage [PASSWORD_FIELD]
for iTunes, Apple Books and App Store purchases.
To cancel your purchase within 14 days of receiving this invoice,
report a problem
report a problem
or
contact us
contact us
.
Learn more about your right of withdrawal
Learn more about your right of withdrawal
Apple ID is now Apple Account. You can still sign in with the same email address or phone number and password.
Apple Account
Apple Account
•
Terms of Sale
Terms of Sale
•
Privacy Policy
Privacy Policy
Copyright © 2026 Apple Distribution International Ltd.
All rights reserved
All rights reserved
Hollyhill Industrial Estate, Hollyhill, Cork, Ireland. Ireland VAT Reg No. IE9700053D
Reply
Reply
Forward
Forward
Add reaction
Calendar
Keep
Tasks
Contacts
Get add-ons
Hide side panel...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15333
|
|
15332
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your invoice from Apple. - [EMAIL] - Gmail
Your invoice from Apple. - [EMAIL] - Gmail
Close tab
Location Logger
Location Logger
Finance Hub
Finance Hub
Select: transaction_imports - db - Adminer
Select: transaction_imports - db - Adminer
Claude Code | Claude Platform
Claude Code | Claude Platform
April 2026 spending by category - Claude
April 2026 spending by category - Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Applications - Admin - authentik
Applications - Admin - authentik
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Location Logger
Location Logger
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
None selected
Skip to content
Skip to content
Using Gmail with screen readers
Using Gmail with screen readers
Main menu
Gmail
Search
Search
Search mail
Advanced search options
Search mail
Support
Settings
Ask Gemini
Google apps
Google Account: Lukáš Koválik ([EMAIL])
Compose
Labels
Labels
Inbox
Inbox
Starred
Starred
Snoozed
Snoozed...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15332
|
|
15331
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS
All docs · AFFiNE
All docs · AFFiNE
Payments Logger
Payments Logger
Your invoice from Apple. - [EMAIL] - Gmail
Your invoice from Apple. - [EMAIL] - Gmail
Close tab
Location Logger
Location Logger
Finance Hub
Finance Hub
Select: transaction_imports - db - Adminer
Select: transaction_imports - db - Adminer
Claude Code | Claude Platform
Claude Code | Claude Platform
April 2026 spending by category - Claude
April 2026 spending by category - Claude
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
lakylak/finance-hub - finance-hub - Gitea: Git with a cup of tea
Applications - Admin - authentik
Applications - Admin - authentik
Електронно банкиране ДСК Директ от Банка ДСК
Електронно банкиране ДСК Директ от Банка ДСК
Location Logger
Location Logger
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
None selected
Skip to content
Skip to content
Using Gmail with screen readers
Using Gmail with screen readers
Main menu
Gmail
Search
Search
Search mail
Advanced search options
Search mail
Support
Settings
Ask Gemini
Google apps
Google Account: Lukáš Koválik ([EMAIL])
Compose
Labels
Labels
Inbox
Inbox
Starred...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15331
|
|
15330
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15330
|
|
15329
|
Pull requests · screenpipe/screenpipe · GitHub
Pul Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS...
|
Firefox
|
Your invoice from Apple. - kovaliklukas@gmail.com Your invoice from Apple. - kovaliklukas@gmail.com - Gmail — Personal...
|
mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSv mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZlnVMGSvVXsNgQdhSmRlKp...
|
15329
|
|
15328
|
Allow
meet.google.com
to use your camera and micro Allow
meet.google.com
to use your camera and microphone?
FaceTime HD Camera
Show preview
Show preview
Remember for all cameras and microphones
Block
Allow
Meet – mie-gawc-dsi
Close tab
New Tab
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Customize sidebar
Click Allow
Click
Allow
You can still turn off your microphone and camera any time in the meeting.
protected by
reCAPTCHA...
|
Firefox
|
Meet – mie-gawc-dsi — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15328
|
|
15327
|
Allow
meet.google.com
to use your camera and micro Allow
meet.google.com
to use your camera and microphone?
FaceTime HD Camera
Show preview
Show preview
Remember for all cameras and microphones
Block
Allow
Meet – mie-gawc-dsi
Close tab
New Tab
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Open bookmarks (⌘B)
Bitwarden
Customize sidebar
Click Allow
Click
Allow
You can still turn off your microphone and camera any time in the meeting.
protected by
reCAPTCHA...
|
Firefox
|
Meet – mie-gawc-dsi — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15327
|
|
15326
|
Meet – mie-gawc-dsi
Close tab
New Tab
Open Google Meet – mie-gawc-dsi
Close tab
New Tab
Open Google Gemini (⌃X)
Firefox...• MeetFile+ New TabEdit View→ CHistoryBookmarks Profiles Tools WindowHelpX=@ meet.google.com/mie-gawc-dsi?authuser=0&pli=1[ 1 Google MeetC< →0lbl• | Daily - Platform - now100% (4 8• Mon 11 May 9:45:33kovaliklukaswichaccoumReady to join?Ask to joinOther ways to join~4 System def... ~Gemini is available in Meet as your personal in-meeting assistant. It can analyse conversation via temporaryaccess to meeting captions. Using Ask Gemini won't create a recording or store meeting data. The meeting...
|
Firefox
|
Meet – mie-gawc-dsi — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15326
|
|
15325
|
Meet
Meet
Close tab
New Tab
Customize sidebar
Open Meet
Meet
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
rTavsco.s%9 JY-20725-handle-HS-search-rateProletey© HubspotWebhocv D Pagination© HubspotPaginat© HubspotSyncStrategyBase.phpCachedcrmservicebecorator.onp© SyncRelatedActivityManager.php© ProspectCache.phpС Cпескапокetrукemotematch.ongC Paginationcontic© MatchactivityermData.png(C) Paginationstate.•_ Prospectsearchstr> D Redisv D ServiceTraits© OpportunitySync866# SyncermEntities© SyncFieldsTrait./T Writecrmtrait.p•DUts869• Weonook© BatchSyncCollector 872C) BatchSvncRedisSerlClient.phpC) ClosedDealStadesS@DealFieldsService.p© DecorateActivity.ph© FieldDefinitions.php© FieldTypeConvertel© HubspotClientintert(C) HubsnotTokenMan© PayloadBuilder.php• RemoteCrmObjectn© ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivity© WebhookSyncBatclIntegrationApp› Accessors›D ADI• contio> DDTO•D Filtersaobs• ProspectSearchStr.• ServiceTraits895© CrmActivityService.phg=| A2 A6 X1X1AC) Dataclient.oho©DecorateActivity.pt897 €C LocalSearch.nhn© LocalSearchinterfac 899© RemoteSearch.php© Service.phpv Ml isteners© ConvertLeadActivit©PurgeLookupCache> Metadata• M Miarationclass Client extends BaseClient implements HubspotClientInterfaceououc tunction 1sunauthorizedzxcentionExcentionse: 000uplessage - sur tucower teeegetnessageooereturn str_contains($message,'401 unauthorized') |Isul-contaensplllessage.'http 401') ||str_contains($message,'status code 401') |1(preg_match( pattern:'/\b401\b/', $message) && str_contains($message, 'unauthorized'));* Vacloares und rerreshes une access coken it needed berore Ard requests.* Inas ensures cone-runnine processes con t tull aue to coken expzraczon.* achrows soc1alaccountlokenInval1dexcept1onpublic function ensureValidToken(): void1t sthis->oauthaccount === nulu *SnewToken = $this->tokenManager->ensureValidToken($this->oauthAccount) :1+ Snewlioken == nulz <public function getConfig()return $this->config;// returns only active (archived=false)9 usagespublic function getOwners(): arrayreturn $this->getNewInstance() ->crm()->owners() ->getALL();• aparam b00l Sarchived• dreturn arrau<owner>NN=31E custom.log xElaravel.logA SF U# console [PKOb.A console [EU][2026-05-07 14:21:15] Local"neaders".?"Date": ["Thu,07 May 2026 14"Loncent-lyoe". "applicac!"Transter-Encoding": "chur"CF-Ray":"9t80deb8dbo0dcs"CF-Cache-Status": ["DYNAMI"Vary": ["origin,accent-encodino""server-timino":"hcid:de"x-content-tvoe-ootions."x-hubspot-correlation-i"Set-Cookip"."" cfhm=s07-May-26 14:51:15 GMT;"Report-To":["{\"endpoints)":[{\"urZ\": \"https:|V/\\/a.nel\"group\" :\"cf-nell",\"max_age\":604800}"],"NEL" : ["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server": ["cloudflare"]1} €"correlation_id": "95236535-"trace_id":"c7ab8365-903f-4( Pull requests • screenpipe/screenpipe • GitHubDNS / Nameservers | HostingenNginx Proxy ManagerScreenpipe - Archive® SQLite Web: archive.db@ SQLite Web: db.sqliteDXP4800PLUS-B5F8A AFFINE - All In One KnowledaeOSAll docs • AFFiNE= Payments LoggeYour invoice from Apole. - [EMAIL](*) Location Logger' Finance Hub# Select: transaction imnorts - db - AdmineClaude code Claude PlattormS April 2026 spendina by cateaory - Claude0 Iakulaklfinanco,hnh - financo.huh - Giton, Git with 9 0Applications - Admin - authentikЕлектронно банкиране ДСК Директ от Банка ДСК(* Location Logge:L Now Tal• - mail.google.com/mail/u/0/#inbox/FMfcgzQgLjZInVMGSvVXsNgQdhSmRIKpM GmailSearch mailInboxStarredSnoozedImportantSentDraftsPurchasesSocialUpdatesrorumsPromotionsMore3,2068,7656,10038,763Labels[Imap]/Nevyziadaná postaDeleted temsFibankHardware & SoftwareHOSTINGInfected Itemsjiminny-githubJunk E-mailKontaktySent ItemsWORKz centraMore7.488Your invoice from Apple.Order from AppleOrder placedView ordelBased on this emailApple @<[EMAIL]@gmail.com10 Mav 2026|1-13573493610MLV4INUSOJX834131622951Aoo Storeina With Itdlbally - Platrorm - nov100% S2• мon 11 Mаy 9.40.322 of 21,254Order numberMLV4NO30JX03:49 (5 hours ago)Correct:InvoiceBSa.. 447 (Apple Pay)Inclusive of VAT at 20% 0.66 €|CubtatalVAT charged at 20%0,66 €TOTALI3,99 €set neip with subscriptions and purciow to manage [PASSWORD_FIELD] forTo cancel your purchase within 14 days of receiving thisinvolce, report a problem or contact us.Abole |D is now Apole Account. You can still sian in with the same email address or phone number and password...
|
Firefox
|
Meet — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15325
|
|
15324
|
Meet
Meet
Close tab
New Tab
Customize sidebar
Open Meet
Meet
Close tab
New Tab
Customize sidebar
Open Google Gemini (⌃X)
Open history (⇧⌘H)
Firefox•••@* New Tab+ New TabFileEdit→ViewHistoryBookmarksToolsWindowHelp© v Search with Google or enter addresslhl• | Daily - Platform - nowA100% C8• Mon 11 May 9:45:30...
|
Firefox
|
Meet — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15324
|
|
15323
|
rireroxcalMistormelprTavsco.sProletey© SyncRelated rireroxcalMistormelprTavsco.sProletey© SyncRelatedActivityManager.php© HubspotWebhoo© HubspotSyncStrategyBase.phpCachedcmmservicebecorator.onp© ProspectCache.phpv D PaginationС Cпескапокetrукemotematch.ong© HubspotPaginatC Paginationcontic© MatchacuivitycrmData.ong© ermactivilyservice.phg(C) Paginationstate.•_ Prospectsearchstr> D Redisclass Client extends BaseClient implements HubspotClientInterface=| A2 A6 X1X1AVoubuc tunction 1sunauthorizedzxcentionExcentionse: 000uv D ServiceTraitspllessage - surtocowertee-cgetnessageo)r© OpportunitySync866# SyncermEntitiesreturn str_contains($message,'401 unauthorized') |I© SyncFieldsTrait./str_contains($message,'http 401') ||T Writecrmtrait.p!!!869str_contains($message,'status code 401') |1•DUts(preg_match( pattern:'/\b401\b/', $message) && str_contains($message, 'unauthorized'));Weohook© BatchSyncCollector 872C) BatchSvncRedisSerlClient.php* Vacloares and rerresnes une access coken it needed berore Ard requests.C) ClosedDealStadesS@DealFieldsService.p* Inas ensures cone-runnine processes con t tull due to coken expzraczon.© DecorateActivity.ph* achrows soc1alaccountlokenLnval1dexcept1on© FieldDefinitions.php© FieldTypeConvertel© HubspotClientintertpublic function ensureValidToken(): void(C) HubsnotTokenMan© PayloadBuilder.php1t sthis->oauthaccount === nulu *• RemoteCrmObjectn© ResponseNormalizeservice.ono© SyncFieldAction.ph$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);UNUNN© SyncRelatedActivity1+ Snewlioken == nulz <© WebhookSyncBatclIntegrationAppsthis->accesslloken = Snewtioken:› Accessors=31›D ADI• contiopublic function getConfig()> DDTO•D Filtersreturn $this->config;aobs• ProspectSearchStr.• ServiceTraits895// returns only active (archived=false)C) Dataclient.oho©DecorateActivity.pt9 usages897 €tpublic function getOwners(): arrayC LocalSearch.nhn® LocalSearchlnterfac 899return $this->getNewInstance() ->crm()->owners() ->getALL();© RemoteSearch.php© Service.phpv Ml isteners© ConvertLeadActivit©PurgeLookupCache• aparam b00l Sarchived> Metadata• dreturn arrau<owner>• M MiarationE custom.log xElaravel.logA SF U# console [PKOb.A console [EU)[2026-05-07 14:21:15] Local"neaders".?"Date": ["Thu,07 May 2026 14"Loncent-lyoe". "applicac!"Transter-Encoding": "chul"Connection" : ["keep-alive""CF-Ray": ["9f80deb8db60dc3"CF-Cache-Status":"DYNAM!"Strict-Transport-Secur1ty"Vary": ["origin,accept-encoding"]."server-timino":"hcid:de"x-content-tvoe-ootionsi"Set-Cookip"."" cf hm=.07-May-26 14:51:15 GMT;"Report-To":["{\"endpoints)":[{\"urz\":\"https:\V\V/a.nel\"group\" :\"cf-nell",\"max_age\":604800}"],"NEL": ["€\"success_fraction\":0.01,\"report_tol":\"cf-nell",\"max_age\":604800}"],"Server": ["cloudflare"]1} €"correlation_id": "95236535-"trace_id":"c7ab8365-903f-4→ X( Pull requests • screenpipe/screenpipe • GitHubDNS / Nameservers | HostingenNginx Proxy ManagerScreenpipe - Archive® SQLite Web: archive.db@ SQLite Web: db.sqlite@ GitHub - screenninelscreennine: Run agents that workDXP4800PLUS-B5F8AFFINE - All in One KnowledgeOSE All docs- AFFINE= Payments LoggeM Your invoice from Apple, - [EMAIL] - C® Location Logger' Finance Hubell Select: transaction imnorts - db - Admine* Claude Code | Claude PlatformSW April 2026 spendina by category - Claudeolakylak/finance-hub - finance-hub - Gitea: Git with a ciApplications - Admin - authentikЕлектронно банкиране ДСК Директ от Банка ДСК@ Location LoggerMeet+ New Tab©= meet.google.com/mie-gawc-dsi?authuser=08li=1[1 Google MeetC<40ll O| Daily-Platform-now A 100%C4 &• Mon 11 May 9:45:[EMAIL] to join?Ask to joinOther ways to join4) System def…..meet.aooale.comferring data from www.astatic.com...
|
Firefox
|
Meet — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15323
|
|
15322
|
Firefox FileEditViewHistoryBookmarksProfilesToolsW Firefox FileEditViewHistoryBookmarksProfilesToolsWindowHelpla6|| Daily - Platform • nowADEV (docker)• *з.DOCKERO 81DEV (docker)882APP (-zsh)masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zsh-zsh885100% C47 8• Mon 11 May 9:45:28181ffmpeg- 86DEV...
|
Firefox
|
Meet — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15322
|
|
15321
|
Meet
meet.google.com
Pull requests · screenpipe/sc Meet
meet.google.com
Pull requests · screenpipe/screenpipe · GitHub
Pull requests · screenpipe/screenpipe · GitHub
DNS / Nameservers | Hostinger
DNS / Nameservers | Hostinger
Nginx Proxy Manager
Nginx Proxy Manager
Screenpipe — Archive
Screenpipe — Archive
SQLite Web: archive.db
SQLite Web: archive.db
SQLite Web: db.sqlite
SQLite Web: db.sqlite
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
GitHub - screenpipe/screenpipe: Run agents that work based on what you do. 24/7 local screen & mic recording for the superintelligence era · GitHub
DXP4800PLUS-B5F8
DXP4800PLUS-B5F8
AFFiNE - All In One KnowledgeOS
AFFiNE - All In One KnowledgeOS...
|
Firefox
|
Meet — Personal
|
meet.google.com/mie-gawc-dsi?authuser=0&pli=1
|
15321
|
|
15320
|
FirefoxFileEditViewHistoryBookmarksToolsWindowHelp FirefoxFileEditViewHistoryBookmarksToolsWindowHelp••DEV (docker)DOCKERO ₴1DEV (docker)882APP (-zsh)|• жзmasterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY]@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]| Daily - Platform • now-zsh-zsh885100% C8• Mon 11 May 9:45:22181screenpipe"O 86DEV...
|
Firefox
|
Personal — Mozilla Firefox
|
NULL
|
15320
|
|
15319
|
PhostormINavicarecodeFV faVsco.js°9 JY-20725-handl PhostormINavicarecodeFV faVsco.js°9 JY-20725-handle-HS-search-rate-linroledey© HubspotWebhoo© HubspotSyncStrategyBase.phpCachedcrmservicebecorator.onp© ProspectCache.phpv D Pagination© HubspotPaginatC Paginationcontic© MatchacuivitycrmData.ong© ermactivilyservice.phg(C) Paginationstate.•_ Prospectsearchstr> 0 Redisclass Cllent extends Baseclient 1mpLements HubspotcllentintertaceM A2 A65 X1X1 Av D ServiceTraits# Opportunitysync# SyncermEntitiesTSyncFieldstrait.T Writecrmtrait.p869•DUts• Weonook© BatchSyncCollector 872C) BatchSvncRedisSerl• Client.ohoC) ClosedDealStadesSG DealFieldsService.p© DecorateActivity.ph© FieldDefinitions.phpC) SieldTvneConverte(0) HubsnotClientinter(C) HubsnotTokenMan©Payloadbullder.onoG DomotoCrmOhiontl© ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivit© WebhookSyncBatcl• Ca IntegrationApp› Accessors•D ApI• contioMDTO•D Filtersaobs• ProspectSearchStr.895• ServiceTraitsC) Dataclient.ohoC) DecorateActivitv.oh897 GC LocalSearch.nhn0 Loca|Searchinterfat899© RemoteSearch.php© Service.phpv Ml isteners© ConvertLeadActivite Duraol aokunGachalM Motadata• M Miarationoubuc tunction 1sunauthorizedzxcention Excention sel: 000lpllessage - surtocowertee-cgetnessageo)rneturn stn contains Smessage.sul-contaensplllessage'401 unauthorized') ||'http 401') |1str_contains($message,'status code 401') ||(preg_match( pattern: '/\b401\b/', Smessage) && str_contains($message, 'unauthorized'));* Vacloares und rerreshes une access coken it needed berore Ard requests.* Inas ensures cone-runnine processes con t tull due to coken expzracion.*dchrows soczalaccountlokenLnval1dexcept1onpublic function ensureValidToken(): void1t sthis->oauthaccount === nulu *SnewToken = sthis->tokenManager->ensureval1dToken(sthis->oauthAccount):1+ Snewlioken == nulz <public function getConfigonotunn Sthic-sconfia.II returns only active (archived=false)9 usagespublic function getOwners: arrayreturn Sthis->getNewInstance->crm->ownersO->getallO:• aparam b00l Sarchived* dreturn arrau<owner>lelner Code will hoin INF to underctand vour Laravel ann code II Generate II Don't Show Anvmore (27 minutes aao)=custom.log ^A SF (jiminny@localhost]4 HS_local (jiminny@localhost]# console [PKob.# console leu)# console [slAGiNg)[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"neaders".?"Uace":L"Inu,or May 2020 14.21.15 6Ml"Jn"Transter-Encod1nq":"chunked")."Connection":"keep-alive""CF-Ray":L"9t80debödb60dcsa-S0F"J,"Strict-Transport-Secur1ty":"max-aqe=31536008* 1ncludeSubDomains: preload")naccent-encodino""access-control-allow-credentials": ["false"]."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\","x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"So+-Cookie"." c+hm-Stlirtd0aXVr.kSandas6hzVVKhzTn0BidvMaheCtm0V-1778163675-1.0.107-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Report-To":["{"endpoints\":[{"unl".httns:la.nel.cloudflane.com./renon+W/v42c=NVA1cVTPQfVm32anS0#xVF/sd2RN\"group)":\"cf-nel\","max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,"report to":|"cf-nel\"."max age":604800}"]"Server": ["cloudflare"]}} {"correlation_ 1d":"95256555-ec98-4541-b9za-adta/Sboyeab","trace_10":C/AD8565-905t-4604-9405-0e5b551e5545"—3140100% 4 • Mon 11 May 9:45:20AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limit Rej caten (eanecaea tas sextentan sexcepten) .)+0 ..wcatch\Sentry:: captureException($exception);Sresponse = sthis→>response→>errorinternalError selt::MESSAGE GENERALEXCEPTIONSEARCHINGRateLimicexcepcion excepczon excends excepcion = so it talls through to the last catch• Reports to Sentry as a 500 internal error (noisy!)• Returns errorInternalError to the client (extension shows "something went wrong")• The user has no idea this is transientThis is the wrong outcome for a transient 429. There's no retry, no backoff, no useful UX!Contirmed callersCrmController.php:84Sresponse = ScrmService->find(Sname, Sscopes):* Bul lhornSearchCommand (CLI debug only). So yes - fand() is essentially controller-only. It is a synchronous request driven by user typing in the ChromeWhat should hannen on rate limitThis is a cunchronous HTTD reauest — we canlt releace-and-retrv like a ich Ontions ordered hv what I'd recommend.(a) Translate RateLimitException → HTTP 429 with Retrv-After header. This is the standard wav.D php} catch (RateLimitException $e) {return Sthis->response->setStatusCode(429)»ertrortoomanynequessl ern search race-tamared, predse reury shomcty.eThe frontend then shows a friendly "too many searches, retry in N seconds" instead of a generic error. No Sentry noise.(b) Inside find(), retry once with a short bounded sleep() (similar to searchCallByRecordingURLToken). HubSpot's SECONDLY policy has retry_after = 1 . Asinale 1sln-reauest retry.wouldlahsorh.the.most.common.case//secondlvlhurst Can.retries.at=a.svnchronousHTiTD reauest.shouldnlfhlock 10s/for TEN_SECONDI( Dunit cachofalures, (achesitreaber wit T. 0 il o ech an exception, butit does cache sucestil empy arcys. The 8009 eacthias et fandlresults actually helps absorb pressure under typina/repeat searches — that's good, leave it.Recommendation: (a) + (b) combined. Catch RateLimitException inside find(), retry once after min(SretryAfter, 1) s, and if it still fails, let it bubble: controllermaps it to HTTP 429 with Retrv-After7. The Syictind cleen(1) Retrv in searchcall cForPeriodBvPade cearchcal BvRecordinauRLTokenCurrent code• Service.php:1584-15993 catch (Fycention Ceycention)Sthis->logger→>info('[HubSpot) Search calls for period failed'. [...1):OK lets make it extremely simple. Only consider eclient- phpffL220-229 for CHubspotPaginationservice.php#fL162-198 lwill remove it from finio 4 spaces...
|
Notion Calendar
|
NULL
|
NULL
|
15319
|
|
15318
|
iTerm2ShellEditViewSessionScriptsProfilesWindowH iTerm2ShellEditViewSessionScriptsProfilesWindowHelpla6|| Daily - Platform • now100% C47 8• Mon 11 May 9:45:20DEV (docker)DOCKER881DEV (docker)882APP (-zsh)• жзmasterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zshDaily - Platformnow - 09:45-10:05Ci Join Google MeetDEV...
|
Notion Calendar
|
NULL
|
NULL
|
15318
|
|
15317
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15317
|
|
15316
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15316
|
|
15315
|
PhpStormVIewINavicareCodeFV faVsco.js?9 JY-20725-h PhpStormVIewINavicareCodeFV faVsco.js?9 JY-20725-handle-HS-search-rate-limitProiect© SyncRelatedActivityManager.php© HubspotWebhocv D Pagination© HubspotPaginat© HubspotSyncStrategyBase.phpCachedcrmservicebecorator.pnp© ProspectCache.phpC Paginationcontig© PaginationState© MatchactivityermData.png© ermactivilyservice.phg•_ Prospectsearchstr> D Redisclass HubspotPaq1nat1onServiceA12 A Vv D ServiceTraits1166OpportunitySync 167+ SyncermEntities& syncFields Trait.) 169T.WriteCrmtrait.p•DUts•Weonook© BatchSyncCollector 173C) BatchSvncRedisSerlC) Client.ohoC) ClosedDealStadesSG DealFieldsService.p© DecorateActivity.ph@ FieldDefinitions.pnf 179C) SieldTvneConverte© HubspotClientinten 181© HubspotTokenMan: 10© PayloadBuilder.pnp1ezG DomatoermAhionti• ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivity 188© WebhookSyncBatcl• Ca IntegrationApp190› Accessors•D ApI• contioMDTO•D Filtersaobs• ProspectSearchStr.• ServiceTraits199C) Dataclient.ohoC) DecorateActivitv.ohC LocalSearch.nhn200© LocalSearchinterfas,/© RemoteSearch.php© Service.phpv Ml istenerse Convortl ond dctivit© PurgeLookupCache ,1,M Motadata• M Miarationprivate function executesearchrequest(cllent scllent, string sobjectlype, array spayload, Paginationstate $catch Exception se) *if (Sclient->isUnauthorizedException(Se)) {Sth1s->loqger->warnina( Hubspot.Got 401durina paqinationattempting token refresh'.'team id' => Sclient->aetConfia()->qetTeam@->qet1d@.error' => se->cetressadeo.ScLient->ensureValidTokenO:Sstate->uodateLasttokenChecko:+rydSrosult = Sclient->seanch/Sohriecttvne Snavloadh•$this->logger->info('[Hubspot] Token refresh and retry successful', ['team_id' => $client->getConfig->getTeam->getIdO,D:return Sresult;} catch (\Exception $retryException) {Sthis->logger->error('[Hubspot] Retry request failed after token refresh'. ['team id' => Sclient->getConfiqO->qetTeamO->qetIdo'oriqinal error' => se->qethessageo'retry error' => SretryException->qetMessageOthrow sretrvexcention:RateLimitExcention and other excentions are re-thrown as-isthrow Se:1 usageprivate function updateLastRecordId(array Spage, PaginationState $state): void{...}private function getNext0ffset(array $page): inti...}private function loqPaginationProgress(PaginationState $state, int $teamId, string Sendpoint): void{...}Helner Code will hoin INF to underctand vour Laravel ann code II Generate II Don't Show Anvmore (26 minutes aadl40100% 4 • Mon 11 May 9:45:08AskJiminnyReportActivityServiceTest v+0 ..=custom.log~=laravel.logA SF (jiminny@localhost]4 HS_local (jiminny@localhost]# console [PKob.# console leu)# console [slAGiNg)[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {W19лV"neaders".?"Uace":L"Inu,or May 2020 14.21.15 6Ml"Jn"Transter-Encod1nq":"chunked")."Connection":"keep-alive"]"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"Strict-Transport-Secur1ty":"max-aqe=31536008* 1ncLudeSubDomains: preload")n"Vary":"orioin.accent-encodino""access-control-allow-credentials":["false"]."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\","x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"So+-Cookie"." c+hm-Stlirtd0aXVr.kSandas6hzVVKhzTn0BidvMaheCtm0V-1778163675-1.0.107-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"."","endpoints\":[{"unl".httns:la.nel.cloudflane.com./renon+W/v42c=NVA1cVTPQfVm32anS0#xVF/sd2RN26\"group)":\"cf-nel\","max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,"report to\":|"cf-nel\"."max age":604800}"]"Server": ["cloudflare"]}} {"correlation_1d":"95256555-ec78-4541-b9za-adta/Sb6Yeab"."trace_10":C/AD8565-905t-4604-9405-0e5b551e5545"CascadeHubspot Rate Limit Recatch (Serviceunaval lab Leexception Sexception) , ..• )- catch (CrmEycention Seycention) ‹catch\Sentry:: captureException($exception);se→›errorinternalError selt::MESSAGE GENERAL EXCEPTION SEARCHING):RateLimicexcepcionextends exception = so lt talls through to the last catch,• Reports to Sentry as a 500 internal error (noisy!)• Returns errorInternalError to the client (extension shows "something went wrona")• The user has no idea this is transientThis is the wrong outcome for a transient 429. There's no retry, no backoff, no useful UX.Contirmed callersCrmController.php:84Sresponse = ScrmService->find(Sname, Sscopes):* Bul lhornSearchCommand (CLI debug only). So yes - fand() is essentially controller-only. It is a synchronous request driven by user typing in the ChromeWhat should hannen on rate limitThis is a cunchronous HTTD reauest — we canlt releace-and-retrv like a ich Ontions ordered hv what I'd recommend.(a) Translate RateLimitException → HTTP 429 with Retrv-After header. This is the standard wav.O php} catch (RateLimitException $e) {return Sthis->response->setStatusCode(429)»ertrortoomanynequessl ern search race-tamared, predse reury shorcty.eThe frontend then shows a friendly "too many searches, retry in N seconds" instead of a generic error. No Sentry noise.(b) Inside find(), retry once with a short bounded sleep() (similar to searchCallByRecordingURLToken). HubSpot's SECONDLY policy has retry_after = 1 . Asinale 1sln-reauest retry.wouldlahsorh.the.most.common.case//secondlvlhurst Can.retries.at=a.svnchronousHTiTD reauest.shouldnlfhlock 10s/for TEN_SECONDI( Dunit cachofalures, (achesitreaber wit T. 0 il o ech an exception, butit does cache sucestil empy arcys. The 8009 eacthias et fandlresults actually helps absorb pressure under typina/repeat searches — that's good, leave it.Recommendation: (a) + (b) combined. Catch RateLimitException inside find(), retry once after min(SretryAfter, 1) s, and if it still fails, let it bubble: controllermaps it to HTTP 429 with Retrv-After7. The Syictind cleen(1) Retrv in searchcall cForPeriodBvPade cearchcal BvRecordinauRLTokenCurrent code• Service.php:1584-15993 catch (Fycention Ceycention)Sthis->logger→>info('(HubSpot) Search calls for period failed'. [...J):callets arce ialxtorfery simple. Onl archicalls foreriod phetrchCeI|RvRecordinal IRI Token) In that cace we will onlv fire if 120 was triaaered formm tind and other+ « CodePo 4 spaces...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15315
|
|
15314
|
iTerm2•• 0ShellEditViewSessionScriptsProfilesWin iTerm2•• 0ShellEditViewSessionScriptsProfilesWindowHelpla6|| Daily - Platform • now100% C47 8• Mon 11 May 9:45:07DEV (docker)DOCKER₴1DEV (docker)882APP (-zsh)• жзmasterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zshDaily - Platformnow - 09:45-10:05Ci Join Google MeetDEV...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15314
|
|
15313
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15313
|
|
15312
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15312
|
|
15311
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15311
|
|
15310
|
Undo
Redo
Cut
Copy
Paste
Paste and Match Style
Php Undo
Redo
Cut
Copy
Paste
Paste and Match Style
PhpStormVIewINavicareCodeFV faVsco.js?9 JY-20725-handle-HS-search-rate-limitProiect© SyncRelatedActivityManager.php© HubspotWebhocv D Pagination© HubspotPaginat© HubspotSyncStrategyBase.phpCachedcrmservicebecorator.pnp© ProspectCache.phpC Paginationcontic© PaginationState© MatchactivityermData.png© ermactivilyservice.phg•_ Prospectsearchstr> 0 Redisclass HubspotPaq1nat1onServiceA12 A Vv D ServiceTraits1166OpportunitySync 167+ SyncermEntitiesu syncFields Trait.) 169T.WriteCrmtrait.p•DUts•Weonook© BatchSyncCollector 173C) BatchSvncRedisSerlC) Client.ohoC) ClosedDealStadesSG DealFieldsService.p© DecorateActivity.ph@ FieldDefinitions.pnf 179C) SieldTvneConverte© HubspotClientinten 181© HubspotTokenMan: 10© PayloadBuilder.pnp1ezG DomotoCrmOhiontl• ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivity 188© WebhookSyncBatcl• Ca IntegrationApp190› Accessors›D ADI• contioMDTO•D Filtersaobs• ProspectSearchStr.• ServiceTraits199C) Dataclient.ohoC) DecorateActivitv.ohC LocalSearch.nhn200© LocalSearchinterfas,/© RemoteSearch.php© Service.phpv Ml istenerse Convortl ond dctivit© PurgeLookupCache ,1,M Motadata• M Miarationprivate function executesearchrequest(cllent scllent, string sobjectlype, array spayload, Paginationstate $catch Exception se) *if (Sclient->isUnauthorizedException(Se)) {Sth1s->loqger->warning( Hubspot.Got 401durina paqinationattempting token refresh'.'team id' => Sclient->aetConfia()->qetTeam@->qet1d@.error' => se->cetressadeoiScLient->ensureValidTokenO:Sstate->uodateLasttokenChecko:+rydSrosult = Sclient->seanch/Sohriecttvne Snavloadh•$this->logger->info('[Hubspot] Token refresh and retry successful', ['team_id' => $client->getConfig->getTeam->getIdO,D:return Sresult;} catch (\Exception $retryException) {Sthis->logger->error('[Hubspot] Retry request failed after token refresh'. ['team id' => Sclient->getConfiqO->qetTeamO->qetIdo'oriqinal error' => se->qethessageo'retry error' => SretryException->qetMessageOthrow sretrvexcention:RateLimitExcention and other excentions are re-thrown as-isthrow Se:1 usageprivate function updateLastRecordId(array Spage, PaginationState $state): voidf...}private function getNext0ffset(array $page): inti...}private function loqPaginationProgress(PaginationState $state, int $teamId, string Sendpoint): void{...}Helner Code will hoin INF to underctand vour Laravel ann code II Generate II Don't Show Anvmore (26 minutes aaoi=custom.log~=laravel.logA SF (jiminny@localhost]4 HS_local (jiminny@localhost]# console [PKob.# console leu)# console [slAGiNg)[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"neaders".?"Uace":L"Inu,or May 2020 14.21.15 6Ml"Jn"Transter-Encod1nq":"chunked")."Connection":"keep-alive"]"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"Strict-Transport-Secur1ty":"max-aqe=31536008* 1ncLudeSubDomains: preload")n"Vary":"orioin.accent-encodino""access-control-allow-credentials":["false"]."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\","x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"Set-Cookie":["__cf_bm=SrUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"."","endpoints\":[{"unl".httns:a.nel.cloudflane.com./nenon+|W/v42c=NVA1cVTPQfVm32anS0ixVF/sd2RI25"group\":\"cf-nel\","max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,"report to\":|"cf-nel\"."max age":604800}"]"Server": ["cloudflare"]}} {"correlation_1d":"95256555-ec98-4541-b9za-adta/Sb6Yeab","trace_10":C/AD8565-905t-4604-9405-0e5b551e5545"• I Daily - Platform • in 1m100% S2• Mon 11 May 9:44:22AskJiminnyReportActivityServiceTest vCascadeHubspot Rate Limit Re+0 ..3. Client: : search() - new unified method routing all search calls through executeRequest( )4. Client:: isHubspotRateLimit()/parseRetryAfter() - 429 detection + header parsingb. HubspotPaginationservice - now calls client→>search() instead or raw MlTP;tixes usteep to skip tirst request, tixes shou laLogProgress ori-by-one6. HandleHubspotRateLimit middleware - catches RateLimitException, re-queues job with delay up to 20 attempts7. MatchActivityCrmData — adds HandleHubspotRateLimit middleware8. MatchCrmbata — swaos incrementRequestCount to run after resolveFromcall not before)ssues FoundCritical1 find() swallows Ratel imitFxcention cilentlv incide a Gache.+ romomber callback• Service.php:598-656Sresponse = sth1s->cl1ent→>search(sobjectlype, Spayload);I" catch (BadRequest se) 1throw Se.Client->search() can now throw RateLimitException (which extends LogicException, not BadRecest . RateLimitE,ception will bubble out of the Cache:: remember callback — which then caches the exception throw-path with the bad result (Laravel's Cache: : remember doesnot cache excentions. but the exceotion willoropagate uo uncaucht past anvthina exoectina anolelto return anarrav). Any caller of aindleT the cRMiorospecisearch used in the Ul/extension) will get an unhandled exception. This is inconsistent with the intent — those callers don't have HandleHubspotRateLimitmiddlewareZ. searchcaulsrorPer1odbyPage searchcallsrorPer1od searchcallbyRecord1ngURLToken eat the RateLimitexceptionAll three catch Fycention• Service.php:1584} catch (Exception Sexception) {RateLimitException extends LogicException which extends Exception ? Let me verifySearched class LogicExdRateLimitException extends LogicException extends \LogicException extends \Exception . So all catch (Exception se) blocks in Service. php will swallow theRateLimitException - rate limit events are silently treated as call-not-found and processing continues, losing the ability to back off. This includes:•searchCallsForPeriodByPage → calls sleep(1) and retries once with no back-off awareness• The do-while loop in getCalls() (line 1561) - continues to next page as if the page returned emptySerious2 MatchActsuitvGrmhnta hac HandloHubenotPatol imit middllowaro hut tha actunl CPM coll nath may not throw Patol imitEycontiorUndoteCrmData() , which eventually calls matchExact lyByEmail() → getContactByEmail(). That method doesnot go through executeReectly with no rate limit wrapping:1 Client.php:503-523public function getcock lets make it extremely s÷ « Codearrav Stields = : arravt.pho#L220-229 for @HubspotPaginationService.pho#L162-198 I will remove it from find and otherPaste and Match StvleSelect AllOnen DevToolsW Windsurf Toams.198•6/1526 charc 26 line hreakslUTE.8Po 4 spacesw...
|
PhpStorm
|
|
NULL
|
15310
|
|
15309
|
Undo
Redo
Open DevTools
Copy
Paste
Paste and Match Undo
Redo
Open DevTools
Copy
Paste
Paste and Match Style
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(ah)Daily - Platform - in 1 m100% C47 8• Mon 11 May 9:44:22DEV (docker)DOCKER•₴81DEV (docker)882APP (-zsh)• жзmasterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-Jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zshDaily - Platformin 1m - 09:45-10:05Ci Join Google MeetDEV...
|
PhpStorm
|
|
NULL
|
15309
|
|
15308
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15308
|
|
15307
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15307
|
|
15306
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15306
|
|
15305
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15305
|
|
15304
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15304
|
|
15303
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15303
|
|
15302
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15302
|
|
15301
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15301
|
|
15300
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15300
|
|
15299
|
Create new PHP Test
PhostormINavicarecodeFV faVsco Create new PHP Test
PhostormINavicarecodeFV faVsco.js°9 JY-20725-handle-HS-search-rate-l© HubspotWebhoo© HubspotSyncStrategyBase.phpv D PaginationC Paginationcontic© PaginationState•_ Prospectsearchstr> 0 Redisv D ServiceTraits# Opportunitysync+ SyncermEntitiesT SuncFieldstrait.T Writecrmtrait.ol•DUts• WeonookC) BatchSvncCollectotC) BatchSvncRedisSerl© Client.phpC) ClosedDealStadesSG DealFieldsService.p© DecorateActivity.ph© FieldDefinitions.phpC) SieldTvneConverte(0) HubsnotClientinterll(C) HubsnotTokenManPayloadbullder.ongG DomotoCrmOhiontl9 ResponseNormalizec) service.ono© SyncFieldAction.ph© SyncRelatedActivitc) WebhooksyncBatc• Ca IntegrationApp› Accessors•D ApI• contioMDTO•D Filtersaobs• ServiceTraitsC) Dataclient.ohoC) DecorateActivitv.ohC LocalSearch.nhn(0 Loca|Searchinterfad© RemoteSearch.php© Service.phpv D Listeners© ConvertLeadActivite Duraol aokunGachalM Motadata• M MiarationС Cпескапокetrукemotematch.ongclass HubspotpaginatzonserviceM A12 ^orivate rtunctionnandlepacnnationstrateaviint SresultsPerPage,inc sceamlaarray t...private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool{...}private function validateTokenIfNeeded(Client Sclient. PaginationState Sstate): void{...}1 usagevrivate function executeSearchRequest(Clrent Sclient, string Sobnectivoe, arrav Soavload. PaqinationState S!Lreturn Sclient->searchSobiecttvoe. Spavload)catchExcention Se) <1fSclient->isUnauthorizedExcentionSe))<Sthis->loagen->warninaHubsoot Got 401 durina nagination. attemotina token refresh!.i'team_id' => $client->getConfig(->getTeam->getid,"ennont => Ce->ae+MeccaneOllSclient->ensureValidTokenO:Sstate->updateLastTokenCheckO:try {Sresult = $client->search($objectType, $payload):Sthis->logger->info('[Hubspot] Token refresh and retry successful'. ['team id' => Sclient->getConfia(->qetTeamO->qetIdO.return sresult} catch (\Exception $retryException) {Sthis->loqger->error('[Hubspot] Retry request failed after token refresh'. ['team_id' => $client->getConfig()->getTeam()->getId(),'oriqinal erron' => Se->aetMessage@'retry erron' => Sretrvexcention->aetMessageOi.lthrow SretryException;olner Code will hoin INF to underctand vour Laravel ann code |l1hel1Daily - Platform - in 4 m100% Lz• • Mon 11 May 9:41:12AskJiminnyReportActivityServiceTestA SF (jiminny@localhost]4 HS_local (jiminny@localhost]Cascade# console [PKOb.# console leu)# console [slAGiNg)Hubspot Rate Limit Re+0 ..[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"neaders".?pustze tuieazon gettonttettytha ttstraing senally, array sizetus E llli alra"Uace":L"Inu,or May 2020 14.21.15 6Ml"Jnuryt"Transter-Encod1nq":"chunked")."Connection":"keep-alive"Scontact = sth1s→>executeRequest(fn () = sthis-saetNeuInstance()->crm()>contacts(hasicAni()->getById($email, implode(',', $fields), null, false, 'email')"CF-Ray":L"9t80debödb60dcsa-S0F"J,"Strict-Transport-Security":["max-aqe=31536000: includeSubDomains: preload"].accent-encodino""server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",return ['id' => Scontact->getId(), 'properties' => $contact->getProperties()]:} catch (RateLimitException Se) {throw se.I let ioh middlevare handle} catch (ContactApiException Se) {// genuine 404 /chas→>log sintol Inuospot ratted to terch contact, l...areturn :"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],"So+-Cookie"." c+hm-Stlirtd0aXVr.kSandas6hzVVKhzTn0BidvMaheCtm0V-1778163675-1.0.1Note executeRequest's current isHubspotRateLimit already includes ContactApiExcept ion so this works as-is — you just need to actually route the call throughit. Same pattern for getContactById, getOpportunityById, getAccountById, get0wners, batchRead0bjects, and the makeRequest() raw HTTP path.07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To".r"*Why this matters with the rate-limiter increment in Matchcrmbata"endpoints\":[{You moved this"unl".httns:la.nel.cloudflane.com./renon+W/v42c=NVA1cVTPQfVm32anS0#xVF/sd2RNMatchCrmData.nhn-112-112\"group)":\"cf-nel\","max_age\":604800}"]|"NEL":["{\"success_fraction\":0.01,Scrmobiects = ScrmûhiectsResolver-sresolveFromcalsthis-scrmService. Sthis-scaldnSrateLimiter->incrementRequestCount(Sactivity->getCrm()):reportto. "cr-nel,"max age":604800}"]"Server": ["cloudflare"]}} {resolveFromCall may issue 2-4 HubSpot API calls (email + phone + domain + identifier matches). The job-level limiter still counts that as 1. Today this was OKbecause there was no client-side limit at all. Once the new reactive limit is in place:• If any inner call 429s, RateLimitException bubbles → middleware releases the job → incrementRequestCount is never called for that attempt"correlation_1d":"95256555-ec98-4541-b9za-adta/sboYeab".•The job-level counter stays low; the next attempt tires immediately when middleware delay expires"trace_10":C/AD8565-905t-4604-9405-0e5b551e5545• Net effect: the client-level reactive limit is doing the real backoff; the job-level limiter is now a much coarser secondary guard. This is fine, but means the ProviderRateLimiter quota numbers should probably be relaxed (or the increment moved closer to each underlvina APl call, e.a. inside executeRequest itseltStrona suadestion.lf.vouwant theliob-leve lllimiter to.remain.meaninaftncrementltfromlinside.@llentz.execnteReanest7#lonce.ner.actuall.iiTD.callnot onceSummary# QuestiorExisting sleepMATphsyarWnRecommendationMap RateLimitException → HTTP 429 with Retry-After in CrmController: optionally one in-process retry of min(retryafter. 1) s inside findReplace blind sleep(1) with sleep(min(Se->getRetryAfter(), 30)) keyed on RateLimitException - or better, let it bubble to Hand leHubspotRaantContoctRufantl ond tho othor dirort.COK mothode lootContoctßutd antonortunitulutd aotferountßutd botchboodbhdocte eabobonunct)middloworo inotfontivo for tho moct common noth Douto thom throuch ovocutoPonuoct() co thoy omit Potol initEvcontion Alen roconcidor whoroOk lets make it extremelv simple, Oniv consider aclient.php#l220-229 for+ « CodeClaude Onus 4.7 MediumWN Windsurf Teamo162-22UTE.8io 4 spaces...
|
PhpStorm
|
|
NULL
|
15299
|
|
15298
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15298
|
|
15297
|
Create new PHP Test
iTerm2ShellEditViewSessionSc Create new PHP Test
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(ah)Daily - Platform - in 4 mA100% C47 8• Mon 11 May 9:41:12DEV (docker)• жз181DOCKER0 81DEV (docker)882APP (-zsh)masterJY-20818-move-AJ-reports-to-separated-datadog-metricJY-20773-fix-automated-reports-user-pilot-trackingJY-20157-AJ-report-not-send-notificationJY-20508-notify-before-AJ-report-expirationJY-20372-ai-reports-promotion-pagesJY-20352-sync-opportunities-without-a-local-owner-user-id-is-nullJY-20738-debug-AJ-tracking-UPJY-18909-automated-reports-ask-jiminnyJY-20692-fix-integration-app-[API_KEY] laysJY-20698-fix-SF-activity-types-on-new-playbookJY-20543-AJ-report-trackingJY-20384-handle-auto-sync-with-no-access-to-event-typeJY-20458-ask-jiminny-user-definitionsJY-19666-fix-import-contacts-account-associationJY-19666-HS-import-contacts-and-accounts-batch-jobJY-20458-Ask-Jiminny-ReportsJY-20200-batch-update-CRM-objects-SalesforceJY-19666-HS-webhooks-add-contact-and-companyJY-20348-trigger-setup-DI-layout-on-team-creationJY-20326-refactor-info-message-in-commandJY-20317-fix-auto-log-delay-issue-on-all-channels-disabledJY-20312-remove-on-update-change-last-synced-at-crm-configurationsJY-20306-SF-skip-auto-sync-for-task-based-playbookJY-20192-remove-deleted-team-from-saved-search-filtersJY-20197-import-opportunity-batch-jobJY-20293-enable-status-field-for-pipedrive-dealsJY-20191-remove-commands-interactive-promptsJY-20118-change-default-sync-strategyJY-20183-add-cache-on-auto-log-delayJY-20197-add-import-opportunity-batch-job20118-hs-opportunity-make-webhook-strategy-defaultJY-20118-make-default-hs-opportunity-sync-strategy-webhook-basedJY-20196-handle-opportunity-without-noteJY-20118-improve-opportunity-importJY-20189-handle-activity-search-on-deleted-groupsJY-20160JY-20145-filter-out-converted-leads-when-matchingJY-20150-skip-push-summary-on-summary-ready-1f-autologJY-20132-fix-note-encodingJY-19792-clean-logslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20725-handle-HS-search-rate-limit) $ devroot@docker_lamp_1:/home/jiminny# ]-zsh-zsh885screenpipe"0 ₴6DEV...
|
PhpStorm
|
|
NULL
|
15297
|
|
15296
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15296
|
|
15295
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
12
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
if ($state->requestCount > 0) {
usleep($delay);
}
$page = $this->executeSearchRequest($client, $type, $payload, $state);
$state->setTotal($page['total'] ?? 0);
$this->updateLastRecordId($page, $state);
// Safely iterate over results with null check
$results = $page['results'] ?? [];
foreach ($results as $row) {
$state->incrementTotalRecords();
yield $row;
}
$state->setOffset($this->getNextOffset($page));
$state->incrementRequestCount();
$this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
}
// RateLimitException and other exceptions are re-thrown as-is
throw $e;
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
NULL
|
15295
|