Skip to content

Commit 31e5050

Browse files
committed
feat(connectors): final audit — add verified scoping filters (grain team/type, granola+greenhouse createdBefore, gitlab milestone, rootly service/team/env), fix incident.io paused category + rootly severity-slug copy
1 parent dc711cb commit 31e5050

6 files changed

Lines changed: 130 additions & 11 deletions

File tree

apps/sim/connectors/gitlab/gitlab.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,16 @@ export const gitlabConnector: ConnectorConfig = {
366366
description:
367367
'Only sync issues with all of these labels (comma-separated). Applies only when syncing issues.',
368368
},
369+
{
370+
id: 'issueMilestone',
371+
title: 'Issue Milestone',
372+
type: 'short-input',
373+
required: false,
374+
mode: 'advanced',
375+
placeholder: 'e.g. v1.0 (milestone title)',
376+
description:
377+
'Only sync issues assigned to this milestone (exact title). Applies only when syncing issues.',
378+
},
369379
{
370380
id: 'maxItems',
371381
title: 'Max Items',
@@ -465,6 +475,9 @@ export const gitlabConnector: ConnectorConfig = {
465475
const issueLabels =
466476
typeof sourceConfig.issueLabels === 'string' ? sourceConfig.issueLabels.trim() : ''
467477
if (issueLabels) params.set('labels', issueLabels)
478+
const issueMilestone =
479+
typeof sourceConfig.issueMilestone === 'string' ? sourceConfig.issueMilestone.trim() : ''
480+
if (issueMilestone) params.set('milestone', issueMilestone)
468481

469482
const url = `${apiBase}/projects/${encodedProject}/issues?${params.toString()}`
470483
logger.info('Listing GitLab issues', {

apps/sim/connectors/grain/grain.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ function isParticipantScope(value: unknown): value is ParticipantScope {
117117
* - `after_datetime` — derived from `lookbackDays`; recordings on/after the window start
118118
* - `participant_scope` — `internal` or `external`
119119
* - `title_search` — substring match against recording titles
120+
* - `team` — recordings belonging to the given team UUID
121+
* - `meeting_type` — recordings classified as the given meeting type UUID
120122
*/
121123
function buildRecordingFilter(
122124
sourceConfig: Record<string, unknown>
@@ -136,6 +138,13 @@ function buildRecordingFilter(
136138
typeof sourceConfig.titleSearch === 'string' ? sourceConfig.titleSearch.trim() : ''
137139
if (titleSearch) filter.title_search = titleSearch
138140

141+
const team = typeof sourceConfig.teamId === 'string' ? sourceConfig.teamId.trim() : ''
142+
if (team) filter.team = team
143+
144+
const meetingType =
145+
typeof sourceConfig.meetingTypeId === 'string' ? sourceConfig.meetingTypeId.trim() : ''
146+
if (meetingType) filter.meeting_type = meetingType
147+
139148
return Object.keys(filter).length > 0 ? filter : undefined
140149
}
141150

@@ -363,6 +372,26 @@ export const grainConnector: ConnectorConfig = {
363372
placeholder: 'e.g. weekly standup',
364373
description: 'Only sync recordings whose title matches this text. Leave blank to sync all.',
365374
},
375+
{
376+
id: 'teamId',
377+
title: 'Team ID',
378+
type: 'short-input',
379+
required: false,
380+
mode: 'advanced',
381+
placeholder: 'e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890',
382+
description:
383+
'Only sync recordings belonging to this team (Grain team UUID). Leave blank to sync all teams.',
384+
},
385+
{
386+
id: 'meetingTypeId',
387+
title: 'Meeting Type ID',
388+
type: 'short-input',
389+
required: false,
390+
mode: 'advanced',
391+
placeholder: 'e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890',
392+
description:
393+
'Only sync recordings of this meeting type (Grain meeting type UUID). Leave blank to sync all types.',
394+
},
366395
],
367396

368397
listDocuments: async (

apps/sim/connectors/granola/granola.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,12 @@ function parseFolderId(sourceConfig: Record<string, unknown>): string | undefine
125125
}
126126

127127
/**
128-
* Parses the optional `createdAfter` date filter from source config. Returns a
129-
* normalized ISO 8601 string when the value is a valid date; otherwise returns
130-
* undefined so the request is not scoped to an invalid date.
128+
* Parses an optional ISO 8601 date filter from a named source-config field.
129+
* Returns a normalized ISO 8601 string when the value is a valid date; otherwise
130+
* returns undefined so the request is not scoped to an invalid date.
131131
*/
132-
function parseCreatedAfter(sourceConfig: Record<string, unknown>): string | undefined {
133-
const raw = sourceConfig.createdAfter
132+
function parseDateFilter(sourceConfig: Record<string, unknown>, key: string): string | undefined {
133+
const raw = sourceConfig[key]
134134
if (typeof raw !== 'string') return undefined
135135
const trimmed = raw.trim()
136136
if (!trimmed) return undefined
@@ -262,6 +262,16 @@ export const granolaConnector: ConnectorConfig = {
262262
description:
263263
'Only sync notes created on or after this date (ISO 8601). Leave blank to sync notes regardless of creation date.',
264264
},
265+
{
266+
id: 'createdBefore',
267+
title: 'Created Before',
268+
type: 'short-input',
269+
required: false,
270+
mode: 'advanced',
271+
placeholder: 'e.g. 2025-12-31 or 2025-12-31T23:59:59Z',
272+
description:
273+
'Only sync notes created on or before this date (ISO 8601). Leave blank to sync notes regardless of creation date.',
274+
},
265275
],
266276

267277
listDocuments: async (
@@ -273,20 +283,23 @@ export const granolaConnector: ConnectorConfig = {
273283
): Promise<ExternalDocumentList> => {
274284
const maxNotes = parseMaxNotes(sourceConfig)
275285
const folderId = parseFolderId(sourceConfig)
276-
const createdAfter = parseCreatedAfter(sourceConfig)
286+
const createdAfter = parseDateFilter(sourceConfig, 'createdAfter')
287+
const createdBefore = parseDateFilter(sourceConfig, 'createdBefore')
277288

278289
const url = new URL(`${GRANOLA_API_BASE}/notes`)
279290
url.searchParams.set('page_size', String(PAGE_SIZE))
280291
if (cursor) url.searchParams.set('cursor', cursor)
281292
if (lastSyncAt) url.searchParams.set('updated_after', lastSyncAt.toISOString())
282293
if (folderId) url.searchParams.set('folder_id', folderId)
283294
if (createdAfter) url.searchParams.set('created_after', createdAfter)
295+
if (createdBefore) url.searchParams.set('created_before', createdBefore)
284296

285297
logger.info('Listing Granola notes', {
286298
hasCursor: Boolean(cursor),
287299
incremental: Boolean(lastSyncAt),
288300
scopedToFolder: Boolean(folderId),
289301
scopedByCreatedAfter: Boolean(createdAfter),
302+
scopedByCreatedBefore: Boolean(createdBefore),
290303
})
291304

292305
const response = await fetchWithRetry(url.toString(), {
@@ -432,6 +445,19 @@ export const granolaConnector: ConnectorConfig = {
432445
}
433446
}
434447

448+
const createdBefore = sourceConfig.createdBefore
449+
if (
450+
typeof createdBefore === 'string' &&
451+
createdBefore.trim() &&
452+
Number.isNaN(new Date(createdBefore.trim()).getTime())
453+
) {
454+
return {
455+
valid: false,
456+
error:
457+
'Created Before must be a valid date (ISO 8601, e.g. 2025-12-31 or 2025-12-31T23:59:59Z)',
458+
}
459+
}
460+
435461
try {
436462
const url = new URL(`${GRANOLA_API_BASE}/notes`)
437463
url.searchParams.set('page_size', '1')

apps/sim/connectors/greenhouse/greenhouse.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,16 @@ export const greenhouseConnector: ConnectorConfig = {
527527
description:
528528
'Sync only candidates created at or after this ISO 8601 timestamp. Leave empty to sync candidates regardless of creation date.',
529529
},
530+
{
531+
id: 'createdBefore',
532+
title: 'Created Before',
533+
type: 'short-input',
534+
required: false,
535+
mode: 'advanced',
536+
placeholder: 'e.g. 2024-12-31T23:59:59Z',
537+
description:
538+
'Sync only candidates created before this ISO 8601 timestamp. Combine with Created After to backfill a bounded date range.',
539+
},
530540
],
531541

532542
listDocuments: async (
@@ -543,6 +553,8 @@ export const greenhouseConnector: ConnectorConfig = {
543553
const jobId = typeof sourceConfig.jobId === 'string' ? sourceConfig.jobId.trim() : ''
544554
const createdAfter =
545555
typeof sourceConfig.createdAfter === 'string' ? sourceConfig.createdAfter.trim() : ''
556+
const createdBefore =
557+
typeof sourceConfig.createdBefore === 'string' ? sourceConfig.createdBefore.trim() : ''
546558

547559
const queryParams = new URLSearchParams({
548560
per_page: String(CANDIDATES_PER_PAGE),
@@ -551,6 +563,7 @@ export const greenhouseConnector: ConnectorConfig = {
551563
if (updatedAfter) queryParams.set('updated_after', updatedAfter)
552564
if (jobId) queryParams.set('job_id', jobId)
553565
if (createdAfter) queryParams.set('created_after', createdAfter)
566+
if (createdBefore) queryParams.set('created_before', createdBefore)
554567

555568
const url = `${GREENHOUSE_API_BASE}/candidates?${queryParams.toString()}`
556569

apps/sim/connectors/incidentio/incidentio.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ export const incidentioConnector: ConnectorConfig = {
378378
options: [
379379
{ label: 'All', id: '' },
380380
{ label: 'Live (active)', id: 'live' },
381+
{ label: 'Paused', id: 'paused' },
381382
{ label: 'Closed', id: 'closed' },
382383
{ label: 'Triage', id: 'triage' },
383384
{ label: 'Learning (post-incident)', id: 'learning' },

apps/sim/connectors/rootly/rootly.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getErrorMessage, toError } from '@sim/utils/errors'
33
import { RootlyIcon } from '@/components/icons'
44
import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils'
55
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
6-
import { joinTagArray, parseTagDate } from '@/connectors/utils'
6+
import { joinTagArray, parseMultiValue, parseTagDate } from '@/connectors/utils'
77

88
const logger = createLogger('RootlyConnector')
99

@@ -17,8 +17,9 @@ const MAX_TIMELINE_EVENTS = 200
1717
* JSON:API relationships to embed inline within each incident's `attributes`.
1818
* Rootly omits these unless requested via `include`, so both the list (stub) and
1919
* detail requests pass them to ensure tag metadata is identical on either path.
20-
* Scoped to exactly the relationships this connector reads (services, teams,
21-
* environments) to avoid fetching unused relationship payloads on every incident.
20+
* Scoped to exactly the relationships this connector reads — `environments`,
21+
* `services`, and `groups` (Rootly's API token for teams) — to avoid fetching
22+
* unused relationship payloads on every incident.
2223
*/
2324
const INCIDENT_INCLUDE = 'environments,services,groups'
2425

@@ -388,9 +389,39 @@ export const rootlyConnector: ConnectorConfig = {
388389
type: 'short-input',
389390
required: false,
390391
mode: 'advanced',
391-
placeholder: 'e.g. SEV0 (default: all)',
392+
placeholder: 'e.g. sev0 (default: all)',
392393
description:
393-
'Only sync incidents with this severity name. Leave blank to sync all severities.',
394+
'Only sync incidents with this severity slug (e.g. sev0, sev1). Leave blank to sync all severities.',
395+
},
396+
{
397+
id: 'services',
398+
title: 'Filter by Services',
399+
type: 'short-input',
400+
required: false,
401+
mode: 'advanced',
402+
multi: true,
403+
placeholder: 'Service slugs (comma-separated, default: all)',
404+
description: 'Only sync incidents affecting these service slugs.',
405+
},
406+
{
407+
id: 'teams',
408+
title: 'Filter by Teams',
409+
type: 'short-input',
410+
required: false,
411+
mode: 'advanced',
412+
multi: true,
413+
placeholder: 'Team slugs (comma-separated, default: all)',
414+
description: 'Only sync incidents owned by these team slugs.',
415+
},
416+
{
417+
id: 'environments',
418+
title: 'Filter by Environments',
419+
type: 'short-input',
420+
required: false,
421+
mode: 'advanced',
422+
multi: true,
423+
placeholder: 'Environment slugs (comma-separated, default: all)',
424+
description: 'Only sync incidents in these environment slugs.',
394425
},
395426
{
396427
id: 'maxIncidents',
@@ -411,6 +442,9 @@ export const rootlyConnector: ConnectorConfig = {
411442
const maxIncidents = parseMaxIncidents(sourceConfig)
412443
const status = typeof sourceConfig.status === 'string' ? sourceConfig.status.trim() : ''
413444
const severity = typeof sourceConfig.severity === 'string' ? sourceConfig.severity.trim() : ''
445+
const services = parseMultiValue(sourceConfig.services)
446+
const teams = parseMultiValue(sourceConfig.teams)
447+
const environments = parseMultiValue(sourceConfig.environments)
414448
const pageNumber = cursor ? Number(cursor) : 1
415449
const startPage = Number.isFinite(pageNumber) && pageNumber > 0 ? pageNumber : 1
416450

@@ -420,6 +454,9 @@ export const rootlyConnector: ConnectorConfig = {
420454
queryParams.set('include', INCIDENT_INCLUDE)
421455
if (status) queryParams.set('filter[status]', status)
422456
if (severity) queryParams.set('filter[severity]', severity)
457+
if (services.length > 0) queryParams.set('filter[services]', services.join(','))
458+
if (teams.length > 0) queryParams.set('filter[teams]', teams.join(','))
459+
if (environments.length > 0) queryParams.set('filter[environments]', environments.join(','))
423460

424461
if (lastSyncAt) {
425462
queryParams.set('filter[updated_at][gt]', lastSyncAt.toISOString())

0 commit comments

Comments
 (0)