@gscdump/sdk 0.23.4 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +179 -1
- package/dist/index.mjs +395 -62
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -636,6 +636,27 @@ interface SearchConsoleStageSummary {
|
|
|
636
636
|
change7d?: number | null;
|
|
637
637
|
change28d?: number | null;
|
|
638
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Trajectory + maturity signals (v2). These are first-class axes: a site that
|
|
641
|
+
* is growing over the robust 90-day window is told to keep expanding, never to
|
|
642
|
+
* "fix indexing", regardless of coverage%. Percent fields are whole numbers
|
|
643
|
+
* (e.g. 42.6 for +42.6%); `positionDelta90d` is current − prior (negative =
|
|
644
|
+
* rank improved). Window contract: callers MUST drop the trailing ~3 GSC lag
|
|
645
|
+
* days before computing these, and the 7-day window is intentionally absent —
|
|
646
|
+
* it is too lag-contaminated to classify on.
|
|
647
|
+
*/
|
|
648
|
+
interface SearchConsoleStageTrajectory {
|
|
649
|
+
clicksPct90d?: number | null;
|
|
650
|
+
impressionsPct90d?: number | null;
|
|
651
|
+
positionDelta90d?: number | null;
|
|
652
|
+
clicksPct28d?: number | null;
|
|
653
|
+
/**
|
|
654
|
+
* Absolute clicks in the PRIOR 28-day window — the baseline a decline would
|
|
655
|
+
* be measured against. Gates decline detection so a percentage crash on
|
|
656
|
+
* trivial traffic (8 → 2 clicks) is not mistaken for a real loss.
|
|
657
|
+
*/
|
|
658
|
+
clicksPrior28d?: number | null;
|
|
659
|
+
}
|
|
639
660
|
interface SearchConsoleStageSitemap {
|
|
640
661
|
errors?: number | null;
|
|
641
662
|
warnings?: number | null;
|
|
@@ -657,8 +678,165 @@ interface ClassifySearchConsoleStageInput {
|
|
|
657
678
|
pageInventory?: SearchConsoleStagePage[] | null;
|
|
658
679
|
ctrOutlierCount?: number | null;
|
|
659
680
|
pageMoverDropCount?: number | null;
|
|
681
|
+
/** v2 trajectory axis — when present, drives the growth override + decline detection. */
|
|
682
|
+
trajectory?: SearchConsoleStageTrajectory | null;
|
|
683
|
+
/** v2 maturity axis — impressions over the trailing 28 days. Gates whether coverage% is even meaningful. */
|
|
684
|
+
impressions28d?: number | null;
|
|
685
|
+
/**
|
|
686
|
+
* v2 on-page technical faults from the crawl audit (broken links/images,
|
|
687
|
+
* server errors, access failures) — counted as hard blockers alongside GSC
|
|
688
|
+
* crawl reasons. Excludes intentional noindex.
|
|
689
|
+
*/
|
|
690
|
+
crawlAuditBlockerCount?: number | null;
|
|
691
|
+
/** v2 authority signal — open recoverable broken backlinks (expansion lever, not a defect). */
|
|
692
|
+
recoverableBacklinkCount?: number | null;
|
|
693
|
+
/** v2 authority signal — cross-competitor content-gap topics (expansion readiness). */
|
|
694
|
+
competitorGapCount?: number | null;
|
|
695
|
+
/**
|
|
696
|
+
* v2 site purpose (AI profile `type`). Benchmarks the verdict against intent:
|
|
697
|
+
* informational types (docs/blog/portfolio) earn structurally low CTR, so the
|
|
698
|
+
* visible-not-clicked bar is raised for them. Unknown/null → `other`.
|
|
699
|
+
*/
|
|
700
|
+
siteType?: string | null;
|
|
660
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* v2 classifier. Trajectory and maturity are first-class axes that run BEFORE
|
|
704
|
+
* the coverage/discovery rungs, so a growing site is told to keep expanding —
|
|
705
|
+
* never to "fix indexing". Reuses the existing stage-key enum (growth →
|
|
706
|
+
* `healthy_growth_ready`, nascent → `waiting_for_data`, mass crawled-not-indexed
|
|
707
|
+
* → `index_rejection`, on-page/crawl faults → `crawl_blocked`).
|
|
708
|
+
*/
|
|
661
709
|
declare function classifySearchConsoleStage(input: ClassifySearchConsoleStageInput): SearchConsoleStage;
|
|
710
|
+
type SiteType = 'saas' | 'ecommerce' | 'docs' | 'blog' | 'agency' | 'portfolio' | 'other';
|
|
711
|
+
/**
|
|
712
|
+
* Normalise the AI profile `type` to the closed enum. The categoriser mostly
|
|
713
|
+
* emits the 7 values but occasionally leaks free-text (e.g. "event") or null
|
|
714
|
+
* (~40% of live sites are unprofiled) — everything unknown collapses to `other`
|
|
715
|
+
* so downstream logic always has a defined bucket.
|
|
716
|
+
*/
|
|
717
|
+
declare function normalizeSiteType(raw: string | null | undefined): SiteType;
|
|
718
|
+
interface SiteTypeBaseline {
|
|
719
|
+
label: string;
|
|
720
|
+
/**
|
|
721
|
+
* Whether indexed-coverage% is a meaningful health signal for this type.
|
|
722
|
+
* docs/blog/portfolio accumulate intentional low-value pages (tags, versions,
|
|
723
|
+
* pagination, archives) so coverage% is noise; ecommerce/saas/agency care.
|
|
724
|
+
*/
|
|
725
|
+
coverageMatters: boolean;
|
|
726
|
+
/**
|
|
727
|
+
* Typical click-through at good positions. Informational types (docs/blog)
|
|
728
|
+
* earn structurally lower CTR (answer shown in SERP, multi-page research), so
|
|
729
|
+
* a low CTR is NOT a defect — the classifier raises the visible-not-clicked
|
|
730
|
+
* bar for these.
|
|
731
|
+
*/
|
|
732
|
+
ctrExpectation: 'low' | 'medium' | 'high';
|
|
733
|
+
/** The default growth lever when the site is healthy and leading. */
|
|
734
|
+
primaryGoal: 'content' | 'authority' | 'conversion' | 'coverage';
|
|
735
|
+
}
|
|
736
|
+
declare const SITE_TYPE_BASELINE: Record<SiteType, SiteTypeBaseline>;
|
|
737
|
+
declare function siteTypeBaseline(raw: string | null | undefined): SiteTypeBaseline;
|
|
738
|
+
type PeerStanding = 'leader' | 'on_par' | 'behind' | 'unknown';
|
|
739
|
+
/** How much to trust the standing: a median over 1–2 peers is noisy. */
|
|
740
|
+
type PeerConfidence = 'high' | 'low' | 'none';
|
|
741
|
+
/**
|
|
742
|
+
* Peer metrics from tracked competitors (`siteCompetitors`). Domain rank is the
|
|
743
|
+
* DataForSEO-derived 0–100 score (`min(100, log10(keywordCount)*20)`), NOT a
|
|
744
|
+
* true authority metric — good enough for relative standing within a peer set.
|
|
745
|
+
* The site's OWN metrics are not stored alongside competitors, so callers must
|
|
746
|
+
* supply them (one cheap cached `estimateDomainTraffic` call).
|
|
747
|
+
*/
|
|
748
|
+
interface PeerBaselineInput {
|
|
749
|
+
siteDomainRank?: number | null;
|
|
750
|
+
siteOrganicTraffic?: number | null;
|
|
751
|
+
competitorDomainRanks?: number[] | null;
|
|
752
|
+
competitorOrganicTraffic?: number[] | null;
|
|
753
|
+
}
|
|
754
|
+
interface SiteBaseline {
|
|
755
|
+
siteType: SiteType;
|
|
756
|
+
baseline: SiteTypeBaseline;
|
|
757
|
+
peerStanding: PeerStanding;
|
|
758
|
+
peerConfidence: PeerConfidence;
|
|
759
|
+
peerMedianDomainRank: number | null;
|
|
760
|
+
peerMedianOrganicTraffic: number | null;
|
|
761
|
+
/** Human-facing goal headline combining type + standing. */
|
|
762
|
+
recommendedGoal: string;
|
|
763
|
+
goalKind: SiteTypeBaseline['primaryGoal'];
|
|
764
|
+
}
|
|
765
|
+
/** Standing is decided on domain rank first; organic traffic breaks the tie. */
|
|
766
|
+
declare function derivePeerStanding(input: PeerBaselineInput): {
|
|
767
|
+
standing: PeerStanding;
|
|
768
|
+
confidence: PeerConfidence;
|
|
769
|
+
peerMedianDomainRank: number | null;
|
|
770
|
+
peerMedianOrganicTraffic: number | null;
|
|
771
|
+
};
|
|
772
|
+
/**
|
|
773
|
+
* Combine the site type with its peer standing into a relative goal. A leader
|
|
774
|
+
* defends and expands on its type's lever; a site that's behind closes the gap
|
|
775
|
+
* on the same lever; unknown standing falls back to the type default.
|
|
776
|
+
*/
|
|
777
|
+
declare function deriveSiteBaseline(rawType: string | null | undefined, peer?: PeerBaselineInput): SiteBaseline;
|
|
778
|
+
type ReachStage = 'waiting_for_data' | 'emerging' | 'growing' | 'plateaued' | 'declining' | 'faded' | 'decayed';
|
|
779
|
+
type HealthStage = 'healthy' | 'crawl_faults' | 'quality_rejection';
|
|
780
|
+
interface TriageEvidence {
|
|
781
|
+
label: string;
|
|
782
|
+
value: string;
|
|
783
|
+
}
|
|
784
|
+
interface ReachVerdict {
|
|
785
|
+
stage: ReachStage;
|
|
786
|
+
summary: string;
|
|
787
|
+
primaryAction: string;
|
|
788
|
+
evidence: TriageEvidence[];
|
|
789
|
+
}
|
|
790
|
+
interface HealthVerdict {
|
|
791
|
+
stage: HealthStage;
|
|
792
|
+
summary: string;
|
|
793
|
+
primaryAction: string;
|
|
794
|
+
evidence: TriageEvidence[];
|
|
795
|
+
}
|
|
796
|
+
interface SiteTriage {
|
|
797
|
+
reach: ReachVerdict;
|
|
798
|
+
health: HealthVerdict;
|
|
799
|
+
/** Which axis leads the dashboard headline. */
|
|
800
|
+
headline: 'reach' | 'health';
|
|
801
|
+
}
|
|
802
|
+
interface SiteTriageInput {
|
|
803
|
+
connected: boolean;
|
|
804
|
+
/** Impressions over the trailing 28 days — the maturity tier. */
|
|
805
|
+
impressions28d?: number | null;
|
|
806
|
+
/** Lifetime-ish impressions (trailing 12 months) — separates new from decayed/faded. */
|
|
807
|
+
impressions12m?: number | null;
|
|
808
|
+
/** Absolute clicks over the trailing 28 days — for the clicks≪impressions decay tell. */
|
|
809
|
+
clicks28d?: number | null;
|
|
810
|
+
clicksPct90d?: number | null;
|
|
811
|
+
clicksPct28d?: number | null;
|
|
812
|
+
/** Absolute prior-28d clicks — gates decline so a % crash on trivial traffic isn't a false decline. */
|
|
813
|
+
clicksPrior28d?: number | null;
|
|
814
|
+
impressionsPct90d?: number | null;
|
|
815
|
+
positionDelta90d?: number | null;
|
|
816
|
+
/** Latest complete week ÷ 90d peak week (lag-trimmed). <0.2 = faded (spike→died). */
|
|
817
|
+
livenessRatio?: number | null;
|
|
818
|
+
totalUrls?: number | null;
|
|
819
|
+
indexed?: number | null;
|
|
820
|
+
issues?: SearchConsoleStageIssue[] | null;
|
|
821
|
+
/** Real on-page faults from the crawl audit: 5xx, broken internal links/images. Excludes intentional noindex/404. */
|
|
822
|
+
crawlAuditBlockerCount?: number | null;
|
|
823
|
+
/** AI profile type — drives purpose-expected subtraction. */
|
|
824
|
+
siteType?: string | null;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Reach liveness: latest complete week ÷ peak rolling-7d week over a daily
|
|
828
|
+
* impressions series (typically the trailing 90 days). `<0.2` means recent
|
|
829
|
+
* impressions have collapsed versus the site's own peak (spike→died) — the
|
|
830
|
+
* signal a period-vs-prior delta cannot see. Trailing zero-impression days
|
|
831
|
+
* (GSC reporting lag) are trimmed before the latest-week sum. Returns null when
|
|
832
|
+
* the series is too short to judge.
|
|
833
|
+
*/
|
|
834
|
+
declare function reachLivenessRatio(daily: Array<{
|
|
835
|
+
impressions: number;
|
|
836
|
+
}> | null | undefined): number | null;
|
|
837
|
+
declare function classifyHealthStage(input: SiteTriageInput): HealthVerdict;
|
|
838
|
+
declare function classifyReachStage(input: SiteTriageInput): ReachVerdict;
|
|
839
|
+
declare function classifySiteTriage(input: SiteTriageInput): SiteTriage;
|
|
662
840
|
declare function serializeWebhookPayload(payload: string | object): string;
|
|
663
841
|
declare function verifyWebhookSignature(payload: string | object, signature: string | null | undefined, secret: string): Promise<boolean>;
|
|
664
842
|
declare function parseWebhookPayload<TData extends Record<string, unknown> = Record<string, unknown>>(payload: string | object, options?: {
|
|
@@ -668,4 +846,4 @@ declare function parseWebhookPayload<TData extends Record<string, unknown> = Rec
|
|
|
668
846
|
validateSignature?: boolean;
|
|
669
847
|
}): Promise<WebhookEnvelope$1<TData>>;
|
|
670
848
|
declare function readWebhookHeaders(headers: Headers | PartnerWebhookHeaders$1 | null | undefined): Required<PartnerWebhookHeaders$1>;
|
|
671
|
-
export { ARCHETYPE_EXECUTION_CLASS, type AddPartnerTeamMemberParams, type AnalyticsClient, type AnalyticsClientOptions, type AnalyticsFetch, type AnalyticsFetchOptions, type AnalyticsHeaders, type ArbitrarySqlQuery, type ArchetypeExecutionClass, type ArchetypeQuery, type ArchetypeQueryBase, type ArchetypeResult, type ArchetypeResultRow, type ArchetypeResultSource, type AuxCloudOnlyQuery, type BackfillRange, type BackfillResponse, type BindPartnerSiteTeamParams, type BuilderState, type BulkRegisterPartnerSiteResult, type BulkRegisterPartnerSitesParams, type BulkRegisterPartnerSitesResponse, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, type CalendarPeriod, type CanonicalDailyRow, type CanonicalWebhookEventType, type ClassifySearchConsoleStageInput, type CompareMode, type CreatePartnerTeamParams, type CreateWebhookEnvelopeOptions, type CustomPeriod, type CwvBucket, type DailyAnonInput, type DataDetailOptions, type DataQueryOptions, type DateRange, type DateRangeResult, type DeletePartnerUserResponse, type EntityDailySparklineQuery, type EntityDailyTimeseriesQuery, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, type GscAnalyzerAccent, type GscAnalyzerCapabilities, type GscAnalyzerCapability, type GscAnalyzerDefinition, type GscAnalyzerDefinitionWithCapability, type GscAnalyzerInsightCard, type GscAnalyzerKind, type GscAnalyzerPanelResult, type GscAnalyzerPanelSpec, type GscAnalyzerStatTile, type GscClassifiedError, type GscColumn, type GscColumnOption, type GscComparisonFilter, type GscConsoleUrlOpts, type GscDailySummary, type GscErrorStatus, type GscRowTotals, type GscdumpAnalysisParams, type GscdumpAnalysisPreset, type GscdumpAnalysisResponse, type GscdumpAnalysisSourcesResponse, type GscdumpAvailableSite, type GscdumpCanonicalMismatchesResponse, type GscdumpDataDetailResponse, type GscdumpDataResponse, type GscdumpDataRow, type GscdumpDateRangeParams, type GscdumpIndexPercentResponse, type GscdumpIndexingDiagnosticsResponse, type GscdumpIndexingResponse, type GscdumpIndexingUrlStatus, type GscdumpIndexingUrlsResponse, type GscdumpKeywordSparklinesParams, type GscdumpKeywordSparklinesResponse, type GscdumpMeta, type GscdumpPermissionRecovery, type GscdumpQueryTrendParams, type GscdumpQueryTrendResponse, type GscdumpSiteRegistration, type GscdumpSitemap, type GscdumpSitemapChangesResponse, type GscdumpSitemapHistory, type GscdumpSitemapsResponse, type GscdumpSyncStatusResponse, type GscdumpTeamMemberRow, type GscdumpTeamRow, type GscdumpTopAssociationParams, type GscdumpTopAssociationResponse, type GscdumpTotals, type GscdumpUserRegistration, type GscdumpUserSettings, type GscdumpUserSite, type GscdumpUserStatus, type GscdumpUserTokenUpdate, type IndexingIssue, type IndexingIssueDetail, type IndexingUrlsParams, type InspectionHistoryResponse, type InspectionIndex, type IssueGroup, type IssueSeverity, type MultiSeriesStackedDailyQuery, PERIOD_PRESETS, PartnerApiError, type PartnerClient, type PartnerClientOptions, type PartnerErrorInfo, type PartnerErrorKind, type PartnerFetch, type PartnerFetchOptions, type PartnerHeaders, type PartnerLifecycleAccount, type PartnerLifecycleResponse, type PartnerLifecycleSite, type PartnerRealtimeClient, type PartnerRealtimeEvent, type PartnerRealtimeEventType, type PartnerRealtimeHandler, type PartnerRealtimeMessage, type PartnerRealtimeOptions, type PartnerRealtimeScope, type PartnerRealtimeStatus, type PartnerWebSocketConstructor, type PartnerWebSocketLike, type PartnerWebhookData, type PartnerWebhookHeaders, type Period, type PeriodPreset, type PresetAnalyzerQuery, type QueryArchetype, type RawDailyRow, type RealtimeAuthFailedEvent, type RealtimeAuthRequiredMessage, type RealtimeConnectedMessage, type RealtimeEnrichmentCompleteEvent, type RealtimeErrorMessage, type RealtimeJobFailedEvent, type RealtimeNeedsReauthEvent, type RealtimePongMessage, type RealtimeSiteAddedEvent, type RealtimeSiteRemovedEvent, type RealtimeSubscribedMessage, type RealtimeSyncCompleteEvent, type RealtimeSyncFailedEvent, type RealtimeSyncJobCompleteEvent, type RealtimeSyncProgressEvent, type RealtimeSyncSiteCompleteEvent, type RegisterPartnerSiteParams, type RegisterPartnerUserParams, type ResolvedArchetypeQuery, type RollingPeriod, type RollupEnvelope, type SearchConsoleStage, type SearchConsoleStageEvidence, type SearchConsoleStageIssue, type SearchConsoleStageKey, type SearchConsoleStagePage, type SearchConsoleStageSeverity, type SearchConsoleStageSitemap, type SearchConsoleStageSummary, type SingleRowLookupQuery, type SiteDailyTimeseriesQuery, type TopNBreakdownQuery, type TwoDimensionDetailQuery, type UpdatePartnerUserTokensParams, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, type WebhookEnvelope, type WebhookEventType, type WhoamiResponse, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifySearchConsoleStage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
|
|
849
|
+
export { ARCHETYPE_EXECUTION_CLASS, type AddPartnerTeamMemberParams, type AnalyticsClient, type AnalyticsClientOptions, type AnalyticsFetch, type AnalyticsFetchOptions, type AnalyticsHeaders, type ArbitrarySqlQuery, type ArchetypeExecutionClass, type ArchetypeQuery, type ArchetypeQueryBase, type ArchetypeResult, type ArchetypeResultRow, type ArchetypeResultSource, type AuxCloudOnlyQuery, type BackfillRange, type BackfillResponse, type BindPartnerSiteTeamParams, type BuilderState, type BulkRegisterPartnerSiteResult, type BulkRegisterPartnerSitesParams, type BulkRegisterPartnerSitesResponse, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, type CalendarPeriod, type CanonicalDailyRow, type CanonicalWebhookEventType, type ClassifySearchConsoleStageInput, type CompareMode, type CreatePartnerTeamParams, type CreateWebhookEnvelopeOptions, type CustomPeriod, type CwvBucket, type DailyAnonInput, type DataDetailOptions, type DataQueryOptions, type DateRange, type DateRangeResult, type DeletePartnerUserResponse, type EntityDailySparklineQuery, type EntityDailyTimeseriesQuery, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, type GscAnalyzerAccent, type GscAnalyzerCapabilities, type GscAnalyzerCapability, type GscAnalyzerDefinition, type GscAnalyzerDefinitionWithCapability, type GscAnalyzerInsightCard, type GscAnalyzerKind, type GscAnalyzerPanelResult, type GscAnalyzerPanelSpec, type GscAnalyzerStatTile, type GscClassifiedError, type GscColumn, type GscColumnOption, type GscComparisonFilter, type GscConsoleUrlOpts, type GscDailySummary, type GscErrorStatus, type GscRowTotals, type GscdumpAnalysisParams, type GscdumpAnalysisPreset, type GscdumpAnalysisResponse, type GscdumpAnalysisSourcesResponse, type GscdumpAvailableSite, type GscdumpCanonicalMismatchesResponse, type GscdumpDataDetailResponse, type GscdumpDataResponse, type GscdumpDataRow, type GscdumpDateRangeParams, type GscdumpIndexPercentResponse, type GscdumpIndexingDiagnosticsResponse, type GscdumpIndexingResponse, type GscdumpIndexingUrlStatus, type GscdumpIndexingUrlsResponse, type GscdumpKeywordSparklinesParams, type GscdumpKeywordSparklinesResponse, type GscdumpMeta, type GscdumpPermissionRecovery, type GscdumpQueryTrendParams, type GscdumpQueryTrendResponse, type GscdumpSiteRegistration, type GscdumpSitemap, type GscdumpSitemapChangesResponse, type GscdumpSitemapHistory, type GscdumpSitemapsResponse, type GscdumpSyncStatusResponse, type GscdumpTeamMemberRow, type GscdumpTeamRow, type GscdumpTopAssociationParams, type GscdumpTopAssociationResponse, type GscdumpTotals, type GscdumpUserRegistration, type GscdumpUserSettings, type GscdumpUserSite, type GscdumpUserStatus, type GscdumpUserTokenUpdate, type HealthStage, type HealthVerdict, type IndexingIssue, type IndexingIssueDetail, type IndexingUrlsParams, type InspectionHistoryResponse, type InspectionIndex, type IssueGroup, type IssueSeverity, type MultiSeriesStackedDailyQuery, PERIOD_PRESETS, PartnerApiError, type PartnerClient, type PartnerClientOptions, type PartnerErrorInfo, type PartnerErrorKind, type PartnerFetch, type PartnerFetchOptions, type PartnerHeaders, type PartnerLifecycleAccount, type PartnerLifecycleResponse, type PartnerLifecycleSite, type PartnerRealtimeClient, type PartnerRealtimeEvent, type PartnerRealtimeEventType, type PartnerRealtimeHandler, type PartnerRealtimeMessage, type PartnerRealtimeOptions, type PartnerRealtimeScope, type PartnerRealtimeStatus, type PartnerWebSocketConstructor, type PartnerWebSocketLike, type PartnerWebhookData, type PartnerWebhookHeaders, type PeerBaselineInput, type PeerConfidence, type PeerStanding, type Period, type PeriodPreset, type PresetAnalyzerQuery, type QueryArchetype, type RawDailyRow, type ReachStage, type ReachVerdict, type RealtimeAuthFailedEvent, type RealtimeAuthRequiredMessage, type RealtimeConnectedMessage, type RealtimeEnrichmentCompleteEvent, type RealtimeErrorMessage, type RealtimeJobFailedEvent, type RealtimeNeedsReauthEvent, type RealtimePongMessage, type RealtimeSiteAddedEvent, type RealtimeSiteRemovedEvent, type RealtimeSubscribedMessage, type RealtimeSyncCompleteEvent, type RealtimeSyncFailedEvent, type RealtimeSyncJobCompleteEvent, type RealtimeSyncProgressEvent, type RealtimeSyncSiteCompleteEvent, type RegisterPartnerSiteParams, type RegisterPartnerUserParams, type ResolvedArchetypeQuery, type RollingPeriod, type RollupEnvelope, SITE_TYPE_BASELINE, type SearchConsoleStage, type SearchConsoleStageEvidence, type SearchConsoleStageIssue, type SearchConsoleStageKey, type SearchConsoleStagePage, type SearchConsoleStageSeverity, type SearchConsoleStageSitemap, type SearchConsoleStageSummary, type SearchConsoleStageTrajectory, type SingleRowLookupQuery, type SiteBaseline, type SiteDailyTimeseriesQuery, type SiteTriage, type SiteTriageInput, type SiteType, type SiteTypeBaseline, type TopNBreakdownQuery, type TriageEvidence, type TwoDimensionDetailQuery, type UpdatePartnerUserTokensParams, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, type WebhookEnvelope, type WebhookEventType, type WhoamiResponse, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifyHealthStage, classifyReachStage, classifySearchConsoleStage, classifySiteTriage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, derivePeerStanding, deriveSiteBaseline, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, normalizeSiteType, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, reachLivenessRatio, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteTypeBaseline, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
|
package/dist/index.mjs
CHANGED
|
@@ -414,7 +414,8 @@ function dateRangeQuery(params) {
|
|
|
414
414
|
function queryTrendQuery(params) {
|
|
415
415
|
const query = {
|
|
416
416
|
startDate: params.startDate,
|
|
417
|
-
endDate: params.endDate
|
|
417
|
+
endDate: params.endDate,
|
|
418
|
+
searchType: params.searchType ?? DEFAULT_SEARCH_TYPE
|
|
418
419
|
};
|
|
419
420
|
if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
|
|
420
421
|
if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
|
|
@@ -625,7 +626,11 @@ function createPartnerClient(options = {}) {
|
|
|
625
626
|
return request(partnerRoutes.sites.topAssociation(siteId), { query }, partnerEndpointSchemas.getTopAssociation.response);
|
|
626
627
|
},
|
|
627
628
|
getKeywordSparklines(siteId, params) {
|
|
628
|
-
const
|
|
629
|
+
const withSearchType = {
|
|
630
|
+
...params,
|
|
631
|
+
searchType: params.searchType ?? DEFAULT_SEARCH_TYPE
|
|
632
|
+
};
|
|
633
|
+
const body = shouldValidate(options, "request") ? partnerEndpointSchemas.getKeywordSparklines.body.parse(withSearchType) : withSearchType;
|
|
629
634
|
return request(partnerRoutes.sites.keywordSparklines(siteId), {
|
|
630
635
|
method: "POST",
|
|
631
636
|
body
|
|
@@ -1261,7 +1266,7 @@ const CALENDAR_TO_UPSTREAM = {
|
|
|
1261
1266
|
"this-month": "mtd",
|
|
1262
1267
|
"this-year": "ytd"
|
|
1263
1268
|
};
|
|
1264
|
-
function fmt(d) {
|
|
1269
|
+
function fmt$1(d) {
|
|
1265
1270
|
return format(d, "yyyy-MM-dd");
|
|
1266
1271
|
}
|
|
1267
1272
|
function buildResultFromIso(start, end) {
|
|
@@ -1305,7 +1310,7 @@ function periodToDateRange(period, stableData = true) {
|
|
|
1305
1310
|
}
|
|
1306
1311
|
const today = todayInPST();
|
|
1307
1312
|
const end = stableData ? subDays(today, 3) : subDays(today, 1);
|
|
1308
|
-
const endIso = fmt(end);
|
|
1313
|
+
const endIso = fmt$1(end);
|
|
1309
1314
|
const upstreamPreset = ROLLING_TO_UPSTREAM[period] ?? CALENDAR_TO_UPSTREAM[period];
|
|
1310
1315
|
if (upstreamPreset) {
|
|
1311
1316
|
const win = resolveWindow({
|
|
@@ -1321,14 +1326,14 @@ function periodToDateRange(period, stableData = true) {
|
|
|
1321
1326
|
break;
|
|
1322
1327
|
case "last-month": {
|
|
1323
1328
|
const prevMonth = subMonths(end, 1);
|
|
1324
|
-
return buildResultFromIso(fmt(startOfMonth(prevMonth)), fmt(endOfMonth(prevMonth)));
|
|
1329
|
+
return buildResultFromIso(fmt$1(startOfMonth(prevMonth)), fmt$1(endOfMonth(prevMonth)));
|
|
1325
1330
|
}
|
|
1326
1331
|
case "this-quarter":
|
|
1327
1332
|
start = startOfQuarter(end);
|
|
1328
1333
|
break;
|
|
1329
1334
|
default: start = subDays(end, 27);
|
|
1330
1335
|
}
|
|
1331
|
-
return buildResultFromIso(fmt(start), endIso);
|
|
1336
|
+
return buildResultFromIso(fmt$1(start), endIso);
|
|
1332
1337
|
}
|
|
1333
1338
|
function periodToDays(period) {
|
|
1334
1339
|
return periodToDateRange(period).days;
|
|
@@ -1478,10 +1483,121 @@ function createPartnerRealtimeClient(options) {
|
|
|
1478
1483
|
};
|
|
1479
1484
|
}
|
|
1480
1485
|
const createGscdumpRealtimeClient = createPartnerRealtimeClient;
|
|
1486
|
+
const KNOWN_SITE_TYPES = new Set([
|
|
1487
|
+
"saas",
|
|
1488
|
+
"ecommerce",
|
|
1489
|
+
"docs",
|
|
1490
|
+
"blog",
|
|
1491
|
+
"agency",
|
|
1492
|
+
"portfolio",
|
|
1493
|
+
"other"
|
|
1494
|
+
]);
|
|
1495
|
+
function normalizeSiteType(raw) {
|
|
1496
|
+
if (!raw) return "other";
|
|
1497
|
+
const t = raw.toLowerCase().trim();
|
|
1498
|
+
return KNOWN_SITE_TYPES.has(t) ? t : "other";
|
|
1499
|
+
}
|
|
1500
|
+
const SITE_TYPE_BASELINE = {
|
|
1501
|
+
docs: {
|
|
1502
|
+
label: "Documentation",
|
|
1503
|
+
coverageMatters: false,
|
|
1504
|
+
ctrExpectation: "low",
|
|
1505
|
+
primaryGoal: "content"
|
|
1506
|
+
},
|
|
1507
|
+
blog: {
|
|
1508
|
+
label: "Blog / content",
|
|
1509
|
+
coverageMatters: false,
|
|
1510
|
+
ctrExpectation: "low",
|
|
1511
|
+
primaryGoal: "content"
|
|
1512
|
+
},
|
|
1513
|
+
portfolio: {
|
|
1514
|
+
label: "Portfolio",
|
|
1515
|
+
coverageMatters: false,
|
|
1516
|
+
ctrExpectation: "low",
|
|
1517
|
+
primaryGoal: "content"
|
|
1518
|
+
},
|
|
1519
|
+
saas: {
|
|
1520
|
+
label: "SaaS",
|
|
1521
|
+
coverageMatters: true,
|
|
1522
|
+
ctrExpectation: "medium",
|
|
1523
|
+
primaryGoal: "authority"
|
|
1524
|
+
},
|
|
1525
|
+
agency: {
|
|
1526
|
+
label: "Agency",
|
|
1527
|
+
coverageMatters: true,
|
|
1528
|
+
ctrExpectation: "medium",
|
|
1529
|
+
primaryGoal: "authority"
|
|
1530
|
+
},
|
|
1531
|
+
ecommerce: {
|
|
1532
|
+
label: "Ecommerce",
|
|
1533
|
+
coverageMatters: true,
|
|
1534
|
+
ctrExpectation: "high",
|
|
1535
|
+
primaryGoal: "coverage"
|
|
1536
|
+
},
|
|
1537
|
+
other: {
|
|
1538
|
+
label: "General",
|
|
1539
|
+
coverageMatters: true,
|
|
1540
|
+
ctrExpectation: "medium",
|
|
1541
|
+
primaryGoal: "content"
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
function siteTypeBaseline(raw) {
|
|
1545
|
+
return SITE_TYPE_BASELINE[normalizeSiteType(raw)];
|
|
1546
|
+
}
|
|
1547
|
+
const MIN_CONFIDENT_PEERS = 3;
|
|
1548
|
+
const LEADER_RATIO = 1.15;
|
|
1549
|
+
const BEHIND_RATIO = .85;
|
|
1550
|
+
function median(values) {
|
|
1551
|
+
if (!values || values.length === 0) return null;
|
|
1552
|
+
const xs = [...values].sort((a, b) => a - b);
|
|
1553
|
+
const mid = Math.floor(xs.length / 2);
|
|
1554
|
+
return xs.length % 2 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2;
|
|
1555
|
+
}
|
|
1556
|
+
function derivePeerStanding(input) {
|
|
1557
|
+
const peerMedianDomainRank = median(input.competitorDomainRanks);
|
|
1558
|
+
const peerMedianOrganicTraffic = median(input.competitorOrganicTraffic);
|
|
1559
|
+
const peerCount = Math.max(input.competitorDomainRanks?.length ?? 0, input.competitorOrganicTraffic?.length ?? 0);
|
|
1560
|
+
const confidence = peerCount === 0 ? "none" : peerCount >= MIN_CONFIDENT_PEERS ? "high" : "low";
|
|
1561
|
+
const compare = (site, peer) => {
|
|
1562
|
+
if (site == null || peer == null || peer === 0) return null;
|
|
1563
|
+
const ratio = site / peer;
|
|
1564
|
+
if (ratio >= LEADER_RATIO) return "leader";
|
|
1565
|
+
if (ratio <= BEHIND_RATIO) return "behind";
|
|
1566
|
+
return "on_par";
|
|
1567
|
+
};
|
|
1568
|
+
return {
|
|
1569
|
+
standing: compare(input.siteDomainRank, peerMedianDomainRank) ?? compare(input.siteOrganicTraffic, peerMedianOrganicTraffic) ?? "unknown",
|
|
1570
|
+
confidence,
|
|
1571
|
+
peerMedianDomainRank,
|
|
1572
|
+
peerMedianOrganicTraffic
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
const GOAL_HEADLINE = {
|
|
1576
|
+
content: "publish & expand content",
|
|
1577
|
+
authority: "build authority & backlinks",
|
|
1578
|
+
conversion: "lift conversion & CTR",
|
|
1579
|
+
coverage: "expand indexable catalogue"
|
|
1580
|
+
};
|
|
1581
|
+
function deriveSiteBaseline(rawType, peer = {}) {
|
|
1582
|
+
const siteType = normalizeSiteType(rawType);
|
|
1583
|
+
const baseline = SITE_TYPE_BASELINE[siteType];
|
|
1584
|
+
const { standing, confidence, peerMedianDomainRank, peerMedianOrganicTraffic } = derivePeerStanding(peer);
|
|
1585
|
+
const lever = GOAL_HEADLINE[baseline.primaryGoal];
|
|
1586
|
+
return {
|
|
1587
|
+
siteType,
|
|
1588
|
+
baseline,
|
|
1589
|
+
peerStanding: standing,
|
|
1590
|
+
peerConfidence: confidence,
|
|
1591
|
+
peerMedianDomainRank,
|
|
1592
|
+
peerMedianOrganicTraffic,
|
|
1593
|
+
recommendedGoal: standing === "behind" ? `Close the gap on peers — ${lever}` : standing === "leader" ? `Defend the lead — ${lever}` : standing === "on_par" ? `Pull ahead of peers — ${lever}` : `${baseline.label}: ${lever}`,
|
|
1594
|
+
goalKind: baseline.primaryGoal
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1481
1597
|
function formatCount(value) {
|
|
1482
1598
|
return new Intl.NumberFormat("en").format(Math.max(0, Math.round(value)));
|
|
1483
1599
|
}
|
|
1484
|
-
function issueCount(issues, ...types) {
|
|
1600
|
+
function issueCount$1(issues, ...types) {
|
|
1485
1601
|
if (!issues?.length) return 0;
|
|
1486
1602
|
const wanted = new Set(types);
|
|
1487
1603
|
return issues.reduce((sum, issue) => sum + (wanted.has(issue.type) ? issue.count : 0), 0);
|
|
@@ -1607,6 +1723,12 @@ function stage(key, evidence) {
|
|
|
1607
1723
|
}[key]
|
|
1608
1724
|
};
|
|
1609
1725
|
}
|
|
1726
|
+
const ESTABLISHED_IMPRESSIONS_28D$1 = 2e4;
|
|
1727
|
+
const NASCENT_IMPRESSIONS_28D$1 = 1e3;
|
|
1728
|
+
const GROWTH_CLICKS_PCT_90D = 10;
|
|
1729
|
+
const GROWTH_IMPRESSIONS_PCT_90D = 20;
|
|
1730
|
+
const DECLINE_CLICKS_PCT$1 = -10;
|
|
1731
|
+
const MIN_DECLINE_PRIOR_CLICKS$1 = 50;
|
|
1610
1732
|
function classifySearchConsoleStage(input) {
|
|
1611
1733
|
const issues = input.issues ?? [];
|
|
1612
1734
|
const summary = input.summary ?? null;
|
|
@@ -1626,33 +1748,84 @@ function classifySearchConsoleStage(input) {
|
|
|
1626
1748
|
source: "indexing"
|
|
1627
1749
|
}]);
|
|
1628
1750
|
const sitemapErrors = totalSitemapErrors(sitemaps);
|
|
1629
|
-
const
|
|
1630
|
-
const
|
|
1631
|
-
const
|
|
1632
|
-
const
|
|
1633
|
-
const
|
|
1634
|
-
const indexBlocks = issueCount(issues, "noindex", "canonical_mismatch");
|
|
1635
|
-
const canonicalMismatches = input.canonicalMismatchCount ?? issueCount(issues, "canonical_mismatch");
|
|
1636
|
-
const zeroImpressionPages = (input.pageInventory ?? []).filter((page) => page.impressions === 0).length;
|
|
1751
|
+
const unknown = issueCount$1(issues, "unknown_to_google");
|
|
1752
|
+
const discovered = issueCount$1(issues, "discovered_not_indexed");
|
|
1753
|
+
const crawled = issueCount$1(issues, "crawled_not_indexed");
|
|
1754
|
+
const hardBlocks = issueCount$1(issues, "blocked_robots", "server_error", "access_denied", "forbidden") + (input.crawlAuditBlockerCount ?? 0);
|
|
1755
|
+
const canonicalMismatches = input.canonicalMismatchCount ?? issueCount$1(issues, "canonical_mismatch");
|
|
1637
1756
|
const visibleNoClickPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && page.clicks === 0).length;
|
|
1638
1757
|
const poorPositionPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && (page.position ?? 0) > 20).length;
|
|
1639
|
-
const pageMoverDropCount = input.pageMoverDropCount ?? 0;
|
|
1640
1758
|
const ctrOutlierCount = input.ctrOutlierCount ?? 0;
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1759
|
+
const traj = input.trajectory ?? null;
|
|
1760
|
+
const impressions28d = input.impressions28d ?? null;
|
|
1761
|
+
const clicks90d = traj?.clicksPct90d ?? null;
|
|
1762
|
+
const imp90d = traj?.impressionsPct90d ?? null;
|
|
1763
|
+
const posDelta90d = traj?.positionDelta90d ?? null;
|
|
1764
|
+
const clicks28d = traj?.clicksPct28d ?? null;
|
|
1765
|
+
const clicksPrior28d = traj?.clicksPrior28d ?? null;
|
|
1766
|
+
const isGrowing = clicks90d != null && clicks90d > GROWTH_CLICKS_PCT_90D || imp90d != null && imp90d > GROWTH_IMPRESSIONS_PCT_90D && posDelta90d != null && posDelta90d < 0;
|
|
1767
|
+
const isDeclining = clicks90d != null && clicks90d < DECLINE_CLICKS_PCT$1 && clicks28d != null && clicks28d < DECLINE_CLICKS_PCT$1 && clicksPrior28d != null && clicksPrior28d >= MIN_DECLINE_PRIOR_CLICKS$1;
|
|
1768
|
+
const isNascent = impressions28d != null && impressions28d < NASCENT_IMPRESSIONS_28D$1;
|
|
1769
|
+
const isEstablished = impressions28d != null && impressions28d >= ESTABLISHED_IMPRESSIONS_28D$1;
|
|
1770
|
+
if (hardBlocks > Math.max(10, totalUrls * .05)) return stage("crawl_blocked", [{
|
|
1771
|
+
label: "Crawl / on-page faults",
|
|
1772
|
+
value: formatCount(hardBlocks),
|
|
1644
1773
|
source: "indexing"
|
|
1645
1774
|
}, {
|
|
1646
1775
|
label: "Indexed pages",
|
|
1647
1776
|
value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
|
|
1648
1777
|
source: "indexing"
|
|
1649
1778
|
}]);
|
|
1650
|
-
if (
|
|
1651
|
-
label: "
|
|
1652
|
-
value:
|
|
1779
|
+
if (isDeclining) return stage("declining_visibility", [{
|
|
1780
|
+
label: "Clicks 90d",
|
|
1781
|
+
value: `${clicks90d.toFixed(1)}%`,
|
|
1782
|
+
source: "performance"
|
|
1783
|
+
}, {
|
|
1784
|
+
label: "Clicks 28d",
|
|
1785
|
+
value: `${clicks28d.toFixed(1)}%`,
|
|
1786
|
+
source: "performance"
|
|
1787
|
+
}]);
|
|
1788
|
+
if (isGrowing) return stage("healthy_growth_ready", [
|
|
1789
|
+
...clicks90d != null ? [{
|
|
1790
|
+
label: "Clicks 90d",
|
|
1791
|
+
value: `+${clicks90d.toFixed(1)}%`,
|
|
1792
|
+
source: "performance"
|
|
1793
|
+
}] : [],
|
|
1794
|
+
...imp90d != null ? [{
|
|
1795
|
+
label: "Impressions 90d",
|
|
1796
|
+
value: `+${imp90d.toFixed(1)}%`,
|
|
1797
|
+
source: "performance"
|
|
1798
|
+
}] : [],
|
|
1799
|
+
...(input.recoverableBacklinkCount ?? 0) > 0 ? [{
|
|
1800
|
+
label: "Recoverable backlinks",
|
|
1801
|
+
value: formatCount(input.recoverableBacklinkCount),
|
|
1802
|
+
source: "performance"
|
|
1803
|
+
}] : [],
|
|
1804
|
+
...(input.competitorGapCount ?? 0) > 0 ? [{
|
|
1805
|
+
label: "Competitor content gaps",
|
|
1806
|
+
value: formatCount(input.competitorGapCount),
|
|
1807
|
+
source: "performance"
|
|
1808
|
+
}] : []
|
|
1809
|
+
]);
|
|
1810
|
+
if (crawled > Math.max(10, totalUrls * .3) && totalUrls > 500) return stage("index_rejection", [{
|
|
1811
|
+
label: "Crawled, not indexed",
|
|
1812
|
+
value: formatCount(crawled),
|
|
1813
|
+
source: "indexing"
|
|
1814
|
+
}, {
|
|
1815
|
+
label: "Not indexed",
|
|
1816
|
+
value: formatCount(notIndexed),
|
|
1817
|
+
source: "indexing"
|
|
1818
|
+
}]);
|
|
1819
|
+
if (isNascent) return stage("waiting_for_data", [{
|
|
1820
|
+
label: "Impressions (28d)",
|
|
1821
|
+
value: formatCount(impressions28d ?? 0),
|
|
1653
1822
|
source: "performance"
|
|
1823
|
+
}, {
|
|
1824
|
+
label: "Indexed pages",
|
|
1825
|
+
value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
|
|
1826
|
+
source: "indexing"
|
|
1654
1827
|
}]);
|
|
1655
|
-
if (sitemaps.length === 0 ||
|
|
1828
|
+
if (!isEstablished && (sitemaps.length === 0 || sitemapErrors > 0 || unknown > Math.max(5, totalUrls * .1))) return stage("weak_discovery", [{
|
|
1656
1829
|
label: "Sitemaps",
|
|
1657
1830
|
value: sitemaps.length === 0 ? "None registered" : `${formatCount(sitemapErrors)} errors`,
|
|
1658
1831
|
source: "sitemap"
|
|
@@ -1670,48 +1843,15 @@ function classifySearchConsoleStage(input) {
|
|
|
1670
1843
|
value: `${indexedPercent.toFixed(1)}%`,
|
|
1671
1844
|
source: "indexing"
|
|
1672
1845
|
}]);
|
|
1673
|
-
if (
|
|
1674
|
-
label: "Crawl blockers",
|
|
1675
|
-
value: formatCount(crawlBlocks),
|
|
1676
|
-
source: "indexing"
|
|
1677
|
-
}, {
|
|
1678
|
-
label: "Indexed pages",
|
|
1679
|
-
value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
|
|
1680
|
-
source: "indexing"
|
|
1681
|
-
}]);
|
|
1682
|
-
if (indexBlocks > Math.max(5, totalUrls * .05) || canonicalMismatches > Math.max(5, totalUrls * .05)) return stage("indexability_blocked", [...indexBlocks > 0 ? [{
|
|
1683
|
-
label: "Index signal blockers",
|
|
1684
|
-
value: formatCount(indexBlocks),
|
|
1685
|
-
source: "indexing"
|
|
1686
|
-
}] : [], ...canonicalMismatches > 0 ? [{
|
|
1846
|
+
if (canonicalMismatches > Math.max(5, totalUrls * .05)) return stage("indexability_blocked", [{
|
|
1687
1847
|
label: "Canonical mismatches",
|
|
1688
1848
|
value: formatCount(canonicalMismatches),
|
|
1689
1849
|
source: "canonical"
|
|
1690
|
-
}] : []]);
|
|
1691
|
-
if (crawled > Math.max(10, totalUrls * .15)) return stage("index_rejection", [{
|
|
1692
|
-
label: "Crawled, not indexed",
|
|
1693
|
-
value: formatCount(crawled),
|
|
1694
|
-
source: "indexing"
|
|
1695
|
-
}, {
|
|
1696
|
-
label: "Not indexed",
|
|
1697
|
-
value: formatCount(notIndexed),
|
|
1698
|
-
source: "indexing"
|
|
1699
1850
|
}]);
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
}, {
|
|
1705
|
-
label: "Not indexed",
|
|
1706
|
-
value: formatCount(notIndexed),
|
|
1707
|
-
source: "indexing"
|
|
1708
|
-
}]);
|
|
1709
|
-
if (zeroImpressionPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("indexed_invisible", [{
|
|
1710
|
-
label: "No-impression pages",
|
|
1711
|
-
value: formatCount(zeroImpressionPages),
|
|
1712
|
-
source: "performance"
|
|
1713
|
-
}]);
|
|
1714
|
-
if (ctrOutlierCount > 0 || visibleNoClickPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("visible_not_clicked", [...ctrOutlierCount > 0 ? [{
|
|
1851
|
+
const lowCtrType = siteTypeBaseline(input.siteType).ctrExpectation === "low";
|
|
1852
|
+
const noClickShare = lowCtrType ? .5 : .25;
|
|
1853
|
+
const noClickTrigger = visibleNoClickPages >= Math.max(1, (input.pageInventory?.length ?? 0) * noClickShare);
|
|
1854
|
+
if (ctrOutlierCount > 0 && !lowCtrType || noClickTrigger) return stage("visible_not_clicked", [...ctrOutlierCount > 0 ? [{
|
|
1715
1855
|
label: "CTR outliers",
|
|
1716
1856
|
value: formatCount(ctrOutlierCount),
|
|
1717
1857
|
source: "performance"
|
|
@@ -1735,6 +1875,199 @@ function classifySearchConsoleStage(input) {
|
|
|
1735
1875
|
source: "indexing"
|
|
1736
1876
|
}]);
|
|
1737
1877
|
}
|
|
1878
|
+
const NASCENT_IMPRESSIONS_28D = 1e3;
|
|
1879
|
+
const ESTABLISHED_IMPRESSIONS_28D = 2e4;
|
|
1880
|
+
const LIFETIME_FLOOR = 500;
|
|
1881
|
+
const GROWTH_CLICKS_PCT = 10;
|
|
1882
|
+
const GROWTH_IMPRESSIONS_PCT = 20;
|
|
1883
|
+
const DECLINE_CLICKS_PCT = -10;
|
|
1884
|
+
const MIN_DECLINE_PRIOR_CLICKS = 50;
|
|
1885
|
+
const FADED_LIVENESS = .2;
|
|
1886
|
+
const DECAY_CTR = .005;
|
|
1887
|
+
const HARD_BLOCK_FLOOR = 10;
|
|
1888
|
+
const HARD_BLOCK_SHARE = .03;
|
|
1889
|
+
const REJECT_SHARE = .3;
|
|
1890
|
+
const REJECT_MIN = 50;
|
|
1891
|
+
function reachLivenessRatio(daily) {
|
|
1892
|
+
if (!daily || daily.length < 14) return null;
|
|
1893
|
+
const series = daily.map((d) => d.impressions);
|
|
1894
|
+
while (series.length && series[series.length - 1] === 0) series.pop();
|
|
1895
|
+
if (series.length < 14) return null;
|
|
1896
|
+
const rolling7 = (end) => {
|
|
1897
|
+
let sum = 0;
|
|
1898
|
+
for (let i = Math.max(0, end - 6); i <= end; i++) sum += series[i];
|
|
1899
|
+
return sum;
|
|
1900
|
+
};
|
|
1901
|
+
const latestWeek = rolling7(series.length - 1);
|
|
1902
|
+
let peakWeek = 0;
|
|
1903
|
+
for (let i = 6; i < series.length; i++) peakWeek = Math.max(peakWeek, rolling7(i));
|
|
1904
|
+
return peakWeek > 0 ? latestWeek / peakWeek : null;
|
|
1905
|
+
}
|
|
1906
|
+
function issueCount(issues, ...types) {
|
|
1907
|
+
if (!issues?.length) return 0;
|
|
1908
|
+
const wanted = new Set(types);
|
|
1909
|
+
return issues.reduce((sum, i) => sum + (wanted.has(i.type) ? i.count : 0), 0);
|
|
1910
|
+
}
|
|
1911
|
+
function fmt(n) {
|
|
1912
|
+
return new Intl.NumberFormat("en").format(Math.max(0, Math.round(n)));
|
|
1913
|
+
}
|
|
1914
|
+
const HEALTH_COPY = {
|
|
1915
|
+
healthy: {
|
|
1916
|
+
summary: "Google can crawl and index the pages that should be indexed.",
|
|
1917
|
+
primaryAction: "No indexing cleanup needed — focus on reach."
|
|
1918
|
+
},
|
|
1919
|
+
crawl_faults: {
|
|
1920
|
+
summary: "Real access faults (server errors / broken links) are capping otherwise-indexable pages.",
|
|
1921
|
+
primaryAction: "Fix the 5xx and broken URLs; return 410/404 for pages you retire on purpose."
|
|
1922
|
+
},
|
|
1923
|
+
quality_rejection: {
|
|
1924
|
+
summary: "Google is crawling pages and refusing to index them — a soft quality signal it never reports explicitly.",
|
|
1925
|
+
primaryAction: "Consolidate or improve the rejected pages, or noindex the thin/low-value set."
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
function isIntentionalRetirementSite(input, notFound, serverError) {
|
|
1929
|
+
const type = normalizeSiteType(input.siteType);
|
|
1930
|
+
const typeMatches = type === "agency" || type === "portfolio" || type === "other";
|
|
1931
|
+
const fingerprint = notFound > 50 && serverError <= Math.max(5, notFound * .1);
|
|
1932
|
+
return typeMatches && fingerprint;
|
|
1933
|
+
}
|
|
1934
|
+
function classifyHealthStage(input) {
|
|
1935
|
+
const issues = input.issues ?? [];
|
|
1936
|
+
const totalUrls = input.totalUrls ?? 0;
|
|
1937
|
+
const noindex = issueCount(issues, "noindex");
|
|
1938
|
+
const notFound = issueCount(issues, "not_found");
|
|
1939
|
+
const softFound = issueCount(issues, "soft_404");
|
|
1940
|
+
const serverError = issueCount(issues, "server_error", "blocked_robots", "access_denied", "forbidden");
|
|
1941
|
+
const crawledNotIndexed = issueCount(issues, "crawled_not_indexed");
|
|
1942
|
+
const intentionalDead = isIntentionalRetirementSite(input, notFound, serverError) ? notFound : 0;
|
|
1943
|
+
const indexableUrls = Math.max(1, totalUrls - noindex - intentionalDead);
|
|
1944
|
+
const hardBlocks = serverError + (input.crawlAuditBlockerCount ?? 0);
|
|
1945
|
+
if (hardBlocks > Math.max(HARD_BLOCK_FLOOR, indexableUrls * HARD_BLOCK_SHARE)) return {
|
|
1946
|
+
stage: "crawl_faults",
|
|
1947
|
+
...HEALTH_COPY.crawl_faults,
|
|
1948
|
+
evidence: [{
|
|
1949
|
+
label: "Access faults (5xx / broken)",
|
|
1950
|
+
value: fmt(hardBlocks)
|
|
1951
|
+
}]
|
|
1952
|
+
};
|
|
1953
|
+
const rejectPool = crawledNotIndexed + softFound;
|
|
1954
|
+
if (totalUrls > 0 && rejectPool > REJECT_MIN && rejectPool / totalUrls >= REJECT_SHARE) return {
|
|
1955
|
+
stage: "quality_rejection",
|
|
1956
|
+
...HEALTH_COPY.quality_rejection,
|
|
1957
|
+
evidence: [{
|
|
1958
|
+
label: "Crawled, then refused",
|
|
1959
|
+
value: fmt(rejectPool)
|
|
1960
|
+
}, {
|
|
1961
|
+
label: "Share of known URLs",
|
|
1962
|
+
value: `${(rejectPool / totalUrls * 100).toFixed(0)}%`
|
|
1963
|
+
}]
|
|
1964
|
+
};
|
|
1965
|
+
return {
|
|
1966
|
+
stage: "healthy",
|
|
1967
|
+
...HEALTH_COPY.healthy,
|
|
1968
|
+
evidence: []
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
const REACH_COPY = {
|
|
1972
|
+
waiting_for_data: {
|
|
1973
|
+
summary: "Too new to diagnose — not enough search history yet.",
|
|
1974
|
+
primaryAction: "Submit a clean sitemap, add internal links, and give it time."
|
|
1975
|
+
},
|
|
1976
|
+
emerging: {
|
|
1977
|
+
summary: "Early but climbing — real impressions are starting to land.",
|
|
1978
|
+
primaryAction: "Keep publishing on the themes already gaining impressions."
|
|
1979
|
+
},
|
|
1980
|
+
growing: {
|
|
1981
|
+
summary: "Discoverable, indexed-enough, and trending up. The next work is expansion, not cleanup.",
|
|
1982
|
+
primaryAction: "Expand: striking-distance pages, content gaps, authority."
|
|
1983
|
+
},
|
|
1984
|
+
plateaued: {
|
|
1985
|
+
summary: "Established but flat — visibility is steady, not compounding.",
|
|
1986
|
+
primaryAction: "Refresh top pages and open a new content cluster to restart growth."
|
|
1987
|
+
},
|
|
1988
|
+
declining: {
|
|
1989
|
+
summary: "Established visibility is genuinely falling versus the previous period.",
|
|
1990
|
+
primaryAction: "Investigate the losing pages, recent releases, and competitor moves before expanding."
|
|
1991
|
+
},
|
|
1992
|
+
faded: {
|
|
1993
|
+
summary: "The site had real reach that has collapsed — it spiked and is now near-invisible in search.",
|
|
1994
|
+
primaryAction: "Diagnose the drop (quality, deindexing, lost rankings) — the 90-day total hides it; look at the recent week."
|
|
1995
|
+
},
|
|
1996
|
+
decayed: {
|
|
1997
|
+
summary: "Still shows for old terms but earns almost no clicks — the content has aged out of relevance.",
|
|
1998
|
+
primaryAction: "Refresh the decayed top pages, or mark the site low-priority if it is no longer maintained."
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
function reach(stage, evidence) {
|
|
2002
|
+
return {
|
|
2003
|
+
stage,
|
|
2004
|
+
...REACH_COPY[stage],
|
|
2005
|
+
evidence
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
function classifyReachStage(input) {
|
|
2009
|
+
const imp28d = input.impressions28d ?? 0;
|
|
2010
|
+
const imp12m = input.impressions12m ?? null;
|
|
2011
|
+
const clicks28d = input.clicks28d ?? null;
|
|
2012
|
+
const clicks90dPct = input.clicksPct90d ?? null;
|
|
2013
|
+
const clicks28dPct = input.clicksPct28d ?? null;
|
|
2014
|
+
const priorClicks = input.clicksPrior28d ?? null;
|
|
2015
|
+
const imp90dPct = input.impressionsPct90d ?? null;
|
|
2016
|
+
const posDelta = input.positionDelta90d ?? null;
|
|
2017
|
+
const liveness = input.livenessRatio ?? null;
|
|
2018
|
+
if (imp12m != null && imp12m < LIFETIME_FLOOR) return reach("waiting_for_data", [{
|
|
2019
|
+
label: "Impressions (12m)",
|
|
2020
|
+
value: fmt(imp12m)
|
|
2021
|
+
}]);
|
|
2022
|
+
const hadRealReach = (imp12m ?? imp28d) > LIFETIME_FLOOR;
|
|
2023
|
+
const isGrowing = clicks90dPct != null && clicks90dPct > GROWTH_CLICKS_PCT || imp90dPct != null && imp90dPct > GROWTH_IMPRESSIONS_PCT && posDelta != null && posDelta < 0;
|
|
2024
|
+
if (hadRealReach && liveness != null && liveness < FADED_LIVENESS && !isGrowing) return reach("faded", [{
|
|
2025
|
+
label: "Recent week vs peak",
|
|
2026
|
+
value: `${(liveness * 100).toFixed(0)}%`
|
|
2027
|
+
}]);
|
|
2028
|
+
if (clicks90dPct != null && clicks90dPct < DECLINE_CLICKS_PCT && clicks28dPct != null && clicks28dPct < DECLINE_CLICKS_PCT && priorClicks != null && priorClicks >= MIN_DECLINE_PRIOR_CLICKS) return reach("declining", [{
|
|
2029
|
+
label: "Clicks 90d",
|
|
2030
|
+
value: `${clicks90dPct.toFixed(0)}%`
|
|
2031
|
+
}, {
|
|
2032
|
+
label: "Clicks 28d",
|
|
2033
|
+
value: `${clicks28dPct.toFixed(0)}%`
|
|
2034
|
+
}]);
|
|
2035
|
+
if (isGrowing) return reach("growing", [...clicks90dPct != null ? [{
|
|
2036
|
+
label: "Clicks 90d",
|
|
2037
|
+
value: `+${clicks90dPct.toFixed(0)}%`
|
|
2038
|
+
}] : [], ...imp90dPct != null ? [{
|
|
2039
|
+
label: "Impressions 90d",
|
|
2040
|
+
value: `+${imp90dPct.toFixed(0)}%`
|
|
2041
|
+
}] : []]);
|
|
2042
|
+
const ctr = clicks28d != null && imp28d > 0 ? clicks28d / imp28d : null;
|
|
2043
|
+
if (hadRealReach && imp28d >= NASCENT_IMPRESSIONS_28D && ctr != null && ctr < DECAY_CTR) return reach("decayed", [{
|
|
2044
|
+
label: "Impressions (28d)",
|
|
2045
|
+
value: fmt(imp28d)
|
|
2046
|
+
}, {
|
|
2047
|
+
label: "Clicks (28d)",
|
|
2048
|
+
value: fmt(clicks28d ?? 0)
|
|
2049
|
+
}]);
|
|
2050
|
+
if (imp28d >= ESTABLISHED_IMPRESSIONS_28D) return reach("plateaued", [{
|
|
2051
|
+
label: "Impressions (28d)",
|
|
2052
|
+
value: fmt(imp28d)
|
|
2053
|
+
}]);
|
|
2054
|
+
if (imp28d < NASCENT_IMPRESSIONS_28D) return reach("emerging", [{
|
|
2055
|
+
label: "Impressions (28d)",
|
|
2056
|
+
value: fmt(imp28d)
|
|
2057
|
+
}]);
|
|
2058
|
+
return reach("plateaued", [{
|
|
2059
|
+
label: "Impressions (28d)",
|
|
2060
|
+
value: fmt(imp28d)
|
|
2061
|
+
}]);
|
|
2062
|
+
}
|
|
2063
|
+
function classifySiteTriage(input) {
|
|
2064
|
+
const health = classifyHealthStage(input);
|
|
2065
|
+
return {
|
|
2066
|
+
reach: classifyReachStage(input),
|
|
2067
|
+
health,
|
|
2068
|
+
headline: health.stage === "healthy" ? "reach" : "health"
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
1738
2071
|
const encoder = new TextEncoder();
|
|
1739
2072
|
function toPayloadString(payload) {
|
|
1740
2073
|
return typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
@@ -1809,4 +2142,4 @@ function readWebhookHeaders(headers) {
|
|
|
1809
2142
|
signature: headers.signature ?? null
|
|
1810
2143
|
};
|
|
1811
2144
|
}
|
|
1812
|
-
export { ARCHETYPE_EXECUTION_CLASS, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, PERIOD_PRESETS, PartnerApiError, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifySearchConsoleStage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
|
|
2145
|
+
export { ARCHETYPE_EXECUTION_CLASS, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, PERIOD_PRESETS, PartnerApiError, SITE_TYPE_BASELINE, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifyHealthStage, classifyReachStage, classifySearchConsoleStage, classifySiteTriage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, derivePeerStanding, deriveSiteBaseline, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, normalizeSiteType, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, reachLivenessRatio, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteTypeBaseline, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.24.0",
|
|
5
5
|
"description": "Consumer SDK for hosted gscdump.com integrations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"gscdump": "0.
|
|
44
|
+
"gscdump": "0.24.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"gscdump": {
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"date-fns": "^4.3.0",
|
|
53
53
|
"ofetch": "^1.5.1",
|
|
54
54
|
"zod": "^4.4.3",
|
|
55
|
-
"@gscdump/analysis": "0.
|
|
56
|
-
"@gscdump/engine": "0.
|
|
57
|
-
"@gscdump/contracts": "0.
|
|
55
|
+
"@gscdump/analysis": "0.24.0",
|
|
56
|
+
"@gscdump/engine": "0.24.0",
|
|
57
|
+
"@gscdump/contracts": "0.24.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"typescript": "^6.0.3",
|