docs: expand JavaScript SDK documentation
This commit is contained in:
@@ -29,6 +29,15 @@ const mentions = await socialhose.getMentions({
|
|||||||
console.log(mentions.count);
|
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
|
## Development
|
||||||
|
|
||||||
Use pnpm 9.x.
|
Use pnpm 9.x.
|
||||||
|
|||||||
+31
-114
@@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
TypeScript SDK for the Socialhose Public API.
|
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
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @socialhose/api
|
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
|
## Quickstart
|
||||||
|
|
||||||
@@ -28,6 +30,14 @@ const mentions = await socialhose.getMentions({
|
|||||||
console.log(mentions.count, mentions.results[0]?.content);
|
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
|
## Configuration
|
||||||
|
|
||||||
```ts
|
```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.
|
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
|
- Campaign discovery: `getCampaigns()`, `getCampaign(id)`, `getCampaignIdByMatch(match)`.
|
||||||
- `getCampaigns()`
|
- Analytics: `getOverview()`, `getTimeline()`, `getSentiment()`, `getShareOfVoice()`, `getPlatforms()`, `getTopKeywords()`, `getTrending()`, `getTopMentions()`.
|
||||||
- `getCampaign(id)`
|
- 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
|
## Entity analytics warning
|
||||||
- `getOverview(filters)`
|
|
||||||
- `getTimeline(filters)`
|
|
||||||
- `getSentiment(filters)`
|
|
||||||
- `getShareOfVoice(filters)`
|
|
||||||
- `getPlatforms(filters)`
|
|
||||||
- `getTopKeywords(filters)`
|
|
||||||
- `getTrending(filters)`
|
|
||||||
- `getTopMentions(filters)`
|
|
||||||
|
|
||||||
### Mentions
|
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.
|
||||||
- `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
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
await socialhose.getOverview({
|
const stats = await socialhose.getEntityStats('RSF', 'campaign-id', {
|
||||||
campaign_ids: 'campaign-id',
|
revalidateSeconds: 900,
|
||||||
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:
|
|
||||||
|
|
||||||
```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
|
## 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
|
```ts
|
||||||
import { SocialhoseError } from '@socialhose/api';
|
import { SocialhoseError } from '@socialhose/api';
|
||||||
@@ -176,12 +88,17 @@ try {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Publishing
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
pnpm install
|
||||||
pnpm test
|
pnpm test
|
||||||
pnpm typecheck
|
pnpm typecheck
|
||||||
pnpm build
|
pnpm build
|
||||||
npm publish --access public --provenance
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Publishing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm publish --access public --provenance
|
||||||
|
```
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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));
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
"docs",
|
||||||
|
"examples",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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 {
|
export interface Cache {
|
||||||
|
/** Return a cached value, or `undefined` on miss/expiry. */
|
||||||
get(key: string): Promise<unknown | undefined>;
|
get(key: string): Promise<unknown | undefined>;
|
||||||
|
/** Store a value for the given TTL in milliseconds. */
|
||||||
set(key: string, value: unknown, ttlMs: number): Promise<void>;
|
set(key: string, value: unknown, ttlMs: number): Promise<void>;
|
||||||
|
/** Delete one cache entry. */
|
||||||
delete(key: string): Promise<void>;
|
delete(key: string): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Entry = { at: number; value: unknown; ttlMs: number };
|
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 {
|
export class MemoryCache implements Cache {
|
||||||
private readonly map = new Map<string, Entry>();
|
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 {
|
export class NoopCache implements Cache {
|
||||||
async get(_key: string): Promise<undefined> {
|
async get(_key: string): Promise<undefined> {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ export type { Cache } from './cache';
|
|||||||
export { MemoryCache, NoopCache } from './cache';
|
export { MemoryCache, NoopCache } from './cache';
|
||||||
import { type Cache, 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 Sentiment = 'positive' | 'negative' | 'neutral';
|
||||||
export type SentimentSplit = { positive: number; negative: number; neutral: number };
|
export type SentimentSplit = { positive: number; negative: number; neutral: number };
|
||||||
export type QueryValue = string | number | boolean | null | undefined;
|
export type QueryValue = string | number | boolean | null | undefined;
|
||||||
export type QueryParams = Record<string, QueryValue>;
|
export type QueryParams = Record<string, QueryValue>;
|
||||||
|
|
||||||
|
/** Configuration for {@link SocialhoseClient}. */
|
||||||
export interface SocialhoseClientOptions {
|
export interface SocialhoseClientOptions {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
@@ -21,6 +23,7 @@ export interface SocialhoseClientOptions {
|
|||||||
defaultHeaders?: Record<string, string>;
|
defaultHeaders?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Per-call controls for cache TTL, cancellation, and extra headers. */
|
||||||
export interface RequestOptions {
|
export interface RequestOptions {
|
||||||
/**
|
/**
|
||||||
* Reserved for cache-implementation use; not forwarded to fetch.
|
* Reserved for cache-implementation use; not forwarded to fetch.
|
||||||
@@ -226,6 +229,7 @@ export interface EntityStats extends EntityBrief {
|
|||||||
sparkline: { date: string; count: number }[];
|
sparkline: { date: string; count: number }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Structured error thrown for failed Socialhose requests. */
|
||||||
export class SocialhoseError extends Error {
|
export class SocialhoseError extends Error {
|
||||||
readonly status?: number;
|
readonly status?: number;
|
||||||
readonly path: string;
|
readonly path: string;
|
||||||
@@ -312,6 +316,7 @@ function platformMixOf(mentions: Mention[]): PlatformShare[] {
|
|||||||
.sort((a, b) => b.count - a.count);
|
.sort((a, b) => b.count - a.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Authenticated server-side client for the Socialhose Public API. */
|
||||||
export class SocialhoseClient {
|
export class SocialhoseClient {
|
||||||
readonly apiKey: string;
|
readonly apiKey: string;
|
||||||
readonly baseUrl: string;
|
readonly baseUrl: string;
|
||||||
@@ -326,6 +331,7 @@ export class SocialhoseClient {
|
|||||||
private readonly cacheImpl: Cache;
|
private readonly cacheImpl: Cache;
|
||||||
private readonly usedCacheKeys = new Set<string>();
|
private readonly usedCacheKeys = new Set<string>();
|
||||||
|
|
||||||
|
/** Create a client with authentication, retry, timeout, and cache settings. */
|
||||||
constructor(options: SocialhoseClientOptions) {
|
constructor(options: SocialhoseClientOptions) {
|
||||||
if (!options.fetch && typeof fetch === 'undefined') throw new Error('A fetch implementation is required');
|
if (!options.fetch && typeof fetch === 'undefined') throw new Error('A fetch implementation is required');
|
||||||
this.apiKey = options.apiKey;
|
this.apiKey = options.apiKey;
|
||||||
@@ -340,12 +346,14 @@ export class SocialhoseClient {
|
|||||||
this.cacheImpl = options.cache ?? (this.cacheTtlMs > 0 ? new MemoryCache() : new NoopCache());
|
this.cacheImpl = options.cache ?? (this.cacheTtlMs > 0 ? new MemoryCache() : new NoopCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Delete cache entries used by this client instance. */
|
||||||
async clearCache(): Promise<void> {
|
async clearCache(): Promise<void> {
|
||||||
const keys = [...this.usedCacheKeys];
|
const keys = [...this.usedCacheKeys];
|
||||||
this.usedCacheKeys.clear();
|
this.usedCacheKeys.clear();
|
||||||
await Promise.all(keys.map((k) => this.cacheImpl.delete(k)));
|
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> {
|
async get<T>(path: string, params: QueryParams = {}, options: RequestOptions = {}): Promise<T> {
|
||||||
const query = encodeParams(params);
|
const query = encodeParams(params);
|
||||||
const url = `${joinUrl(this.baseUrl, path)}${query ? `?${query}` : ''}`;
|
const url = `${joinUrl(this.baseUrl, path)}${query ? `?${query}` : ''}`;
|
||||||
@@ -360,20 +368,24 @@ export class SocialhoseClient {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Perform an authenticated JSON POST request. */
|
||||||
async post<T>(path: string, body: unknown, options: RequestOptions = {}): Promise<{ status: number; data: T | null }> {
|
async post<T>(path: string, body: unknown, options: RequestOptions = {}): Promise<{ status: number; data: T | null }> {
|
||||||
const url = joinUrl(this.baseUrl, path);
|
const url = joinUrl(this.baseUrl, path);
|
||||||
return this.requestWithStatus<T>('POST', path, url, body, options);
|
return this.requestWithStatus<T>('POST', path, url, body, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch the first page of campaigns. */
|
||||||
async getCampaigns(options: RequestOptions = {}): Promise<Campaign[]> {
|
async getCampaigns(options: RequestOptions = {}): Promise<Campaign[]> {
|
||||||
const d = await this.get<Paginated<Campaign>>('/campaigns/', { page: 1 }, options);
|
const d = await this.get<Paginated<Campaign>>('/campaigns/', { page: 1 }, options);
|
||||||
return d.results;
|
return d.results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch a single campaign by ID. */
|
||||||
getCampaign(id: string, options: RequestOptions = {}): Promise<Campaign> {
|
getCampaign(id: string, options: RequestOptions = {}): Promise<Campaign> {
|
||||||
return this.get<Campaign>(`/campaigns/${id}/`, {}, options);
|
return this.get<Campaign>(`/campaigns/${id}/`, {}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch aggregate overview analytics. */
|
||||||
async getOverview(filters: AnalyticsFilters = {}, options: RequestOptions = {}): Promise<Overview> {
|
async getOverview(filters: AnalyticsFilters = {}, options: RequestOptions = {}): Promise<Overview> {
|
||||||
const d = await this.get<Overview>('/analytics/overview/', filters as QueryParams, options);
|
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) };
|
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 {
|
export function createSocialhoseClient(options: SocialhoseClientOptions): SocialhoseClient {
|
||||||
return new SocialhoseClient(options);
|
return new SocialhoseClient(options);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user