Files
sdk/sdks/javascript
Mo Elzubeir 252ea713b1 feat: add entity analytics, pluggable cache, and pitfalls docs
- Add Cache interface with MemoryCache and NoopCache implementations
- Make SocialhoseClient accept injectable cache option
- Remove Next.js next.revalidate coupling from transport
- Add entity analytics: getEntityBrief, getEntityStats, getEntityBriefs,
  getCampaignIdByMatch with exact sentiment/platform faceting and
  cumulative-differenced timeline
- Add 23 new tests (29 total) covering entity analytics, cache injection,
  sentiment reconciliation, bounded concurrency, and timeline differencing
- Update README with entity analytics, custom caching, and pitfalls sections
- Fix CI branch: main -> master
2026-05-29 13:10:05 -05:00
..

@socialhose/api

TypeScript SDK for the Socialhose Public API.

Install

npm install @socialhose/api

Node 18+ is required because the SDK uses the built-in fetch, Response, and AbortSignal.timeout APIs. You can pass a custom fetch implementation if needed.

Quickstart

import { SocialhoseClient } from '@socialhose/api';

const socialhose = new SocialhoseClient({
  apiKey: process.env.SOCIALHOSE_API_KEY!,
});

const mentions = await socialhose.getMentions({
  content_search: 'hospital',
  platforms: 'twitter',
  ordering: '-published_at',
});

console.log(mentions.count, mentions.results[0]?.content);

Configuration

const socialhose = new SocialhoseClient({
  apiKey: process.env.SOCIALHOSE_API_KEY!,
  baseUrl: 'https://socialhose.net/api/public/v1',
  timeoutMs: 8_000,
  retries: 3,
  cacheTtlMs: 60_000,
});

The SDK sends Authorization: Api-Key <key> and a browser-like User-Agent by default. The user-agent is intentional: the current Socialhose API edge rejects some non-browser requests.

Endpoints

Campaigns

  • getCampaigns()
  • getCampaign(id)

Analytics

  • getOverview(filters)
  • getTimeline(filters)
  • getSentiment(filters)
  • getShareOfVoice(filters)
  • getPlatforms(filters)
  • getTopKeywords(filters)
  • getTrending(filters)
  • getTopMentions(filters)

Mentions

  • getMentions(filters)

Mailing Lists

  • getMailingLists()
  • inviteMailingListMember(listId, invite)

Entity Analytics

  • getEntityBrief(term, campaignId?) — one request: count + top-20 engagement sample with derived sentiment/platform
  • getEntityStats(term, campaignId?) — full dashboard: exact sentiment faceting, exact platform mix, 14-day cumulative-differenced timeline, 7d momentum
  • getEntityBriefs(terms, campaignId?, concurrency?) — batch entity resolution with bounded concurrency (default 20)
  • getCampaignIdByMatch(substring) — resolve a live campaign ID by matching its name

Low-Level

  • get(path, params) for direct GET access
  • post(path, body) for direct POST access

Filtering examples

await socialhose.getOverview({
  campaign_ids: 'campaign-id',
  date_from: '2026-05-01',
  date_to: '2026-05-29',
  platforms: 'twitter,reddit',
  sentiments: 'negative',
});

await socialhose.getTimeline({
  campaign_ids: 'campaign-id',
  interval: 'day',
});

Custom Caching

The SDK ships with MemoryCache (in-memory, per-entry TTL) and NoopCache (no caching). You can inject your own by implementing the Cache interface:

import { SocialhoseClient, Cache } from '@socialhose/api';

class RedisCache implements Cache {
  async get(key: string) { /* redis.get(key) */ }
  async set(key: string, value: unknown, ttlMs: number) { /* redis.set(key, value, 'PX', ttlMs) */ }
  async delete(key: string) { /* redis.del(key) */ }
}

const socialhose = new SocialhoseClient({
  apiKey: process.env.SOCIALHOSE_API_KEY!,
  cache: new RedisCache(),
});

Pass revalidateSeconds per request to control per-call TTL in your cache implementation:

await socialhose.getMentions(
  { content_search: 'ozempic' },
  { revalidateSeconds: 3600 },
);

Entity Analytics

Search across all mentions for a specific term, person, or organization:

// Quick count + top mentions
const brief = await socialhose.getEntityBrief('Burhan');
console.log(brief.total, brief.sentiment, brief.platformMix);

// Full dashboard: exact distributions, timeline, momentum
const stats = await socialhose.getEntityStats('RSF', 'campaign-id');
console.log(
  stats.total,
  stats.momentumPct,   // last 7 days vs prior 7
  stats.sentiment,      // exact (facets reconcile) or estimated (from sample)
  stats.sparkline,      // 14-day daily volume
);

// Batch resolve many entities with bounded concurrency
const briefs = await socialhose.getEntityBriefs(
  ['Burhan', 'Hemedti', 'SAF', 'RSF'],
  'campaign-id',
  10, // concurrency
);

Entity analytics fan out multiple requests per entity (sentiment faceting: 3 calls; platform mix: 6 calls; timeline: 15 calls). Set cacheTtlMs or inject a persistent cache to stay under the ~60 req/min rate limit.

Pitfalls

Cloudflare UA blocking. The Socialhose API sits behind Cloudflare, which rejects some non-browser User-Agent headers. The SDK defaults to a Chrome 124 UA — don't change it unless you've verified the new UA works.

Entity timeline uses cumulative differencing. The analytics timeline endpoint is campaign-scoped and ignores content_search. The SDK facets /mentions/ by day using cumulative date_from-only queries and subtracts consecutive counts. This avoids the API's date_to inclusivity bug: overlapping [date_from, date_to] windows share a day and double-count it. Don't "simplify" this to day windows.

Sentiment reconciliation checks. The getEntityStats exact sentiment and platform distributions validate that facet counts reconcile with the known total (e.g., positive + negative + neutral === total). If they don't match, the API silently dropped the content_search filter — the SDK falls back to estimates from the brief's sample rather than showing wrong data.

Rate limit ~60 req/min per API key. Entity analytics fan out many parallel requests. Use the cache option with a persistent store (Redis, Next.js Data Cache) to keep warm loads under the limit.

Never expose the API key to the browser. This SDK is designed for server-side use. Always set SOCIALHOSE_API_KEY as a server-side environment variable.

Errors

Failed requests throw SocialhoseError with status, path, and response body when available.

import { SocialhoseError } from '@socialhose/api';

try {
  await socialhose.getCampaign('missing');
} catch (error) {
  if (error instanceof SocialhoseError) {
    console.error(error.status, error.path, error.body);
  }
}

Publishing

pnpm test
pnpm typecheck
pnpm build
npm publish --access public --provenance