Allow for scheduling journey entrances by hour (#615)

This commit is contained in:
Chris Anderson 2025-01-19 22:59:29 -06:00 committed by GitHub
parent 729397907d
commit e14155baa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 35 additions and 16 deletions

View file

@ -21,7 +21,7 @@ export default class Journey extends Model {
name,
published: true,
})
const { steps, children } = await setJourneyStepMap(journey.id, stepMap)
const { steps, children } = await setJourneyStepMap(journey, stepMap)
return { journey, steps, children }
}
}

View file

@ -167,7 +167,7 @@ router.get('/:journeyId/steps', async ctx => {
})
router.put('/:journeyId/steps', async ctx => {
const { steps, children } = await setJourneyStepMap(ctx.state.journey!.id, validate(journeyStepsParamsSchema, ctx.request.body))
const { steps, children } = await setJourneyStepMap(ctx.state.journey!, validate(journeyStepsParamsSchema, ctx.request.body))
ctx.body = await toJourneyStepMap(steps, children)
})

View file

@ -6,6 +6,7 @@ import Journey, { JourneyParams, UpdateJourneyParams } from './Journey'
import { JourneyStep, JourneyEntrance, JourneyUserStep, JourneyStepMap, toJourneyStepMap, JourneyStepChild } from './JourneyStep'
import { createTagSubquery, getTags, setTags } from '../tags/TagService'
import { User } from '../users/User'
import { getProject } from '../projects/ProjectService'
export const pagedJourneys = async (params: PageParams, projectId: number) => {
const result = await Journey.search(
@ -107,15 +108,16 @@ export const getJourneyStepMap = async (journeyId: number) => {
return toJourneyStepMap(steps, children)
}
export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepMap) => {
export const setJourneyStepMap = async (journey: Journey, stepMap: JourneyStepMap) => {
return await App.main.db.transaction(async trx => {
const [steps, children] = await Promise.all([
getJourneySteps(journeyId, trx),
getJourneyStepChildren(journeyId, trx),
getJourneySteps(journey.id, trx),
getJourneyStepChildren(journey.id, trx),
])
const now = new Date()
const project = await getProject(journey.project_id)
// Create or update steps
for (const [external_id, { type, x = 0, y = 0, data = {}, data_key, name = '' }] of Object.entries(stepMap)) {
@ -126,7 +128,7 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
let next_scheduled_at: null | Date = null
if (type === JourneyEntrance.type && data.trigger === 'schedule') {
if (step.data?.schedule !== data.schedule) {
next_scheduled_at = JourneyEntrance.fromJson({ data }).nextDate(now)
next_scheduled_at = JourneyEntrance.fromJson({ data }).nextDate(project?.timezone ?? 'UTC', now)
} else {
next_scheduled_at = step.next_scheduled_at
}
@ -137,7 +139,7 @@ export const setJourneyStepMap = async (journeyId: number, stepMap: JourneyStepM
: await JourneyStep.insertAndFetch({
...fields,
external_id,
journey_id: journeyId,
journey_id: journey.id,
type,
}, trx),
)

View file

@ -149,7 +149,7 @@ export const duplicateJourney = async (journey: Journey) => {
const params: Partial<Journey> = pick(journey, ['project_id', 'name', 'description'])
params.name = `Copy of ${params.name}`
params.published = false
const newJourneyId = await Journey.insert(params)
const newJourney = await Journey.insertAndFetch(params)
const steps = await getJourneyStepMap(journey.id)
const newSteps: JourneyStepMap = {}
@ -165,7 +165,7 @@ export const duplicateJourney = async (journey: Journey) => {
children: step.children?.map(({ external_id, ...rest }) => ({ external_id: uuidMap[external_id], ...rest })),
}
}
await setJourneyStepMap(newJourneyId, newSteps)
await setJourneyStepMap(newJourney, newSteps)
return await getJourney(newJourneyId, journey.project_id)
return await getJourney(newJourney.id, journey.project_id)
}

View file

@ -9,7 +9,7 @@ import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'
import Rule from '../rules/Rule'
import { check } from '../rules/RuleEngine'
import App from '../app'
import { RRule } from 'rrule'
import { rrulestr } from 'rrule'
import { JourneyState } from './JourneyState'
import { EventPostJob, UserPatchJob } from '../jobs'
import { exitUserFromJourney, getJourneyUserStepByExternalId } from './JourneyRepository'
@ -122,13 +122,13 @@ export class JourneyEntrance extends JourneyStep {
this.schedule = json?.data?.schedule
}
nextDate(after = this.next_scheduled_at): Date | null {
nextDate(timezone: string, after = this.next_scheduled_at): Date | null {
if (this.trigger !== 'schedule' || !after) return null
if (this.schedule) {
try {
const rule = RRule.fromString(this.schedule)
const rule = rrulestr(this.schedule, { tzid: timezone })
// If there is no frequency, only run once
if (!rule.options.freq) {
@ -138,7 +138,7 @@ export class JourneyEntrance extends JourneyStep {
return rule.options.dtstart
}
return RRule.fromString(this.schedule).after(after)
return rrulestr(this.schedule, { tzid: timezone }).after(after)
} catch (err) {
App.main.error.notify(err as Error, {
entranceId: this.id,

View file

@ -1,4 +1,5 @@
import App from '../app'
import { getProject } from '../projects/ProjectService'
import { Job } from '../queue'
import { JourneyEntrance, JourneyStep } from './JourneyStep'
import ScheduledEntranceJob from './ScheduledEntranceJob'
@ -19,15 +20,16 @@ export default class ScheduledEntranceOrchestratorJob extends Job {
.whereJsonPath('journey_steps.data', '$.multiple', '=', true)
.whereNotNull('journey_steps.next_scheduled_at')
.where('journey_steps.next_scheduled_at', '<=', new Date()),
)
) as Array<JourneyEntrance & { project_id: number }>
if (!entrances.length) return
const jobs: Job[] = []
for (const entrance of entrances) {
const project = await getProject(entrance.project_id)
await JourneyStep.update(q => q.where('id', entrance.id), {
next_scheduled_at: entrance.nextDate(),
next_scheduled_at: entrance.nextDate(project?.timezone ?? 'UTC'),
})
if (entrance.list_id) {

View file

@ -127,6 +127,16 @@ export default function RRuleEditor({ label, onChange, value }: RRuleEditorProps
setValues({ ...options, until: value ? date : null })
}}
/>
<TextInput
name="hour"
label="Hour (24hr Format)"
type="number"
min={1}
max={24}
required
value={Number(options.byhour ?? 0)}
onChange={byhour => setValues({ ...options, byhour })}
/>
<TextInput
name="interval"
label="Interval"

View file

@ -106,6 +106,11 @@ export const entranceStep: JourneyStepType<EntranceConfig> = {
const rule = RRule.fromString(schedule)
if (rule.options.freq) {
s = rule.toText()
if (rule.options.freq === RRule.DAILY) {
s += Number(rule.options.byhour) < 12
? 'am'
: 'pm'
}
} else {
s = 'once'
}