orangeslice 2.4.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.js +14 -3
- package/dist/ctx.d.ts +15 -0
- package/dist/ctx.js +1 -0
- package/dist/index.d.ts +1 -1
- package/docs/integrations/index.md +28 -0
- package/docs/integrations/websiteTracking/index.md +86 -0
- package/docs/integrations/websiteTracking/setupTracking.md +110 -0
- package/docs/services/ctx/index.md +22 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -105,6 +105,14 @@ async function fetchWithRedirect(url, init) {
|
|
|
105
105
|
}
|
|
106
106
|
return res;
|
|
107
107
|
}
|
|
108
|
+
function formatRequestError(endpoint, status, message) {
|
|
109
|
+
// 4xx → caller-fixable problem: surface just the server's message so it reads
|
|
110
|
+
// like a normal validation error (e.g. `Cannot resolve sheet "leads"`).
|
|
111
|
+
// 5xx → infra-shaped problem: keep the noisy prefix so callers can grep logs.
|
|
112
|
+
if (status >= 400 && status < 500)
|
|
113
|
+
return message;
|
|
114
|
+
return `[orangeslice] ${endpoint}: ${status} ${message}`;
|
|
115
|
+
}
|
|
108
116
|
async function pollUntilComplete(baseUrl, endpoint, pending) {
|
|
109
117
|
const pollUrl = resolvePollUrl(baseUrl, pending);
|
|
110
118
|
const timeoutAt = Date.now() + POLL_TIMEOUT_MS;
|
|
@@ -122,11 +130,14 @@ async function pollUntilComplete(baseUrl, endpoint, pending) {
|
|
|
122
130
|
}
|
|
123
131
|
if (!res.ok) {
|
|
124
132
|
const message = asErrorMessage(data) || JSON.stringify(data);
|
|
125
|
-
throw new Error(
|
|
133
|
+
throw new Error(formatRequestError(endpoint, res.status, message));
|
|
126
134
|
}
|
|
127
135
|
const message = asErrorMessage(data);
|
|
128
136
|
if (message) {
|
|
129
|
-
|
|
137
|
+
// 2xx body carrying an `error` field is semantically a caller-fixable
|
|
138
|
+
// error delivered asynchronously — treat it like a 4xx so the message
|
|
139
|
+
// surfaces cleanly without the `[orangeslice] /endpoint: …` prefix.
|
|
140
|
+
throw new Error(formatRequestError(endpoint, 400, message));
|
|
130
141
|
}
|
|
131
142
|
return data;
|
|
132
143
|
}
|
|
@@ -153,7 +164,7 @@ async function post(endpoint, payload) {
|
|
|
153
164
|
if (!res.ok) {
|
|
154
165
|
const data = await readResponseBody(res);
|
|
155
166
|
const message = asErrorMessage(data) || (typeof data === "string" ? data : JSON.stringify(data));
|
|
156
|
-
throw new Error(
|
|
167
|
+
throw new Error(formatRequestError(endpoint, res.status, message));
|
|
157
168
|
}
|
|
158
169
|
const data = await readResponseBody(res);
|
|
159
170
|
if (isPendingResponse(data)) {
|
package/dist/ctx.d.ts
CHANGED
|
@@ -25,6 +25,20 @@ export interface SpreadsheetListItem {
|
|
|
25
25
|
orgId: string | null;
|
|
26
26
|
userId: string | null;
|
|
27
27
|
}
|
|
28
|
+
export interface SpreadsheetDescribeColumn {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}
|
|
32
|
+
export interface SpreadsheetDescribeSheet {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
columns: SpreadsheetDescribeColumn[];
|
|
36
|
+
}
|
|
37
|
+
export interface SpreadsheetDescribeResult {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
sheets: SpreadsheetDescribeSheet[];
|
|
41
|
+
}
|
|
28
42
|
export interface SqlQueryResult {
|
|
29
43
|
rows: Record<string, unknown>[];
|
|
30
44
|
rowIds: string[];
|
|
@@ -61,6 +75,7 @@ export declare const ctx: {
|
|
|
61
75
|
sql: (spreadsheetId: string, sql: string) => Promise<SqlResult>;
|
|
62
76
|
spreadsheet: (spreadsheetId: string) => {
|
|
63
77
|
sql: (sql: string) => Promise<SqlResult>;
|
|
78
|
+
describe: () => Promise<SpreadsheetDescribeResult>;
|
|
64
79
|
sheet: (sheetName: string) => {
|
|
65
80
|
addRows: (rows: Record<string, unknown> | Record<string, unknown>[]) => Promise<RowsAddResult>;
|
|
66
81
|
};
|
package/dist/ctx.js
CHANGED
|
@@ -44,6 +44,7 @@ function createSpreadsheetHandle(spreadsheetId) {
|
|
|
44
44
|
assertNotRun(sql);
|
|
45
45
|
return (0, api_1.post)("/ctx/sql", { spreadsheetId, sql });
|
|
46
46
|
},
|
|
47
|
+
describe: () => (0, api_1.post)("/ctx/spreadsheet/describe", { spreadsheetId }),
|
|
47
48
|
sheet: (sheetName) => createSheetHandle(spreadsheetId, sheetName)
|
|
48
49
|
};
|
|
49
50
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { Skill, SkillListParams, SkillCreateParams, SkillUpdateParams } fro
|
|
|
7
7
|
export { findCareersPage, scrapeCareersPage } from "./careers";
|
|
8
8
|
export type { FindCareersPageParams, FindCareersPageResult, ScrapeCareersPageParams, ScrapeCareersPageResult, ScrapeCareersPageJob } from "./careers";
|
|
9
9
|
export { ctx } from "./ctx";
|
|
10
|
-
export type { Spreadsheet, SpreadsheetListItem, SqlResult, SqlQueryResult, SqlActionResult, RowsAddResult } from "./ctx";
|
|
10
|
+
export type { Spreadsheet, SpreadsheetListItem, SpreadsheetDescribeResult, SpreadsheetDescribeSheet, SpreadsheetDescribeColumn, SqlResult, SqlQueryResult, SqlActionResult, RowsAddResult } from "./ctx";
|
|
11
11
|
export { linkedinSearch } from "./b2b";
|
|
12
12
|
export type { LinkedInSearchParams, LinkedInSearchResponse } from "./b2b";
|
|
13
13
|
export { crunchbaseSearch } from "./crunchbase";
|
|
@@ -240,6 +240,34 @@ await integrations.attio.createNote({
|
|
|
240
240
|
|
|
241
241
|
See [attio/](./attio/) for all available functions.
|
|
242
242
|
|
|
243
|
+
### Website tracking
|
|
244
|
+
|
|
245
|
+
LinkedIn-person identification for US-based website visitors. Customers
|
|
246
|
+
paste a single `<script>` tag on their site; each identified visit lands
|
|
247
|
+
as a row on the spreadsheet that owns the trigger.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Enroll a single domain — creates the trigger + sheet ingester, registers
|
|
251
|
+
// the domain in our master webhook routing table, adds it to the upstream
|
|
252
|
+
// allow-list, and returns the <script> the user pastes on their site.
|
|
253
|
+
const { script } = await integrations.websiteTracking.setupTracking({
|
|
254
|
+
domain: "acme.com",
|
|
255
|
+
sheetName: "Website Visitors"
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Inspect / remove later
|
|
259
|
+
const tracked = await integrations.websiteTracking.listTrackedDomains();
|
|
260
|
+
await integrations.websiteTracking.removeTracking("acme.com");
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Pricing: **80 credits per identified visitor** (charged at the master
|
|
264
|
+
webhook). Repeat visits and company-only pings are forwarded to the sheet
|
|
265
|
+
but don't cost credits. When the account runs out of credits the domain
|
|
266
|
+
is automatically paused; the next top-up resumes it.
|
|
267
|
+
|
|
268
|
+
See [websiteTracking/](./websiteTracking/) for the full recipe,
|
|
269
|
+
canonical visitor shape, and trigger code template.
|
|
270
|
+
|
|
243
271
|
### Gmail
|
|
244
272
|
|
|
245
273
|
Read and write emails from connected Google Gmail accounts.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integrations/websiteTracking
|
|
3
|
+
description: Track website visitors (LinkedIn person identification). Use when a user asks to identify who is visiting their site, build website-intent lists, or trigger outreach from anonymous traffic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Website Visitor Identification
|
|
7
|
+
|
|
8
|
+
Identify the LinkedIn profile of US-based visitors to a customer's
|
|
9
|
+
website. The customer pastes a single `<script>` tag on their site;
|
|
10
|
+
every identified visit lands as a row on the spreadsheet that owns the
|
|
11
|
+
trigger.
|
|
12
|
+
|
|
13
|
+
- **One shared tracking script** is pasted on the customer's site. No
|
|
14
|
+
per-customer external account, no logins, no separate billing.
|
|
15
|
+
- Every visitor ping fans out to a **master webhook** we own. The
|
|
16
|
+
master webhook looks up the captured domain, charges the customer's
|
|
17
|
+
Orange Slice credit balance, then forwards the visitor record to a
|
|
18
|
+
trigger on the customer's spreadsheet which writes it as a row.
|
|
19
|
+
|
|
20
|
+
## Pricing
|
|
21
|
+
|
|
22
|
+
**80 credits per identified visitor.** A visitor counts as "identified"
|
|
23
|
+
when we resolve both a LinkedIn URL and a first name on a non-repeat
|
|
24
|
+
visit. Company-only pings and repeat visitor pings are recorded for
|
|
25
|
+
context but NEVER charge credits.
|
|
26
|
+
|
|
27
|
+
When the customer's balance hits zero, the master webhook automatically
|
|
28
|
+
pauses their domain so they stop incurring charges. The next top-up
|
|
29
|
+
resumes the domain automatically — the customer doesn't have to re-paste
|
|
30
|
+
the script.
|
|
31
|
+
|
|
32
|
+
## The Recipe (call once per domain)
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const { script, triggerId } = await integrations.websiteTracking.setupTracking({
|
|
36
|
+
domain: "acme.com",
|
|
37
|
+
sheetName: "Website Visitors" // optional, defaults to this
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Hand `script` to the user — they paste it on their site.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`setupTracking` does all of the following atomically:
|
|
44
|
+
|
|
45
|
+
1. Creates a sheet-aware trigger that ingests visitor payloads into the
|
|
46
|
+
chosen sheet (uses `addRows` with `createMissingColumns: true` so the
|
|
47
|
+
user doesn't have to pre-create columns).
|
|
48
|
+
2. Inserts the domain → trigger mapping into our master routing table.
|
|
49
|
+
3. Calls the upstream allow-list API to register the domain.
|
|
50
|
+
4. Returns the canonical `<script>` tag the user pastes on their site.
|
|
51
|
+
|
|
52
|
+
If the same domain is re-enrolled it is idempotent — the existing
|
|
53
|
+
trigger and mapping are reused.
|
|
54
|
+
|
|
55
|
+
## After setupTracking — what to tell the user
|
|
56
|
+
|
|
57
|
+
1. Paste the returned `script` into the `<head>` of their website.
|
|
58
|
+
2. Let them know identified visitors will appear in the chosen sheet
|
|
59
|
+
within ~5 minutes of installing. Pricing: 80 credits per identified
|
|
60
|
+
person.
|
|
61
|
+
3. If they want to enrich rows further, point out that the trigger only
|
|
62
|
+
ingests — they should add **column code** (or just let the AI suggest
|
|
63
|
+
columns) for tasks like "compute company size" or "draft an outreach
|
|
64
|
+
email" so per-row work shows in the UI and is retryable.
|
|
65
|
+
|
|
66
|
+
## Multiple sites
|
|
67
|
+
|
|
68
|
+
A single account can track many domains, each with its own trigger and
|
|
69
|
+
sheet. Just call `setupTracking` once per domain. A domain can only be
|
|
70
|
+
tracked by ONE Orange Slice account at a time — re-enrolling from another
|
|
71
|
+
account throws unless the original account previously called
|
|
72
|
+
`removeTracking` (then the row is a tombstone and the new account can
|
|
73
|
+
claim it).
|
|
74
|
+
|
|
75
|
+
## Inspecting / removing
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// What's tracked on this spreadsheet?
|
|
79
|
+
const mappings = await integrations.websiteTracking.listTrackedDomains();
|
|
80
|
+
|
|
81
|
+
// Stop tracking a domain.
|
|
82
|
+
await integrations.websiteTracking.removeTracking("acme.com");
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
See [setupTracking.md](./setupTracking.md) for the full input/output
|
|
86
|
+
contract.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integrations/websiteTracking/setupTracking
|
|
3
|
+
description: Atomic enrollment helper — creates the receiving trigger, registers the domain in the master webhook table, adds the domain to the upstream allow-list, returns the user-facing <script> tag.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# integrations.websiteTracking.setupTracking
|
|
7
|
+
|
|
8
|
+
Enroll one customer domain into website-visitor tracking.
|
|
9
|
+
|
|
10
|
+
## Input
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
{
|
|
14
|
+
domain: string; // e.g. "acme.com" (with or without scheme/www)
|
|
15
|
+
sheetName?: string; // defaults to "Website Visitors"
|
|
16
|
+
triggerName?: string; // defaults to `Website visitors – {domain}`
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The `domain` is normalized: protocol, `www.`, paths, query, and ports are
|
|
21
|
+
stripped before storage. `"https://www.acme.com/pricing"` and `"acme.com"`
|
|
22
|
+
both store as `acme.com`.
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
{
|
|
28
|
+
script: string; // <script> tag for the user to paste
|
|
29
|
+
domain: string; // normalized domain
|
|
30
|
+
triggerId: string;
|
|
31
|
+
mappingId: string;
|
|
32
|
+
sheetName: string;
|
|
33
|
+
triggerName: string;
|
|
34
|
+
alreadyOwned: boolean; // true if mapping existed before this call
|
|
35
|
+
alreadyOnAllowList: boolean; // true if the upstream allow-list already had the domain
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Errors
|
|
40
|
+
|
|
41
|
+
- `Invalid domain` — `domain` couldn't be normalized to a host with a TLD.
|
|
42
|
+
- `Domain "<x>" is already claimed by another account` — every domain
|
|
43
|
+
belongs to exactly one Orange Slice account (a `removed` tombstone from
|
|
44
|
+
a prior teardown does NOT lock the domain).
|
|
45
|
+
|
|
46
|
+
## What the trigger does
|
|
47
|
+
|
|
48
|
+
The trigger code generated by this helper stays thin per the
|
|
49
|
+
[triggers-runtime.md](../../triggers-runtime.md) rule: it just normalizes
|
|
50
|
+
the upstream payload into the canonical visitor shape and pushes a row
|
|
51
|
+
into the user's chosen sheet with `createMissingColumns: true` and
|
|
52
|
+
`run: true`. Any enrichment / scoring belongs in column code on that
|
|
53
|
+
sheet.
|
|
54
|
+
|
|
55
|
+
## Canonical visitor shape
|
|
56
|
+
|
|
57
|
+
Every identified visit is delivered to the trigger as a record with
|
|
58
|
+
these fields:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
{
|
|
62
|
+
linkedinUrl: string | null;
|
|
63
|
+
firstName: string | null;
|
|
64
|
+
lastName: string | null;
|
|
65
|
+
title: string | null;
|
|
66
|
+
companyName: string | null;
|
|
67
|
+
businessEmail: string | null;
|
|
68
|
+
website: string | null;
|
|
69
|
+
industry: string | null;
|
|
70
|
+
employeeCount: number | string | null;
|
|
71
|
+
estimatedRevenue: string | null;
|
|
72
|
+
city: string | null;
|
|
73
|
+
state: string | null;
|
|
74
|
+
zip: string | null;
|
|
75
|
+
seenAt: string | null;
|
|
76
|
+
referrer: string | null;
|
|
77
|
+
capturedUrl: string | null;
|
|
78
|
+
tags: string | null;
|
|
79
|
+
isRepeatVisit: boolean;
|
|
80
|
+
extra?: Record<string, unknown>; // optional extras (UTMs, seniority, etc.)
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Example chat flow
|
|
85
|
+
|
|
86
|
+
When a user says "I want to track who visits my site", do this:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const result = await integrations.websiteTracking.setupTracking({
|
|
90
|
+
domain: "acme.com"
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Then tell the user:
|
|
94
|
+
// 1. Paste this snippet (`result.script`) into the <head> of acme.com.
|
|
95
|
+
// 2. Identified visitors will land in the "Website Visitors" sheet.
|
|
96
|
+
// 3. Each identified visitor costs 80 credits. Repeat / company-only
|
|
97
|
+
// visits are free.
|
|
98
|
+
// 4. If credits run out, tracking pauses automatically and resumes on
|
|
99
|
+
// the next top-up.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Pricing recap
|
|
103
|
+
|
|
104
|
+
Charged once per ping where:
|
|
105
|
+
- `linkedinUrl` is present, AND
|
|
106
|
+
- `firstName` is present, AND
|
|
107
|
+
- `isRepeatVisit` is not `true`.
|
|
108
|
+
|
|
109
|
+
Cost: **80 credits**. Company-only and repeat visits are still forwarded
|
|
110
|
+
to the sheet (so the user can analyze recurring traffic) but cost zero.
|
|
@@ -53,8 +53,30 @@ await ss.sheet("contacts").addRows([
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
- **`ss.sql(sql)`** — Execute EAV-SQL (same as `ctx.sql` but without needing to pass spreadsheetId).
|
|
56
|
+
- **`ss.describe()`** — Return the structural snapshot of the spreadsheet: its `id`, `name`, and an array of `sheets`, each with its `columns`. Use this to discover sheet/column names before issuing SQL — especially when attaching to a spreadsheet you didn't create.
|
|
56
57
|
- **`ss.sheet(name).addRows(rows)`** — Insert one or more rows into the named sheet. Accepts a single row object or an array. Missing columns are auto-created.
|
|
57
58
|
|
|
59
|
+
### Attaching to an existing spreadsheet
|
|
60
|
+
|
|
61
|
+
When the spreadsheet was created elsewhere (e.g. in the UI), use `describe()` to discover what's inside before running SQL:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const { spreadsheets } = await ctx.listSpreadsheets();
|
|
65
|
+
const target = spreadsheets.find((s) => s.name === "Find more paying customers V1")!;
|
|
66
|
+
|
|
67
|
+
const ss = ctx.spreadsheet(target.id);
|
|
68
|
+
const info = await ss.describe();
|
|
69
|
+
// info.sheets: [{ id, name, columns: [{ id, name }] }, ...]
|
|
70
|
+
|
|
71
|
+
for (const sheet of info.sheets) {
|
|
72
|
+
const cols = sheet.columns.map((c) => c.name).join(", ");
|
|
73
|
+
console.log(`${sheet.name}: ${cols}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Now you know the real sheet names; query safely
|
|
77
|
+
const result = await ss.sql(`SELECT * FROM "${info.sheets[0].name}" LIMIT 10`);
|
|
78
|
+
```
|
|
79
|
+
|
|
58
80
|
## EAV-SQL Reference
|
|
59
81
|
|
|
60
82
|
The `sql` method supports a subset of SQL mapped to Orange Slice's EAV storage:
|