docs: expand JavaScript SDK documentation

This commit is contained in:
Mo Elzubeir
2026-05-29 13:35:09 -05:00
parent 252ea713b1
commit c860cf6d88
11 changed files with 850 additions and 116 deletions
+9
View File
@@ -29,6 +29,15 @@ const mentions = await socialhose.getMentions({
console.log(mentions.count);
```
Documentation lives with the package:
- [Quick README](./sdks/javascript/README.md)
- [Usage guide](./sdks/javascript/docs/GUIDE.md)
- [API reference](./sdks/javascript/docs/API.md)
- [Entity analytics](./sdks/javascript/docs/ENTITY_ANALYTICS.md)
- [Caching and retries](./sdks/javascript/docs/CACHING_AND_RETRIES.md)
- [Examples](./sdks/javascript/examples)
## Development
Use pnpm 9.x.
+31 -114
View File
@@ -2,13 +2,15 @@
TypeScript SDK for the Socialhose Public API.
Use it from backend TypeScript/JavaScript to fetch campaigns, analytics, mentions, mailing lists, and SDK-composed entity analytics with authentication, retries, timeouts, and caching handled consistently.
## Install
```bash
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.
Node 18+ is required because the SDK uses built-in `fetch`, `Response`, and `AbortSignal.timeout`. You can pass a custom `fetch` implementation if needed.
## Quickstart
@@ -28,6 +30,14 @@ const mentions = await socialhose.getMentions({
console.log(mentions.count, mentions.results[0]?.content);
```
## Documentation
- [Usage guide](./docs/GUIDE.md) — what the SDK is, setup, common workflows, operational guidance.
- [API reference](./docs/API.md) — every public class, function, method, option, and exported type.
- [Entity analytics](./docs/ENTITY_ANALYTICS.md) — how term-level analytics are built, accuracy safeguards, rate-limit guidance.
- [Caching and retries](./docs/CACHING_AND_RETRIES.md) — cache contract, retry policy, failure semantics.
- [Examples](./examples) — runnable TypeScript examples.
## Configuration
```ts
@@ -42,127 +52,29 @@ const socialhose = new SocialhoseClient({
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
## Main capabilities
### Campaigns
- `getCampaigns()`
- `getCampaign(id)`
- Campaign discovery: `getCampaigns()`, `getCampaign(id)`, `getCampaignIdByMatch(match)`.
- Analytics: `getOverview()`, `getTimeline()`, `getSentiment()`, `getShareOfVoice()`, `getPlatforms()`, `getTopKeywords()`, `getTrending()`, `getTopMentions()`.
- Mentions: `getMentions()` with campaign, date, platform, sentiment, text search, pagination, and ordering filters.
- Mailing lists: `getMailingLists()`, `inviteMailingListMember()`.
- Entity analytics: `getEntityBrief()`, `getEntityStats()`, `getEntityBriefs()`.
- Low-level access: `get(path, params)`, `post(path, body)`.
- Caching: built-in `MemoryCache`, disabling via `NoopCache`, or any custom `Cache` implementation.
### Analytics
- `getOverview(filters)`
- `getTimeline(filters)`
- `getSentiment(filters)`
- `getShareOfVoice(filters)`
- `getPlatforms(filters)`
- `getTopKeywords(filters)`
- `getTrending(filters)`
- `getTopMentions(filters)`
## Entity analytics warning
### 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
Entity analytics fan out multiple `/mentions/` requests per term. Use `cacheTtlMs`, `revalidateSeconds`, or a persistent shared cache to stay under the approximate 60 req/min API-key rate limit.
```ts
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',
const stats = await socialhose.getEntityStats('RSF', 'campaign-id', {
revalidateSeconds: 900,
});
```
## 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:
```ts
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:
```ts
await socialhose.getMentions(
{ content_search: 'ozempic' },
{ revalidateSeconds: 3600 },
);
```
## Entity Analytics
Search across all mentions for a specific term, person, or organization:
```ts
// 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.
Failed requests throw `SocialhoseError` with `status`, `path`, `body`, and `cause` when available.
```ts
import { SocialhoseError } from '@socialhose/api';
@@ -176,12 +88,17 @@ try {
}
```
## Publishing
## Development
```bash
pnpm install
pnpm test
pnpm typecheck
pnpm build
npm publish --access public --provenance
```
Publishing:
```bash
npm publish --access public --provenance
```
+366
View File
@@ -0,0 +1,366 @@
# API Reference
Public API reference for `@socialhose/api`, the TypeScript SDK for the Socialhose Public API.
The SDK is intended for server-side JavaScript/TypeScript. It authenticates requests with an API key, wraps Socialhose REST endpoints, normalizes common response shapes, retries transient failures, applies request timeouts, and caches GET responses.
## Package exports
```ts
import {
SocialhoseClient,
createSocialhoseClient,
SocialhoseError,
MemoryCache,
NoopCache,
type Cache,
type SocialhoseClientOptions,
type RequestOptions,
type AnalyticsFilters,
type MentionFilters,
} from '@socialhose/api';
```
## Authentication
Every request sends:
- `Authorization: Api-Key <apiKey>`
- `Accept: application/json`
- a browser-like `User-Agent` by default
- `Content-Type: application/json` for `POST`
Do not use this SDK directly in browser code. Keep `SOCIALHOSE_API_KEY` on the server.
## `SocialhoseClientOptions`
Configuration object for `new SocialhoseClient(options)` and `createSocialhoseClient(options)`.
- `apiKey: string` — Socialhose API key. Required; empty strings fail on first request.
- `baseUrl?: string` — API root. Default: `https://socialhose.net/api/public/v1`.
- `userAgent?: string` — request user-agent. Default is Chrome-like because the API edge may reject generic Node clients.
- `fetch?: typeof fetch` — custom fetch implementation for tests, instrumentation, or runtimes without global fetch.
- `timeoutMs?: number` — per-attempt timeout. Default: `8000`.
- `retries?: number` — retry count after the first attempt. Default: `3`.
- `retryDelayMs?: (attempt: number) => number` — retry backoff function. Default: exponential backoff with jitter.
- `cacheTtlMs?: number` — default GET cache TTL in milliseconds. Default: `60000`; set `0` to disable the built-in memory cache.
- `cache?: Cache` — custom cache implementation. If supplied, it replaces the internal cache.
- `defaultHeaders?: Record<string, string>` — extra headers sent with every request.
## `RequestOptions`
Per-request options.
- `revalidateSeconds?: number` — override cache TTL for this call, in seconds. Passed only to `Cache.set`; not sent to the API.
- `signal?: AbortSignal` — caller abort signal combined with the SDK timeout.
- `headers?: Record<string, string>` — request-specific headers.
## `new SocialhoseClient(options)`
Creates an authenticated API client.
```ts
const socialhose = new SocialhoseClient({
apiKey: process.env.SOCIALHOSE_API_KEY!,
timeoutMs: 8_000,
retries: 3,
cacheTtlMs: 60_000,
});
```
## `createSocialhoseClient(options)`
Factory wrapper around the constructor.
```ts
const socialhose = createSocialhoseClient({ apiKey });
```
Returns `SocialhoseClient`.
## Low-level HTTP methods
### `client.get<T>(path, params?, options?)`
Performs an authenticated cached GET request.
- `path: string` — endpoint path, with or without leading slash.
- `params?: QueryParams` — query parameters. `undefined`, `null`, and `''` values are omitted.
- `options?: RequestOptions` — cache TTL, abort signal, and extra headers.
Returns `Promise<T>`.
Behavior:
- Builds the URL from `baseUrl`, `path`, and `params`.
- Uses the full URL as the cache key.
- Retries network errors, timeouts, `429`, and `5xx` responses.
- Throws `SocialhoseError` for non-OK responses after retries.
### `client.post<T>(path, body, options?)`
Performs an authenticated JSON POST request.
- `path: string` — endpoint path.
- `body: unknown` — JSON-serialized request body.
- `options?: RequestOptions` — abort signal and extra headers.
Returns `Promise<{ status: number; data: T | null }>`.
Behavior:
- Never uses the GET cache.
- Sends `Content-Type: application/json`.
- Returns the HTTP status so higher-level methods can normalize status-specific outcomes.
- Lets POST `409` responses through for conflict normalization.
## Campaign methods
### `client.getCampaigns(options?)`
Fetches first-page campaigns.
Endpoint: `GET /campaigns/?page=1`
Returns `Promise<Campaign[]>`.
Use it to list accessible campaigns or populate campaign selectors. The helper reads page 1 only; use `get<Paginated<Campaign>>()` for manual pagination.
### `client.getCampaign(id, options?)`
Fetches one campaign by ID.
Endpoint: `GET /campaigns/{id}/`
Returns `Promise<Campaign>`.
Throws `SocialhoseError` when the campaign is missing or inaccessible.
### `client.getCampaignIdByMatch(match, options?)`
Returns the first campaign ID whose name contains `match`, case-insensitively.
Returns `Promise<string | undefined>`.
If campaign fetching fails or no campaign matches, returns `undefined`. Use exact IDs when names may collide.
## Analytics filters
`AnalyticsFilters` are passed through as query params:
- `campaign_ids?: string` — one ID or comma-separated IDs.
- `date_from?: string` — lower date bound, commonly `YYYY-MM-DD`.
- `date_to?: string` — upper date bound, commonly `YYYY-MM-DD`.
- `platforms?: string` — platform or comma-separated platforms.
- `sentiments?: string` — sentiment or comma-separated sentiments.
## Analytics methods
### `client.getOverview(filters?, options?)`
Endpoint: `GET /analytics/overview/`
Returns `Promise<Overview>` with total mentions, authors, estimated reach, sentiment distribution, platform breakdown, engagement, and growth metrics.
The SDK coerces `total_mentions` and `total_authors` to numbers because some API responses may return numeric strings.
### `client.getTimeline(filters?, options?)`
Endpoint: `GET /analytics/timeline/`
Filters include `interval?: 'day' | 'week' | 'month'`, defaulting to `day`.
Returns `Promise<TimelinePoint[]>`. If the API response lacks `series`, returns `[]`.
### `client.getSentiment(filters?, options?)`
Endpoint: `GET /analytics/sentiment/`
Returns `Promise<{ distribution: SentimentSplit; by_platform: Record<string, SentimentSplit> }>`.
### `client.getShareOfVoice(filters?, options?)`
Endpoint: `GET /analytics/share-of-voice/`
Returns `Promise<{ total_mentions: number; campaigns: ShareOfVoiceItem[] }>`.
Each campaign row includes mention count, share percentage, engagement, and sentiment.
### `client.getPlatforms(filters?, options?)`
Endpoint: `GET /analytics/platforms/`
Returns `Promise<PlatformStat[]>`. If the API response lacks `platforms`, returns `[]`.
### `client.getTopKeywords(filters?, options?)`
Endpoint: `GET /analytics/top-keywords/`
Filters include `limit?: number`, defaulting to `12`.
Returns `Promise<KeywordStat[]>`. If the API response lacks `keywords`, returns `[]`.
### `client.getTrending(filters?, options?)`
Endpoint: `GET /analytics/trending/`
Filters include `limit?: number`, defaulting to `8`.
Returns `Promise<TrendingItem[]>`. If the API response lacks `trending`, returns `[]`.
### `client.getTopMentions(filters?, options?)`
Endpoint: `GET /analytics/top-mentions/`
Filters include `limit?: number`, defaulting to `6`.
Returns `Promise<TopMention[]>`. If the API response lacks `mentions`, returns `[]`.
## Mention methods
### `client.getMentions(filters?, optionsOrRevalidate?)`
Endpoint: `GET /mentions/`
Returns `Promise<Paginated<Mention>>`.
`MentionFilters` include all `AnalyticsFilters` plus:
- `page?: number` — page number, defaulting to `1`.
- `content_search?: string` — text search term.
- `ordering?: string` — sort key, such as `-published_at` or `-engagement_count`.
The second argument may be `RequestOptions` or the legacy shorthand `number` for `revalidateSeconds`.
## Mailing-list methods
### `client.getMailingLists(options?)`
Endpoint: `GET /mailing-lists/?page=1`
Returns `Promise<MailingList[]>` from the first page.
### `client.inviteMailingListMember(listId, invite, options?)`
Endpoint: `POST /mailing-lists/{listId}/members/`
`invite` fields:
- `email: string`
- `first_name?: string`
- `last_name?: string`
- `invitation_message?: string`
Returns `Promise<InviteResult>`:
- `{ outcome: 'invited', invitation }` for `201` plus `status: 'invited'`.
- `{ outcome: 'already', detail }` for `409` conflicts.
- `{ outcome: 'error', detail }` for unexpected successful/conflict response shapes.
Other non-OK statuses throw `SocialhoseError`.
## Entity analytics methods
Entity analytics are SDK-composed views built on `/mentions/` with `content_search`. They provide term/person/org/topic analytics when native analytics endpoints are campaign-scoped.
### `client.getEntityBrief(term, campaignId?, options?)`
Runs one mention search ordered by engagement.
Returns `Promise<EntityBrief>`:
- `term` — requested search term.
- `total` — exact API count.
- `exact` — true when the sample covers the full population.
- `sentiment` — sample-derived sentiment split.
- `platformMix` — sample-derived platform counts.
- `sample` — first page ordered by engagement.
`total` is exact. Sentiment and platform mix are exact only when `exact` is true.
### `client.getEntityStats(term, campaignId?, options?)`
Builds a richer dashboard for one term.
Returns `Promise<EntityStats>` with `EntityBrief` fields plus:
- `recent` — newest mentions.
- `recent7d` — latest seven-day count from the sparkline.
- `prev7d` — previous seven-day count.
- `momentumPct` — seven-day percentage change, or `null` if not computable.
- `sparkline` — daily counts for the last 14 days when date faceting is trustworthy.
Failure model:
- The initial brief must succeed.
- Follow-up subrequests are best-effort.
- Exact facets are used only when reconciliation checks pass; otherwise sample-derived values are retained.
### `client.getEntityBriefs(terms, campaignId?, concurrency?, options?)`
Fetches many entity briefs with bounded concurrency.
Returns `Promise<Map<string, EntityBrief>>`.
Failed terms are omitted from the map. Default concurrency is `20`; reduce it under rate limits.
## Cache API
### `Cache`
```ts
interface Cache {
get(key: string): Promise<unknown | undefined>;
set(key: string, value: unknown, ttlMs: number): Promise<void>;
delete(key: string): Promise<void>;
}
```
The key is the full GET URL. `get()` returns `undefined` on cache miss. `ttlMs` is in milliseconds.
### `MemoryCache`
In-memory `Map` cache with per-entry TTL. Good for tests, scripts, and single-process services. Not enough for distributed rate-limit protection.
### `NoopCache`
Cache implementation that never stores values. Use it to force fresh data or disable caching explicitly.
### `client.clearCache()`
Deletes cache keys used by the current client instance. It is not a global purge for shared cache backends.
## `SocialhoseError`
Structured request failure.
Fields:
- `name: 'SocialhoseError'`
- `status?: number`
- `path: string`
- `body?: string`
- `cause?: unknown`
Thrown after retry exhaustion or unsupported non-OK HTTP responses.
## Core data types
- `Sentiment``'positive' | 'negative' | 'neutral'`.
- `SentimentSplit``{ positive: number; negative: number; neutral: number }`.
- `Paginated<T>``{ count, next, previous, results }`.
- `Campaign` — campaign metadata.
- `Overview` — aggregate analytics.
- `TimelinePoint` — one time-series bucket.
- `ShareOfVoiceItem` — campaign comparison row.
- `PlatformStat` — platform count and engagement.
- `KeywordStat` — keyword count and sentiment.
- `TrendingItem` — keyword momentum.
- `TopMention` — compact high-impact mention.
- `Mention` — full mention record with content, engagement, metadata, and author profile.
- `MailingList` — mailing-list metadata.
- `MailingListInvitation` — mailing-list invitation state.
- `InviteResult` — normalized invite result.
- `PlatformShare` — entity analytics platform count.
- `EntityBrief` — compact term analytics.
- `EntityStats` — full term dashboard.
+104
View File
@@ -0,0 +1,104 @@
# Caching, Retries, and Failure Semantics
## GET lifecycle
1. Build full URL from `baseUrl`, `path`, and query params.
2. Check cache by full URL.
3. Fetch with auth headers and timeout on cache miss.
4. Retry network errors, timeout errors, `429`, and `5xx`.
5. Parse JSON.
6. Throw `SocialhoseError` for unsupported non-OK responses.
7. Store successful parsed response in cache.
## POST lifecycle
1. Build URL.
2. Fetch JSON body with auth headers and timeout.
3. Retry network errors, timeout errors, `429`, and `5xx`.
4. Parse JSON.
5. Return `{ status, data }`.
6. Never cache.
## Retry policy
Defaults:
```ts
retries: 3,
retryDelayMs: (attempt) => 400 * 2 ** attempt + Math.random() * 200,
timeoutMs: 8_000,
```
Retries apply to transient conditions only:
- network failures
- SDK timeout/abort failures
- `429 Too Many Requests`
- `5xx` server errors
Retries do not apply to normal client errors such as `400`, `401`, `403`, and `404`.
## Cache contract
```ts
interface Cache {
get(key: string): Promise<unknown | undefined>;
set(key: string, value: unknown, ttlMs: number): Promise<void>;
delete(key: string): Promise<void>;
}
```
The cache key is the full request URL. `ttlMs` is in milliseconds and comes from `cacheTtlMs` or per-request `revalidateSeconds`.
## Redis-style cache example
```ts
import { SocialhoseClient, type Cache } from '@socialhose/api';
class RedisJsonCache implements Cache {
constructor(private redis: {
get(k: string): Promise<string | null>;
set(k: string, v: string, mode: 'PX', ttl: number): Promise<unknown>;
del(k: string): Promise<unknown>;
}) {}
async get(key: string) {
const value = await this.redis.get(key);
return value == null ? undefined : JSON.parse(value);
}
async set(key: string, value: unknown, ttlMs: number) {
if (ttlMs <= 0) return;
await this.redis.set(key, JSON.stringify(value), 'PX', ttlMs);
}
async delete(key: string) {
await this.redis.del(key);
}
}
const socialhose = new SocialhoseClient({
apiKey: process.env.SOCIALHOSE_API_KEY!,
cache: new RedisJsonCache(redis),
});
```
## Method failure semantics
Campaign, analytics, mention, and list methods:
- Throw `SocialhoseError` after retry exhaustion or unsupported non-OK responses.
- Return normalized arrays when the API wraps arrays in properties such as `series`, `platforms`, or `keywords`.
`inviteMailingListMember()`:
- `201 + status: invited` -> `outcome: 'invited'`.
- `409` -> `outcome: 'already'`.
- Unexpected success/conflict shapes -> `outcome: 'error'`.
- Other non-OK statuses throw.
Entity methods:
- `getEntityBrief()` throws if the mention search fails.
- `getEntityStats()` requires the initial brief but treats later subrequests as best-effort.
- `getEntityBriefs()` skips failed terms and returns successful entries.
+85
View File
@@ -0,0 +1,85 @@
# Entity Analytics
Entity analytics are SDK-composed analytics for a single term, person, organization, hashtag, product, or incident.
They exist because Socialhose analytics endpoints are campaign-scoped, while `/mentions/` supports `content_search` and returns an exact `count`.
## `getEntityBrief()`
`getEntityBrief(term, campaignId?)` performs one mention search ordered by engagement.
It returns:
- exact total mention count
- top mention sample
- sample-derived sentiment split
- sample-derived platform mix
- `exact` flag indicating whether the sample covers the full population
This is cheap enough for grids, tables, and autocomplete-like entity lists.
## `getEntityStats()`
`getEntityStats(term, campaignId?)` expands a brief into a detail dashboard.
It fetches:
- engagement-ordered sample
- newest mentions
- sentiment facets
- platform facets
- cumulative date boundaries for a 14-day sparkline
The first brief request is required. Subrequests are best-effort; failures return fallbacks instead of zeroing out the entity.
## Accuracy safeguards
### Sentiment reconciliation
The SDK facets `/mentions/` by sentiment and accepts exact sentiment only when:
```txt
positive + negative + neutral === total
```
If the sum does not match, the SDK assumes a filter was ignored or misapplied and falls back to sample-derived sentiment.
### Platform reconciliation
The SDK facets `/mentions/` over known platform keys. If the platform facet sum exceeds the entity total, the SDK assumes the API ignored a filter and falls back to sample-derived platform mix.
A sum below total is allowed because some mentions may be on platforms outside the SDK's known list.
### Timeline cumulative differencing
The SDK avoids adjacent `[date_from, date_to]` daily windows because inclusive `date_to` can double-count boundary days.
Instead it requests cumulative counts with `date_from` only:
```txt
count(on/after day N) - count(on/after day N+1) = day N count
```
If the earliest cumulative count exceeds the known entity total, the SDK treats date-filtered `content_search` as untrustworthy and returns an empty sparkline rather than wrong bars.
## Rate-limit guidance
A single `getEntityStats()` can perform approximately:
- 1 brief request
- 1 recent request
- 3 sentiment facet requests
- 6 platform facet requests
- 15 timeline boundary requests
Use caching and keep batch concurrency low.
Recommended pattern:
```ts
const stats = await socialhose.getEntityStats('cholera', campaignId, {
revalidateSeconds: 900,
});
```
Use `getEntityBriefs()` for lists and call `getEntityStats()` only for selected detail views.
+173
View File
@@ -0,0 +1,173 @@
# Usage Guide
## What this SDK is
`@socialhose/api` is a TypeScript client for the Socialhose Public API. It is built for backend services, scripts, serverless functions, and dashboards that need campaign analytics, mention search, mailing-list management, and term-level entity analytics.
The SDK handles:
1. API-key authentication.
2. Typed endpoint helpers.
3. Timeouts, retries, and request cancellation.
4. GET caching with an injectable cache interface.
5. Entity analytics assembled from `/mentions/` when native analytics endpoints are campaign-scoped.
## Install
```bash
npm install @socialhose/api
```
Requires Node 18+ or a custom `fetch` implementation.
## Basic setup
```ts
import { SocialhoseClient } from '@socialhose/api';
const socialhose = new SocialhoseClient({
apiKey: process.env.SOCIALHOSE_API_KEY!,
});
```
Keep the API key server-side. Do not instantiate this client in browser code.
## Production setup
```ts
const socialhose = new SocialhoseClient({
apiKey: process.env.SOCIALHOSE_API_KEY!,
timeoutMs: 10_000,
retries: 3,
cacheTtlMs: 60_000,
});
```
Recommendations:
- Keep the default browser-like user-agent unless you have verified an alternative works.
- Use a shared cache such as Redis for multi-process or serverless deployments.
- Reduce entity batch concurrency if you see `429` responses.
- Use `revalidateSeconds` for dashboards so repeated views do not fan out fresh requests.
## List campaigns
```ts
const campaigns = await socialhose.getCampaigns();
for (const campaign of campaigns) {
console.log(campaign.id, campaign.name, campaign.status);
}
```
## Search mentions
```ts
const page = await socialhose.getMentions({
campaign_ids: 'campaign-id',
content_search: 'hospital',
platforms: 'twitter,reddit',
sentiments: 'negative',
ordering: '-published_at',
});
console.log(page.count);
console.log(page.results[0]?.content);
```
## Fetch campaign dashboard analytics
```ts
const filters = { campaign_ids: 'campaign-id', date_from: '2026-05-01' };
const [overview, timeline, sentiment, platforms, topMentions] = await Promise.all([
socialhose.getOverview(filters),
socialhose.getTimeline({ ...filters, interval: 'day' }),
socialhose.getSentiment(filters),
socialhose.getPlatforms(filters),
socialhose.getTopMentions({ ...filters, limit: 10 }),
]);
```
## Analyze a term or entity
```ts
const stats = await socialhose.getEntityStats('RSF', 'campaign-id', {
revalidateSeconds: 900,
});
console.log({
total: stats.total,
sentiment: stats.sentiment,
platformMix: stats.platformMix,
momentumPct: stats.momentumPct,
topUrls: stats.sample.slice(0, 3).map((m) => m.url),
});
```
Use `getEntityBrief()` for cheap cards/lists and `getEntityStats()` for detail pages.
## Batch entity briefs
```ts
const briefs = await socialhose.getEntityBriefs(
['Burhan', 'Hemedti', 'SAF', 'RSF'],
'campaign-id',
5,
{ revalidateSeconds: 3600 },
);
```
Failed terms are skipped. Lower concurrency if rate limits are visible.
## Pagination
`getCampaigns()` and `getMailingLists()` return first-page arrays. `getMentions()` exposes a `page` filter.
For manual pagination, use `get()`:
```ts
let page = 1;
while (true) {
const response = await socialhose.get('/mentions/', { page, content_search: 'cholera' });
// process response.results
if (!response.next) break;
page += 1;
}
```
## Error handling
```ts
import { SocialhoseError } from '@socialhose/api';
try {
await socialhose.getOverview({ campaign_ids: 'bad-id' });
} catch (error) {
if (error instanceof SocialhoseError) {
console.error({ status: error.status, path: error.path, body: error.body });
} else {
throw error;
}
}
```
## Caching
GET requests are cached; POST requests are not.
```ts
await socialhose.getMentions(
{ content_search: 'sudan' },
{ revalidateSeconds: 900 },
);
```
The default cache is process-local. Use a persistent/shared cache for production dashboards and rate-limit control.
## Operational pitfalls
- Entity analytics are request-heavy. One `getEntityStats()` call can issue roughly 20+ requests.
- The API rate limit is roughly 60 requests/minute per API key.
- The entity timeline uses cumulative differencing intentionally to avoid inclusive `date_to` double-counting.
- Exact entity sentiment/platform values are used only when facet counts reconcile with the known total.
- `409` mailing-list conflicts are normalized to `{ outcome: 'already' }`; other non-OK responses throw.
+29
View File
@@ -0,0 +1,29 @@
import { SocialhoseClient, SocialhoseError } from '@socialhose/api';
const socialhose = new SocialhoseClient({
apiKey: process.env.SOCIALHOSE_API_KEY!,
});
try {
const campaigns = await socialhose.getCampaigns();
const campaignId = campaigns[0]?.id;
const mentions = await socialhose.getMentions({
campaign_ids: campaignId,
content_search: 'hospital',
ordering: '-published_at',
});
console.log({
campaigns: campaigns.length,
mentionCount: mentions.count,
firstMention: mentions.results[0]?.url,
});
} catch (error) {
if (error instanceof SocialhoseError) {
console.error({ status: error.status, path: error.path, body: error.body });
process.exitCode = 1;
} else {
throw error;
}
}
@@ -0,0 +1,26 @@
import { SocialhoseClient } from '@socialhose/api';
const socialhose = new SocialhoseClient({
apiKey: process.env.SOCIALHOSE_API_KEY!,
cacheTtlMs: 15 * 60 * 1000,
});
const campaignId = process.argv[2];
const term = process.argv[3] ?? 'RSF';
const stats = await socialhose.getEntityStats(term, campaignId, {
revalidateSeconds: 15 * 60,
});
console.log(JSON.stringify({
term: stats.term,
total: stats.total,
exact: stats.exact,
sentiment: stats.sentiment,
platformMix: stats.platformMix,
recent7d: stats.recent7d,
prev7d: stats.prev7d,
momentumPct: stats.momentumPct,
sparkline: stats.sparkline,
topUrls: stats.sample.slice(0, 5).map((m) => m.url),
}, null, 2));
+2
View File
@@ -7,6 +7,8 @@
"sideEffects": false,
"files": [
"dist",
"docs",
"examples",
"README.md",
"LICENSE"
],
+12 -2
View File
@@ -1,12 +1,22 @@
/**
* Pluggable cache contract used by SocialhoseClient for GET responses.
* The SDK uses the full request URL as the key.
*/
export interface Cache {
/** Return a cached value, or `undefined` on miss/expiry. */
get(key: string): Promise<unknown | undefined>;
/** Store a value for the given TTL in milliseconds. */
set(key: string, value: unknown, ttlMs: number): Promise<void>;
/** Delete one cache entry. */
delete(key: string): Promise<void>;
}
type Entry = { at: number; value: unknown; ttlMs: number };
/** In-memory Map-backed cache with per-entry TTL. */
/**
* In-memory Map-backed cache with per-entry TTL.
* Best for tests, scripts, and single-process services.
*/
export class MemoryCache implements Cache {
private readonly map = new Map<string, Entry>();
@@ -29,7 +39,7 @@ export class MemoryCache implements Cache {
}
}
/** No-op cache — all operations are no-ops; useful for disabling caching. */
/** Cache implementation that never stores values; useful for disabling caching. */
export class NoopCache implements Cache {
async get(_key: string): Promise<undefined> {
return undefined;
+13
View File
@@ -2,11 +2,13 @@ export type { Cache } from './cache';
export { MemoryCache, NoopCache } from './cache';
import { type Cache, MemoryCache, NoopCache } from './cache';
/** Sentiment label assigned to a mention by Socialhose. */
export type Sentiment = 'positive' | 'negative' | 'neutral';
export type SentimentSplit = { positive: number; negative: number; neutral: number };
export type QueryValue = string | number | boolean | null | undefined;
export type QueryParams = Record<string, QueryValue>;
/** Configuration for {@link SocialhoseClient}. */
export interface SocialhoseClientOptions {
apiKey: string;
baseUrl?: string;
@@ -21,6 +23,7 @@ export interface SocialhoseClientOptions {
defaultHeaders?: Record<string, string>;
}
/** Per-call controls for cache TTL, cancellation, and extra headers. */
export interface RequestOptions {
/**
* Reserved for cache-implementation use; not forwarded to fetch.
@@ -226,6 +229,7 @@ export interface EntityStats extends EntityBrief {
sparkline: { date: string; count: number }[];
}
/** Structured error thrown for failed Socialhose requests. */
export class SocialhoseError extends Error {
readonly status?: number;
readonly path: string;
@@ -312,6 +316,7 @@ function platformMixOf(mentions: Mention[]): PlatformShare[] {
.sort((a, b) => b.count - a.count);
}
/** Authenticated server-side client for the Socialhose Public API. */
export class SocialhoseClient {
readonly apiKey: string;
readonly baseUrl: string;
@@ -326,6 +331,7 @@ export class SocialhoseClient {
private readonly cacheImpl: Cache;
private readonly usedCacheKeys = new Set<string>();
/** Create a client with authentication, retry, timeout, and cache settings. */
constructor(options: SocialhoseClientOptions) {
if (!options.fetch && typeof fetch === 'undefined') throw new Error('A fetch implementation is required');
this.apiKey = options.apiKey;
@@ -340,12 +346,14 @@ export class SocialhoseClient {
this.cacheImpl = options.cache ?? (this.cacheTtlMs > 0 ? new MemoryCache() : new NoopCache());
}
/** Delete cache entries used by this client instance. */
async clearCache(): Promise<void> {
const keys = [...this.usedCacheKeys];
this.usedCacheKeys.clear();
await Promise.all(keys.map((k) => this.cacheImpl.delete(k)));
}
/** Perform an authenticated cached GET request. */
async get<T>(path: string, params: QueryParams = {}, options: RequestOptions = {}): Promise<T> {
const query = encodeParams(params);
const url = `${joinUrl(this.baseUrl, path)}${query ? `?${query}` : ''}`;
@@ -360,20 +368,24 @@ export class SocialhoseClient {
return value;
}
/** Perform an authenticated JSON POST request. */
async post<T>(path: string, body: unknown, options: RequestOptions = {}): Promise<{ status: number; data: T | null }> {
const url = joinUrl(this.baseUrl, path);
return this.requestWithStatus<T>('POST', path, url, body, options);
}
/** Fetch the first page of campaigns. */
async getCampaigns(options: RequestOptions = {}): Promise<Campaign[]> {
const d = await this.get<Paginated<Campaign>>('/campaigns/', { page: 1 }, options);
return d.results;
}
/** Fetch a single campaign by ID. */
getCampaign(id: string, options: RequestOptions = {}): Promise<Campaign> {
return this.get<Campaign>(`/campaigns/${id}/`, {}, options);
}
/** Fetch aggregate overview analytics. */
async getOverview(filters: AnalyticsFilters = {}, options: RequestOptions = {}): Promise<Overview> {
const d = await this.get<Overview>('/analytics/overview/', filters as QueryParams, options);
return { ...d, total_mentions: num(d.total_mentions), total_authors: num(d.total_authors) };
@@ -745,6 +757,7 @@ export class SocialhoseClient {
}
}
/** Create a {@link SocialhoseClient}; convenience wrapper around the constructor. */
export function createSocialhoseClient(options: SocialhoseClientOptions): SocialhoseClient {
return new SocialhoseClient(options);
}