thumbgate 0.9.9
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/.claude-plugin/README.md +134 -0
- package/.claude-plugin/bundle/icon.png +0 -0
- package/.claude-plugin/bundle/icon.svg +18 -0
- package/.claude-plugin/bundle/server/index.js +24 -0
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.well-known/mcp/server-card.json +231 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/adapters/README.md +9 -0
- package/adapters/amp/skills/rlhf-feedback/SKILL.md +22 -0
- package/adapters/chatgpt/INSTALL.md +83 -0
- package/adapters/chatgpt/openapi.yaml +1281 -0
- package/adapters/claude/.mcp.json +14 -0
- package/adapters/codex/config.toml +9 -0
- package/adapters/gemini/function-declarations.json +224 -0
- package/adapters/mcp/server-stdio.js +788 -0
- package/adapters/opencode/opencode.json +15 -0
- package/bin/cli.js +1483 -0
- package/bin/memory.sh +64 -0
- package/bin/obsidian-sync.sh +20 -0
- package/bin/postinstall.js +37 -0
- package/config/build-metadata.json +4 -0
- package/config/e2e-critical-flows.json +45 -0
- package/config/gate-templates.json +77 -0
- package/config/gates/claim-verification.json +29 -0
- package/config/gates/computer-use.json +39 -0
- package/config/gates/default.json +117 -0
- package/config/github-about.json +25 -0
- package/config/mcp-allowlists.json +135 -0
- package/config/model-tiers.json +33 -0
- package/config/partner-routing.json +132 -0
- package/config/policy-bundles/constrained-v1.json +64 -0
- package/config/policy-bundles/default-v1.json +91 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/skill-packs/react-testing.json +23 -0
- package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
- package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
- package/config/skill-specs/pr-reviewer.json +9 -0
- package/config/skill-specs/release-status.json +9 -0
- package/config/skill-specs/ticket-triage.json +9 -0
- package/config/subagent-profiles.json +32 -0
- package/config/tessl-tiles.json +29 -0
- package/config/thumbgate-settings.managed.json +12 -0
- package/openapi/openapi.yaml +1281 -0
- package/package.json +286 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +64 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
- package/plugins/claude-codex-bridge/.mcp.json +12 -0
- package/plugins/claude-codex-bridge/INSTALL.md +43 -0
- package/plugins/claude-codex-bridge/README.md +46 -0
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
- package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
- package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
- package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
- package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
- package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
- package/plugins/codex-profile/.mcp.json +12 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +66 -0
- package/plugins/codex-profile/README.md +37 -0
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
- package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
- package/plugins/cursor-marketplace/LICENSE +21 -0
- package/plugins/cursor-marketplace/README.md +124 -0
- package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
- package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
- package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
- package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
- package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
- package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
- package/plugins/cursor-marketplace/mcp.json +12 -0
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
- package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
- package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
- package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
- package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
- package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
- package/plugins/gemini-extension/INSTALL.md +92 -0
- package/plugins/gemini-extension/gemini_prompt.txt +14 -0
- package/plugins/gemini-extension/tool_contract.json +45 -0
- package/plugins/opencode-profile/INSTALL.md +57 -0
- package/public/assets/instagram-card.png +0 -0
- package/public/assets/tiktok-agent-memory.mp4 +0 -0
- package/public/blog.html +400 -0
- package/public/dashboard.html +1093 -0
- package/public/guide.html +317 -0
- package/public/index.html +1195 -0
- package/public/learn/agent-harness-pattern.html +180 -0
- package/public/learn/ai-agent-persistent-memory.html +202 -0
- package/public/learn/learn.css +45 -0
- package/public/learn/mcp-pre-action-gates-explained.html +172 -0
- package/public/learn/stop-ai-agent-force-push.html +134 -0
- package/public/learn/vibe-coding-safety-net.html +142 -0
- package/public/learn.html +213 -0
- package/public/lessons.html +650 -0
- package/public/vercel.json +8 -0
- package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/access-anomaly-detector.js +12 -0
- package/scripts/adk-consolidator.js +266 -0
- package/scripts/agent-readiness.js +220 -0
- package/scripts/agent-security-hardening.js +227 -0
- package/scripts/agentic-data-pipeline.js +847 -0
- package/scripts/analytics-report.js +328 -0
- package/scripts/analytics-window.js +158 -0
- package/scripts/async-job-runner.js +1001 -0
- package/scripts/audit-trail.js +398 -0
- package/scripts/auto-promote-gates.js +293 -0
- package/scripts/auto-wire-hooks.js +316 -0
- package/scripts/autonomous-sales-agent.js +39 -0
- package/scripts/autoresearch-runner.js +216 -0
- package/scripts/background-agent-governance.js +237 -0
- package/scripts/behavioral-extraction.js +93 -0
- package/scripts/belief-update.js +84 -0
- package/scripts/billing.js +2438 -0
- package/scripts/bot-detector.js +50 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/build-claude-mcpb.js +189 -0
- package/scripts/build-metadata.js +97 -0
- package/scripts/check-congruence.js +322 -0
- package/scripts/cli-feedback.js +135 -0
- package/scripts/cli-telemetry.js +87 -0
- package/scripts/cloudflare-dynamic-sandbox.js +315 -0
- package/scripts/code-reasoning.js +350 -0
- package/scripts/codegraph-context.js +466 -0
- package/scripts/commercial-offer.js +56 -0
- package/scripts/computer-use-firewall.js +250 -0
- package/scripts/context-engine.js +694 -0
- package/scripts/contextfs.js +1287 -0
- package/scripts/conversation-context.js +119 -0
- package/scripts/creator-campaigns.js +239 -0
- package/scripts/daemon-manager.js +108 -0
- package/scripts/daily-digest.js +11 -0
- package/scripts/dashboard-render-spec.js +395 -0
- package/scripts/dashboard.js +1058 -0
- package/scripts/data-governance.js +173 -0
- package/scripts/delegation-runtime.js +900 -0
- package/scripts/deploy-gcp.sh +44 -0
- package/scripts/deploy-policy.js +231 -0
- package/scripts/disagreement-mining.js +315 -0
- package/scripts/dispatch-brief.js +159 -0
- package/scripts/distribution-surfaces.js +44 -0
- package/scripts/dpo-optimizer.js +206 -0
- package/scripts/ensure-repo-bootstrap.js +129 -0
- package/scripts/ephemeral-agent-store.js +219 -0
- package/scripts/eval-harness.js +56 -0
- package/scripts/evolution-state.js +241 -0
- package/scripts/experiment-tracker.js +267 -0
- package/scripts/export-databricks-bundle.js +242 -0
- package/scripts/export-dpo-pairs.js +344 -0
- package/scripts/export-kto-pairs.js +309 -0
- package/scripts/export-training.js +450 -0
- package/scripts/failure-diagnostics.js +558 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-fallback.js +110 -0
- package/scripts/feedback-history-distiller.js +391 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +1887 -0
- package/scripts/feedback-paths.js +145 -0
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-root-consolidator.js +238 -0
- package/scripts/feedback-schema.js +426 -0
- package/scripts/feedback-session.js +286 -0
- package/scripts/feedback-to-memory.js +185 -0
- package/scripts/feedback-to-rules.js +164 -0
- package/scripts/filesystem-search.js +405 -0
- package/scripts/funnel-analytics.js +35 -0
- package/scripts/gate-satisfy.js +42 -0
- package/scripts/gate-stats.js +116 -0
- package/scripts/gate-templates.js +70 -0
- package/scripts/gates-engine.js +816 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/generate-pretool-hook.sh +40 -0
- package/scripts/github-about.js +350 -0
- package/scripts/github-outreach.js +65 -0
- package/scripts/gtm-revenue-loop.js +520 -0
- package/scripts/hallucination-detector.js +226 -0
- package/scripts/hf-papers.js +317 -0
- package/scripts/history-distiller.js +200 -0
- package/scripts/hook-auto-capture.sh +100 -0
- package/scripts/hook-stop-pr-thread-check.sh +68 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/hook-stop-verify-deploy.sh +31 -0
- package/scripts/hook-thumbgate-cache-updater.js +48 -0
- package/scripts/hook-verify-before-done.sh +20 -0
- package/scripts/hosted-config.js +156 -0
- package/scripts/hybrid-feedback-context.js +675 -0
- package/scripts/install-mcp.js +159 -0
- package/scripts/intent-router.js +392 -0
- package/scripts/internal-agent-bootstrap.js +490 -0
- package/scripts/jsonl-watcher.js +155 -0
- package/scripts/lesson-db.js +613 -0
- package/scripts/lesson-inference.js +310 -0
- package/scripts/lesson-retrieval.js +95 -0
- package/scripts/lesson-rotation.js +137 -0
- package/scripts/lesson-search.js +644 -0
- package/scripts/lesson-synthesis.js +196 -0
- package/scripts/license.js +50 -0
- package/scripts/local-model-profile.js +384 -0
- package/scripts/markdown-escape.js +12 -0
- package/scripts/marketing-experiment.js +671 -0
- package/scripts/mcp-config.js +149 -0
- package/scripts/mcp-policy.js +99 -0
- package/scripts/memalign-recall.js +111 -0
- package/scripts/memory-firewall.js +222 -0
- package/scripts/memory-migration.js +296 -0
- package/scripts/meta-policy.js +190 -0
- package/scripts/metered-billing.js +16 -0
- package/scripts/model-tier-router.js +301 -0
- package/scripts/money-watcher.js +71 -0
- package/scripts/multi-hop-recall.js +240 -0
- package/scripts/natural-language-harness.js +330 -0
- package/scripts/obsidian-export.js +713 -0
- package/scripts/operational-dashboard.js +103 -0
- package/scripts/operational-summary.js +93 -0
- package/scripts/optimize-context.js +17 -0
- package/scripts/org-dashboard.js +201 -0
- package/scripts/partner-orchestration.js +146 -0
- package/scripts/per-step-scoring.js +165 -0
- package/scripts/perplexity-marketing.js +466 -0
- package/scripts/pii-scanner.js +153 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/post-everywhere.js +308 -0
- package/scripts/post-to-x-retry.sh +22 -0
- package/scripts/post-to-x.js +369 -0
- package/scripts/pr-manager.js +236 -0
- package/scripts/predictive-insights.js +356 -0
- package/scripts/principle-extractor.js +162 -0
- package/scripts/pro-features.js +40 -0
- package/scripts/pro-local-dashboard.js +174 -0
- package/scripts/problem-detail.js +53 -0
- package/scripts/product-feedback.js +134 -0
- package/scripts/profile-router.js +245 -0
- package/scripts/prompt-dlp.js +221 -0
- package/scripts/prompt-guard.js +83 -0
- package/scripts/prove-adapters.js +863 -0
- package/scripts/prove-attribution.js +365 -0
- package/scripts/prove-automation.js +653 -0
- package/scripts/prove-autoresearch.js +304 -0
- package/scripts/prove-claim-verification.js +277 -0
- package/scripts/prove-cloudflare-sandbox.js +163 -0
- package/scripts/prove-data-pipeline.js +410 -0
- package/scripts/prove-data-quality.js +227 -0
- package/scripts/prove-evolution.js +352 -0
- package/scripts/prove-harnesses.js +287 -0
- package/scripts/prove-intelligence.js +259 -0
- package/scripts/prove-lancedb.js +371 -0
- package/scripts/prove-local-intelligence.js +342 -0
- package/scripts/prove-loop-closure.js +263 -0
- package/scripts/prove-predictive-insights.js +357 -0
- package/scripts/prove-runtime.js +350 -0
- package/scripts/prove-seo-gsd.js +234 -0
- package/scripts/prove-settings.js +279 -0
- package/scripts/prove-subway-upgrades.js +277 -0
- package/scripts/prove-tessl.js +229 -0
- package/scripts/prove-training-export.js +327 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/prove-xmemory.js +332 -0
- package/scripts/publish-decision.js +133 -0
- package/scripts/pulse.js +80 -0
- package/scripts/rate-limiter.js +125 -0
- package/scripts/reddit-dm-outreach.js +182 -0
- package/scripts/reddit-monitor-cron.sh +26 -0
- package/scripts/reflector-agent.js +221 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/revenue-status.js +472 -0
- package/scripts/risk-scorer.js +459 -0
- package/scripts/rlaif-self-audit.js +129 -0
- package/scripts/rlhf_session_start.sh +32 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/schedule-manager.js +251 -0
- package/scripts/secret-scanner.js +414 -0
- package/scripts/self-heal.js +147 -0
- package/scripts/self-healing-check.js +188 -0
- package/scripts/semantic-layer.js +98 -0
- package/scripts/seo-gsd.js +1153 -0
- package/scripts/settings-hierarchy.js +214 -0
- package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
- package/scripts/skill-exporter.js +262 -0
- package/scripts/skill-generator.js +446 -0
- package/scripts/skill-materializer.js +134 -0
- package/scripts/skill-packs.js +136 -0
- package/scripts/skill-proposer.js +99 -0
- package/scripts/skill-quality-tracker.js +282 -0
- package/scripts/slo-alert-engine.js +14 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/social-analytics/db/schema.sql +32 -0
- package/scripts/social-analytics/db/social-analytics.db +0 -0
- package/scripts/social-analytics/digest.js +256 -0
- package/scripts/social-analytics/generate-instagram-card.js +97 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +107 -0
- package/scripts/social-analytics/load-env.js +46 -0
- package/scripts/social-analytics/mcp-server.js +289 -0
- package/scripts/social-analytics/normalizer.js +580 -0
- package/scripts/social-analytics/notify.js +162 -0
- package/scripts/social-analytics/poll-all.js +92 -0
- package/scripts/social-analytics/pollers/github.js +195 -0
- package/scripts/social-analytics/pollers/instagram.js +253 -0
- package/scripts/social-analytics/pollers/linkedin.js +330 -0
- package/scripts/social-analytics/pollers/plausible.js +247 -0
- package/scripts/social-analytics/pollers/reddit.js +306 -0
- package/scripts/social-analytics/pollers/threads.js +233 -0
- package/scripts/social-analytics/pollers/tiktok.js +203 -0
- package/scripts/social-analytics/pollers/x.js +227 -0
- package/scripts/social-analytics/pollers/youtube.js +304 -0
- package/scripts/social-analytics/pollers/zernio.js +183 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +98 -0
- package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
- package/scripts/social-analytics/publishers/devto.js +122 -0
- package/scripts/social-analytics/publishers/instagram.js +317 -0
- package/scripts/social-analytics/publishers/linkedin.js +294 -0
- package/scripts/social-analytics/publishers/reddit.js +390 -0
- package/scripts/social-analytics/publishers/threads.js +275 -0
- package/scripts/social-analytics/publishers/tiktok.js +217 -0
- package/scripts/social-analytics/publishers/x.js +259 -0
- package/scripts/social-analytics/publishers/youtube.js +223 -0
- package/scripts/social-analytics/publishers/zernio.js +378 -0
- package/scripts/social-analytics/run-digest.js +34 -0
- package/scripts/social-analytics/store.js +257 -0
- package/scripts/social-analytics/utm.js +143 -0
- package/scripts/social-pipeline.js +2628 -0
- package/scripts/social-quality-gate.js +18 -0
- package/scripts/social-reply-monitor.js +445 -0
- package/scripts/status-dashboard.js +155 -0
- package/scripts/statusline-lesson.js +16 -0
- package/scripts/statusline-tower.js +8 -0
- package/scripts/statusline.sh +116 -0
- package/scripts/stripe-live-status.js +115 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +70 -0
- package/scripts/sync-github-about.js +52 -0
- package/scripts/sync-version.js +447 -0
- package/scripts/synthetic-dpo.js +234 -0
- package/scripts/telemetry-analytics.js +821 -0
- package/scripts/tessl-export.js +371 -0
- package/scripts/test-coverage.js +120 -0
- package/scripts/thompson-sampling.js +417 -0
- package/scripts/thumbgate-search.js +189 -0
- package/scripts/tool-kpi-tracker.js +12 -0
- package/scripts/tool-registry.js +811 -0
- package/scripts/train_from_feedback.py +933 -0
- package/scripts/user-profile.js +78 -0
- package/scripts/validate-feedback.js +581 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +197 -0
- package/scripts/verification-loop.js +291 -0
- package/scripts/verify-obsidian-setup.sh +269 -0
- package/scripts/verify-run.js +269 -0
- package/scripts/webhook-delivery.js +62 -0
- package/scripts/weekly-auto-post.js +124 -0
- package/scripts/workflow-runs.js +154 -0
- package/scripts/workflow-sprint-intake.js +475 -0
- package/scripts/workspace-evolver.js +374 -0
- package/scripts/x-autonomous-marketing.js +139 -0
- package/scripts/xmemory-lite.js +405 -0
- package/skills/agent-memory/SKILL.md +97 -0
- package/skills/rlhf-feedback/SKILL.md +49 -0
- package/skills/solve-architecture-autonomy/SKILL.md +17 -0
- package/skills/solve-architecture-autonomy/tool.js +33 -0
- package/skills/thumbgate/SKILL.md +114 -0
- package/src/api/server.js +4206 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LinkedIn engagement poller using the LinkedIn Posts API and related REST endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Required environment variables:
|
|
7
|
+
* LINKEDIN_ACCESS_TOKEN — OAuth 2.0 access token with r_liteprofile, r_organization_social,
|
|
8
|
+
* and r_1st_connections_size scopes (required)
|
|
9
|
+
* LINKEDIN_PERSON_URN — Authenticated member URN, e.g. urn:li:person:XXXXX (required)
|
|
10
|
+
*
|
|
11
|
+
* LinkedIn API references:
|
|
12
|
+
* Posts API: https://api.linkedin.com/rest/posts
|
|
13
|
+
* Social Metadata: https://api.linkedin.com/v2/socialMetadata/{encoded-urn}
|
|
14
|
+
* Profile: https://api.linkedin.com/v2/me
|
|
15
|
+
* Network sizes: https://api.linkedin.com/v2/networkSizes/{encoded-urn}
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const LI_REST_BASE = 'https://api.linkedin.com/rest';
|
|
19
|
+
const LI_V2_BASE = 'https://api.linkedin.com/v2';
|
|
20
|
+
|
|
21
|
+
// Standard headers required by LinkedIn's versioned REST API.
|
|
22
|
+
function buildRestHeaders(token) {
|
|
23
|
+
return {
|
|
24
|
+
Authorization: `Bearer ${token}`,
|
|
25
|
+
'LinkedIn-Version': '202401',
|
|
26
|
+
'X-Restli-Protocol-Version': '2.0.0',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildV2Headers(token) {
|
|
31
|
+
return {
|
|
32
|
+
Authorization: `Bearer ${token}`,
|
|
33
|
+
'X-Restli-Protocol-Version': '2.0.0',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safely coerces a value to an integer, returning 0 for null/undefined/NaN.
|
|
39
|
+
* @param {*} v
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
function toInt(v) {
|
|
43
|
+
const n = parseInt(v, 10);
|
|
44
|
+
return Number.isFinite(n) ? n : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fetches the most recent posts authored by the authenticated member.
|
|
49
|
+
*
|
|
50
|
+
* Uses the Posts API (LinkedIn-Version: 202401).
|
|
51
|
+
* Returns up to 20 posts sorted by recency.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} token - LinkedIn OAuth access token.
|
|
54
|
+
* @param {string} personUrn - Member URN, e.g. "urn:li:person:XXXXX".
|
|
55
|
+
* @returns {Promise<object[]>} Array of post objects.
|
|
56
|
+
*/
|
|
57
|
+
async function fetchLinkedInPosts(token, personUrn) {
|
|
58
|
+
if (!token) throw new Error('fetchLinkedInPosts: token is required');
|
|
59
|
+
if (!personUrn) throw new Error('fetchLinkedInPosts: personUrn is required');
|
|
60
|
+
|
|
61
|
+
const url =
|
|
62
|
+
`${LI_REST_BASE}/posts` +
|
|
63
|
+
`?author=${encodeURIComponent(personUrn)}&q=author&count=20`;
|
|
64
|
+
|
|
65
|
+
console.log(`[linkedin] Fetching posts for ${personUrn}`);
|
|
66
|
+
|
|
67
|
+
const res = await fetch(url, { headers: buildRestHeaders(token) });
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const body = await res.text().catch(() => '');
|
|
70
|
+
throw new Error(`fetchLinkedInPosts HTTP ${res.status}: ${body}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const json = await res.json();
|
|
74
|
+
if (json.serviceErrorCode || json.code) {
|
|
75
|
+
throw new Error(`fetchLinkedInPosts API error: ${JSON.stringify(json)}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return json.elements ?? [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetches share statistics (impressions, likes, comments, shares) for a post.
|
|
83
|
+
*
|
|
84
|
+
* LinkedIn's analytics APIs have significant restrictions for personal accounts vs company pages.
|
|
85
|
+
* Strategy:
|
|
86
|
+
* 1. Try the organizational entity share statistics endpoint (works for company pages).
|
|
87
|
+
* 2. Fall back to /v2/socialMetadata (works for personal member posts).
|
|
88
|
+
*
|
|
89
|
+
* @param {string} token - LinkedIn OAuth access token.
|
|
90
|
+
* @param {string} postUrn - Post URN, e.g. "urn:li:share:XXXXX" or "urn:li:ugcPost:XXXXX".
|
|
91
|
+
* @returns {Promise<object>} Normalized stats: { impressions, likes, comments, shares }.
|
|
92
|
+
*/
|
|
93
|
+
async function fetchPostAnalytics(token, postUrn) {
|
|
94
|
+
if (!token) throw new Error('fetchPostAnalytics: token is required');
|
|
95
|
+
if (!postUrn) throw new Error('fetchPostAnalytics: postUrn is required');
|
|
96
|
+
|
|
97
|
+
// Attempt 1: Organizational entity share statistics (company pages / pages with analytics).
|
|
98
|
+
const orgStatsUrl =
|
|
99
|
+
`${LI_REST_BASE}/organizationalEntityShareStatistics` +
|
|
100
|
+
`?q=organizationalEntity&shares=List(${encodeURIComponent(postUrn)})`;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(orgStatsUrl, { headers: buildRestHeaders(token) });
|
|
104
|
+
if (res.ok) {
|
|
105
|
+
const json = await res.json();
|
|
106
|
+
const el = (json.elements ?? [])[0];
|
|
107
|
+
if (el) {
|
|
108
|
+
const stats = el.totalShareStatistics ?? {};
|
|
109
|
+
console.log(`[linkedin] Got org share stats for ${postUrn}`);
|
|
110
|
+
return {
|
|
111
|
+
impressions: toInt(stats.impressionCount),
|
|
112
|
+
likes: toInt(stats.likeCount),
|
|
113
|
+
comments: toInt(stats.commentCount),
|
|
114
|
+
shares: toInt(stats.shareCount),
|
|
115
|
+
clicks: toInt(stats.clickCount),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.warn(`[linkedin] Org share stats error for ${postUrn} (trying fallback): ${err.message}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Attempt 2: socialMetadata — available for personal member posts.
|
|
124
|
+
const socialMetaUrl = `${LI_V2_BASE}/socialMetadata/${encodeURIComponent(postUrn)}`;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(socialMetaUrl, { headers: buildV2Headers(token) });
|
|
128
|
+
if (res.ok) {
|
|
129
|
+
const json = await res.json();
|
|
130
|
+
console.log(`[linkedin] Got socialMetadata for ${postUrn}`);
|
|
131
|
+
return {
|
|
132
|
+
impressions: 0, // socialMetadata does not expose impression counts.
|
|
133
|
+
likes: toInt(json.numLikes ?? json.likes?.paging?.total ?? 0),
|
|
134
|
+
comments: toInt(json.numComments ?? json.comments?.paging?.total ?? 0),
|
|
135
|
+
shares: toInt(json.numShares ?? json.shares?.paging?.total ?? 0),
|
|
136
|
+
clicks: 0,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const body = await res.text().catch(() => '');
|
|
140
|
+
console.warn(`[linkedin] socialMetadata HTTP ${res.status} for ${postUrn}: ${body}`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.warn(`[linkedin] socialMetadata error for ${postUrn}: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Both endpoints failed — return zeroed stats so polling continues.
|
|
146
|
+
return { impressions: 0, likes: 0, comments: 0, shares: 0, clicks: 0 };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fetches the authenticated member's basic profile information.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} token - LinkedIn OAuth access token.
|
|
153
|
+
* @returns {Promise<object>} Profile object with id, localizedFirstName, localizedLastName, etc.
|
|
154
|
+
*/
|
|
155
|
+
async function fetchLinkedInProfile(token) {
|
|
156
|
+
if (!token) throw new Error('fetchLinkedInProfile: token is required');
|
|
157
|
+
|
|
158
|
+
const url = `${LI_V2_BASE}/me`;
|
|
159
|
+
|
|
160
|
+
const res = await fetch(url, { headers: buildV2Headers(token) });
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const body = await res.text().catch(() => '');
|
|
163
|
+
throw new Error(`fetchLinkedInProfile HTTP ${res.status}: ${body}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const json = await res.json();
|
|
167
|
+
if (json.serviceErrorCode) {
|
|
168
|
+
throw new Error(`fetchLinkedInProfile API error: ${JSON.stringify(json)}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return json;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Fetches the network/connection size for a member or company.
|
|
176
|
+
*
|
|
177
|
+
* For company pages, uses CompanyFollowedByMember edgeType.
|
|
178
|
+
* For personal accounts the networkSizes endpoint returns connection count
|
|
179
|
+
* using MEMBER_TO_MEMBER_CONNECTION edgeType.
|
|
180
|
+
*
|
|
181
|
+
* LinkedIn limits this to the authenticated member's own network size
|
|
182
|
+
* due to privacy restrictions.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} token - LinkedIn OAuth access token.
|
|
185
|
+
* @param {string} personUrn - Member or organization URN.
|
|
186
|
+
* @returns {Promise<number>} Follower / connection count.
|
|
187
|
+
*/
|
|
188
|
+
async function fetchFollowerCount(token, personUrn) {
|
|
189
|
+
if (!token) throw new Error('fetchFollowerCount: token is required');
|
|
190
|
+
if (!personUrn) throw new Error('fetchFollowerCount: personUrn is required');
|
|
191
|
+
|
|
192
|
+
// Determine edge type based on URN type.
|
|
193
|
+
const isOrg = personUrn.includes('urn:li:organization:');
|
|
194
|
+
const edgeType = isOrg
|
|
195
|
+
? 'CompanyFollowedByMember'
|
|
196
|
+
: 'MEMBER_TO_MEMBER_CONNECTION';
|
|
197
|
+
|
|
198
|
+
const url =
|
|
199
|
+
`${LI_V2_BASE}/networkSizes/${encodeURIComponent(personUrn)}` +
|
|
200
|
+
`?edgeType=${edgeType}`;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch(url, { headers: buildV2Headers(token) });
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
const body = await res.text().catch(() => '');
|
|
206
|
+
console.warn(`[linkedin] fetchFollowerCount HTTP ${res.status}: ${body}`);
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
const json = await res.json();
|
|
210
|
+
// Response shape: { firstDegreeSize: N } or { followerCount: N }
|
|
211
|
+
return toInt(json.firstDegreeSize ?? json.followerCount ?? 0);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.warn(`[linkedin] fetchFollowerCount error: ${err.message}`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Main entry point. Polls LinkedIn for recent posts and per-post analytics,
|
|
220
|
+
* normalises the data, and upserts it into the local SQLite database.
|
|
221
|
+
*
|
|
222
|
+
* Also records a follower snapshot for today.
|
|
223
|
+
*
|
|
224
|
+
* @param {import('better-sqlite3').Database} db - Initialised db instance.
|
|
225
|
+
*/
|
|
226
|
+
async function pollLinkedIn(db) {
|
|
227
|
+
const token = process.env.LINKEDIN_ACCESS_TOKEN;
|
|
228
|
+
const personUrn = process.env.LINKEDIN_PERSON_URN;
|
|
229
|
+
|
|
230
|
+
if (!token) throw new Error('LINKEDIN_ACCESS_TOKEN is not set');
|
|
231
|
+
if (!personUrn) throw new Error('LINKEDIN_PERSON_URN is not set');
|
|
232
|
+
|
|
233
|
+
const { upsertMetric, upsertFollowerSnapshot } = require('../store');
|
|
234
|
+
|
|
235
|
+
const fetchedAt = new Date().toISOString();
|
|
236
|
+
const today = fetchedAt.slice(0, 10);
|
|
237
|
+
|
|
238
|
+
console.log(`[linkedin] Starting poll for ${personUrn}`);
|
|
239
|
+
|
|
240
|
+
const posts = await fetchLinkedInPosts(token, personUrn);
|
|
241
|
+
console.log(`[linkedin] Got ${posts.length} posts`);
|
|
242
|
+
|
|
243
|
+
for (const post of posts) {
|
|
244
|
+
// Post URN is at post.id for the Posts API (urn:li:share:... or urn:li:ugcPost:...).
|
|
245
|
+
const postUrn = post.id ?? post.urn ?? '';
|
|
246
|
+
if (!postUrn) {
|
|
247
|
+
console.warn('[linkedin] Post missing id/urn, skipping');
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const analytics = await fetchPostAnalytics(token, postUrn);
|
|
252
|
+
|
|
253
|
+
// Determine content type: article vs standard post.
|
|
254
|
+
const hasArticle = !!(post.content?.article);
|
|
255
|
+
const contentType = hasArticle ? 'article' : 'post';
|
|
256
|
+
|
|
257
|
+
// Published time: Posts API returns createdAt as milliseconds epoch.
|
|
258
|
+
let publishedAt = null;
|
|
259
|
+
if (post.createdAt) {
|
|
260
|
+
publishedAt = new Date(post.createdAt).toISOString();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const record = {
|
|
264
|
+
platform: 'linkedin',
|
|
265
|
+
content_type: contentType,
|
|
266
|
+
post_id: postUrn,
|
|
267
|
+
post_url: post.content?.article?.source ?? null,
|
|
268
|
+
published_at: publishedAt,
|
|
269
|
+
metric_date: today,
|
|
270
|
+
impressions: analytics.impressions,
|
|
271
|
+
reach: 0,
|
|
272
|
+
likes: analytics.likes,
|
|
273
|
+
comments: analytics.comments,
|
|
274
|
+
shares: analytics.shares,
|
|
275
|
+
saves: 0,
|
|
276
|
+
clicks: analytics.clicks,
|
|
277
|
+
video_views: 0,
|
|
278
|
+
followers_delta: 0,
|
|
279
|
+
extra_json: JSON.stringify({
|
|
280
|
+
commentary_snippet: (post.commentary ?? '').slice(0, 120),
|
|
281
|
+
visibility: post.visibility ?? null,
|
|
282
|
+
lifecycle_state: post.lifecycleState ?? null,
|
|
283
|
+
}),
|
|
284
|
+
fetched_at: fetchedAt,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
upsertMetric(db, record);
|
|
288
|
+
console.log(`[linkedin] Upserted metric for post ${postUrn} (${contentType})`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Follower / connection snapshot.
|
|
292
|
+
try {
|
|
293
|
+
console.log('[linkedin] Fetching follower/connection count…');
|
|
294
|
+
const followerCount = await fetchFollowerCount(token, personUrn);
|
|
295
|
+
upsertFollowerSnapshot(db, {
|
|
296
|
+
platform: 'linkedin',
|
|
297
|
+
follower_count: followerCount,
|
|
298
|
+
snapshot_date: today,
|
|
299
|
+
});
|
|
300
|
+
console.log(`[linkedin] Upserted follower snapshot: ${followerCount}`);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.warn(`[linkedin] Follower snapshot error (non-fatal): ${err.message}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log('[linkedin] Poll complete.');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
fetchLinkedInPosts,
|
|
310
|
+
fetchPostAnalytics,
|
|
311
|
+
fetchLinkedInProfile,
|
|
312
|
+
fetchFollowerCount,
|
|
313
|
+
pollLinkedIn,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// Allow running directly: node scripts/social-analytics/pollers/linkedin.js
|
|
317
|
+
if (require.main === module) {
|
|
318
|
+
(async () => {
|
|
319
|
+
const { initDb } = require('../store');
|
|
320
|
+
const db = initDb();
|
|
321
|
+
try {
|
|
322
|
+
await pollLinkedIn(db);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.error('[linkedin] Fatal error:', err.message);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
} finally {
|
|
327
|
+
db.close();
|
|
328
|
+
}
|
|
329
|
+
})();
|
|
330
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plausible Analytics poller — reads web traffic metrics via the Plausible API.
|
|
5
|
+
*
|
|
6
|
+
* Required environment variables:
|
|
7
|
+
* PLAUSIBLE_API_KEY — Plausible API token
|
|
8
|
+
* PLAUSIBLE_SITE_ID — Site domain registered in Plausible (e.g. example.com)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { upsertMetric, initDb } = require('../store');
|
|
12
|
+
|
|
13
|
+
const PLAUSIBLE_BASE = 'https://plausible.io/api/v1';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds a Plausible API request URL and executes it.
|
|
17
|
+
* Adds site_id and all extra params to the query string.
|
|
18
|
+
* Throws on non-2xx responses or API-level error bodies.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} endpoint - Path under /api/v1 (e.g. '/stats/aggregate')
|
|
21
|
+
* @param {Record<string, string|number>} params - Additional query parameters.
|
|
22
|
+
* @returns {Promise<object>} Parsed JSON response body.
|
|
23
|
+
*/
|
|
24
|
+
async function plausibleQuery(endpoint, params = {}) {
|
|
25
|
+
const apiKey = process.env.PLAUSIBLE_API_KEY;
|
|
26
|
+
const siteId = process.env.PLAUSIBLE_SITE_ID;
|
|
27
|
+
|
|
28
|
+
if (!apiKey) throw new Error('PLAUSIBLE_API_KEY is not set');
|
|
29
|
+
if (!siteId) throw new Error('PLAUSIBLE_SITE_ID is not set');
|
|
30
|
+
|
|
31
|
+
const qs = new URLSearchParams({ site_id: siteId, ...params });
|
|
32
|
+
const url = `${PLAUSIBLE_BASE}${endpoint}?${qs.toString()}`;
|
|
33
|
+
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${apiKey}`,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
const body = await res.text().catch(() => '');
|
|
42
|
+
throw new Error(`Plausible API ${res.status} for ${endpoint}: ${body}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const json = await res.json();
|
|
46
|
+
if (json.error) {
|
|
47
|
+
throw new Error(`Plausible API error for ${endpoint}: ${json.error}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return json;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetches aggregate site metrics: visitors, pageviews, bounce_rate, visit_duration.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} [period='7d'] - Plausible period string (e.g. '7d', '30d', 'month').
|
|
57
|
+
* @returns {Promise<object>} Aggregate results object from Plausible.
|
|
58
|
+
*/
|
|
59
|
+
async function getVisitors(period = '7d') {
|
|
60
|
+
return plausibleQuery('/stats/aggregate', {
|
|
61
|
+
metrics: 'visitors,pageviews,bounce_rate,visit_duration',
|
|
62
|
+
period,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fetches traffic source breakdown.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} [period='7d'] - Plausible period string.
|
|
70
|
+
* @returns {Promise<object>} Breakdown results by visit:source.
|
|
71
|
+
*/
|
|
72
|
+
async function getSourceAttribution(period = '7d') {
|
|
73
|
+
return plausibleQuery('/stats/breakdown', {
|
|
74
|
+
property: 'visit:source',
|
|
75
|
+
metrics: 'visitors,events',
|
|
76
|
+
period,
|
|
77
|
+
limit: 20,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetches breakdown by a specific UTM parameter.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} utmParam - UTM dimension (e.g. 'utm_source', 'utm_medium', 'utm_campaign').
|
|
85
|
+
* @param {string} [period='7d'] - Plausible period string.
|
|
86
|
+
* @returns {Promise<object>} Breakdown results for the given UTM property.
|
|
87
|
+
*/
|
|
88
|
+
async function getUTMBreakdown(utmParam, period = '7d') {
|
|
89
|
+
return plausibleQuery('/stats/breakdown', {
|
|
90
|
+
property: `visit:${utmParam}`,
|
|
91
|
+
metrics: 'visitors,events',
|
|
92
|
+
period,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fetches aggregate event count for a named custom event.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} eventName - Plausible custom event name (e.g. 'CTA Click').
|
|
100
|
+
* @param {string} [period='7d'] - Plausible period string.
|
|
101
|
+
* @returns {Promise<object>} Aggregate results with events count.
|
|
102
|
+
*/
|
|
103
|
+
async function getCustomEventCount(eventName, period = '7d') {
|
|
104
|
+
return plausibleQuery('/stats/aggregate', {
|
|
105
|
+
metrics: 'events',
|
|
106
|
+
period,
|
|
107
|
+
filters: `event:name==${eventName}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Builds a funnel object from visitors, CTA Click, Checkout Start, and Purchase events.
|
|
113
|
+
* Computes conversion rates between each stage.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} [period='7d'] - Plausible period string.
|
|
116
|
+
* @returns {Promise<object>} Funnel metrics with conversion rates.
|
|
117
|
+
*/
|
|
118
|
+
async function getFunnelMetrics(period = '7d') {
|
|
119
|
+
const [visitorsRes, ctaRes, checkoutRes, purchaseRes] = await Promise.all([
|
|
120
|
+
getVisitors(period),
|
|
121
|
+
getCustomEventCount('CTA Click', period),
|
|
122
|
+
getCustomEventCount('Checkout Start', period),
|
|
123
|
+
getCustomEventCount('Purchase', period),
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const visitors = visitorsRes.results?.visitors?.value ?? 0;
|
|
127
|
+
const ctaClicks = ctaRes.results?.events?.value ?? 0;
|
|
128
|
+
const checkoutStarts = checkoutRes.results?.events?.value ?? 0;
|
|
129
|
+
const purchases = purchaseRes.results?.events?.value ?? 0;
|
|
130
|
+
|
|
131
|
+
const rate = (numerator, denominator) =>
|
|
132
|
+
denominator > 0 ? Math.round((numerator / denominator) * 10000) / 100 : 0;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
period,
|
|
136
|
+
visitors,
|
|
137
|
+
cta_clicks: ctaClicks,
|
|
138
|
+
checkout_starts: checkoutStarts,
|
|
139
|
+
purchases,
|
|
140
|
+
visitor_to_cta_pct: rate(ctaClicks, visitors),
|
|
141
|
+
cta_to_checkout_pct: rate(checkoutStarts, ctaClicks),
|
|
142
|
+
checkout_to_purchase_pct: rate(purchases, checkoutStarts),
|
|
143
|
+
visitor_to_purchase_pct: rate(purchases, visitors),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Main polling entry point. Fetches visitors, source attribution, and funnel
|
|
149
|
+
* metrics for the last 7 days and upserts results into the engagement_metrics
|
|
150
|
+
* table as platform='web', content_type='page'.
|
|
151
|
+
*
|
|
152
|
+
* @param {import('better-sqlite3').Database} db - Initialised db instance.
|
|
153
|
+
* @returns {Promise<object>} Summary of stored results.
|
|
154
|
+
*/
|
|
155
|
+
async function pollPlausible(db) {
|
|
156
|
+
const siteId = process.env.PLAUSIBLE_SITE_ID;
|
|
157
|
+
if (!process.env.PLAUSIBLE_API_KEY) throw new Error('PLAUSIBLE_API_KEY is not set');
|
|
158
|
+
if (!siteId) throw new Error('PLAUSIBLE_SITE_ID is not set');
|
|
159
|
+
|
|
160
|
+
const period = '7d';
|
|
161
|
+
const fetchedAt = new Date().toISOString();
|
|
162
|
+
const metricDate = fetchedAt.slice(0, 10);
|
|
163
|
+
|
|
164
|
+
console.log(`[plausible] Fetching web metrics for site ${siteId} (${period})…`);
|
|
165
|
+
|
|
166
|
+
const [visitorsRes, sourceRes, funnelRes] = await Promise.all([
|
|
167
|
+
getVisitors(period),
|
|
168
|
+
getSourceAttribution(period),
|
|
169
|
+
getFunnelMetrics(period),
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
const aggregateResults = visitorsRes.results ?? {};
|
|
173
|
+
const visitors = aggregateResults.visitors?.value ?? 0;
|
|
174
|
+
const pageviews = aggregateResults.pageviews?.value ?? 0;
|
|
175
|
+
const bounceRate = aggregateResults.bounce_rate?.value ?? 0;
|
|
176
|
+
const visitDuration = aggregateResults.visit_duration?.value ?? 0;
|
|
177
|
+
|
|
178
|
+
console.log(
|
|
179
|
+
`[plausible] visitors=${visitors} pageviews=${pageviews} ` +
|
|
180
|
+
`bounce_rate=${bounceRate}% avg_duration=${visitDuration}s`
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Store aggregate web traffic as a single canonical record for the period.
|
|
184
|
+
const record = {
|
|
185
|
+
platform: 'web',
|
|
186
|
+
content_type: 'page',
|
|
187
|
+
post_id: `${siteId}:${period}:${metricDate}`,
|
|
188
|
+
post_url: `https://${siteId}`,
|
|
189
|
+
published_at: null,
|
|
190
|
+
metric_date: metricDate,
|
|
191
|
+
impressions: pageviews,
|
|
192
|
+
reach: visitors,
|
|
193
|
+
likes: 0,
|
|
194
|
+
comments: 0,
|
|
195
|
+
shares: 0,
|
|
196
|
+
saves: 0,
|
|
197
|
+
clicks: funnelRes.cta_clicks,
|
|
198
|
+
video_views: 0,
|
|
199
|
+
followers_delta: 0,
|
|
200
|
+
extra_json: JSON.stringify({
|
|
201
|
+
period,
|
|
202
|
+
bounce_rate: bounceRate,
|
|
203
|
+
visit_duration_seconds: visitDuration,
|
|
204
|
+
sources: sourceRes.results ?? [],
|
|
205
|
+
funnel: funnelRes,
|
|
206
|
+
}),
|
|
207
|
+
fetched_at: fetchedAt,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
upsertMetric(db, record);
|
|
211
|
+
console.log(`[plausible] Upserted web metric record for ${metricDate}`);
|
|
212
|
+
console.log(
|
|
213
|
+
`[plausible] Funnel: ${funnelRes.visitors} visitors → ` +
|
|
214
|
+
`${funnelRes.cta_clicks} CTA clicks → ` +
|
|
215
|
+
`${funnelRes.checkout_starts} checkouts → ` +
|
|
216
|
+
`${funnelRes.purchases} purchases (${funnelRes.visitor_to_purchase_pct}%)`
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
console.log('[plausible] Poll complete.');
|
|
220
|
+
return { record, funnel: funnelRes };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
plausibleQuery,
|
|
225
|
+
getVisitors,
|
|
226
|
+
getSourceAttribution,
|
|
227
|
+
getUTMBreakdown,
|
|
228
|
+
getCustomEventCount,
|
|
229
|
+
getFunnelMetrics,
|
|
230
|
+
pollPlausible,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Allow running directly: node scripts/social-analytics/pollers/plausible.js
|
|
234
|
+
if (require.main === module) {
|
|
235
|
+
(async () => {
|
|
236
|
+
const db = initDb();
|
|
237
|
+
try {
|
|
238
|
+
const results = await pollPlausible(db);
|
|
239
|
+
console.log('[plausible] Results:', JSON.stringify(results, null, 2));
|
|
240
|
+
} catch (err) {
|
|
241
|
+
console.error('[plausible] Fatal error:', err.message);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
} finally {
|
|
244
|
+
db.close();
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
}
|