Skip to content

Commit 5df3814

Browse files
authored
PLU-432: feat: add survey for pipe publishing (#896)
## Problem Need a way to get survey feedback for pipe publishing experience. ## Solution Integrated Lens Survey widget to collect user feedback when a flow is published. The survey will appear for every pipe because every pipe has a different setup experience. **Features**: - Added Lens Survey widget that appears when a flow is published - Automatically sets `showSurvey` flag to true when a flow is published for the first time - `showSurvey` flag is set to false after the first time it is published - Tracks flow ID and user email as attributes in the survey - Added CSP configuration to allow loading the survey from the Lens domain ## Before & After Screenshots **AFTER**: https://github.com/user-attachments/assets/c9e32062-6340-41fc-ae21-159388a94c30 ## Tests 1. Check that already published pipes will not have the survey 2. Upon publishing a pipe for the first time, the survey should appear at the bottom right 3. Publishing the pipe again afterwards will not pop the survey anymore 4. The survey should appear for other pipes **Note** - Created 2 different surveys: 1 is for prod and the other is for staging/uat/dev to not pollute the responses for prod. To test the staging survey, need to login to https://lens.hack2025.com.sg and modify the domain accordingly to https://staging.plumber.gov.sg for staging, https://uat.plumber.gov.sg for uat and http://localhost:3001 for dev **New environment variables**: Frontend app config has a new `lensSurveyClientKey` which is the same for staging/uat/dev but different for prod because 2 different surveys created **New dependencies**: - `lens-widget`: Widget for collecting user feedback - Updated `@emotion/react` and `@emotion/styled` to ^11.14.0 (from 11.10.5) - Updated `react-icons` to ^4.12.0 (from 4.10.1)
1 parent fb8caae commit 5df3814

File tree

14 files changed

+211
-108
lines changed

14 files changed

+211
-108
lines changed

package-lock.json

Lines changed: 138 additions & 76 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/graphql/__tests__/mutations/update-flow-status.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ describe('updateFlowStatus', () => {
100100
expect(patchSpy).toHaveBeenCalledWith({
101101
active: true,
102102
publishedAt: expect.any(String),
103+
config: {
104+
showSurvey: true,
105+
},
103106
})
104107

105108
// jobName is constructed as "flow-<flow.id>"
@@ -132,6 +135,9 @@ describe('updateFlowStatus', () => {
132135
expect(patchSpy).toHaveBeenCalledWith({
133136
active: false,
134137
publishedAt: null,
138+
config: {
139+
showSurvey: undefined,
140+
},
135141
})
136142

137143
expect(flowQueue.removeRepeatableByKey).toHaveBeenCalledWith('repeat-key')
@@ -154,6 +160,9 @@ describe('updateFlowStatus', () => {
154160
expect(patchSpy).toHaveBeenCalledWith({
155161
active: true,
156162
publishedAt: expect.any(String),
163+
config: {
164+
showSurvey: true,
165+
},
157166
})
158167

159168
// But no job should be added when trigger type is webhook.
@@ -175,6 +184,9 @@ describe('updateFlowStatus', () => {
175184
expect(patchSpy).toHaveBeenCalledWith({
176185
active: false,
177186
publishedAt: null,
187+
config: {
188+
showSurvey: undefined,
189+
},
178190
})
179191

180192
// For webhook triggers no removal of a repeatable job should be attempted.

packages/backend/src/graphql/mutations/duplicate-flow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ const duplicateFlow: MutationResolvers['duplicateFlow'] = async (
9090

9191
// duplicate the flow with the previous config (only keep notification frequency)
9292
delete prevConfig['duplicateCount']
93-
delete prevConfig['demoConfig']
9493
delete prevConfig['templateConfig']
94+
delete prevConfig['showSurvey']
9595

9696
const duplicatedFlow = await context.currentUser
9797
.$relatedQuery('flows', trx)

packages/backend/src/graphql/mutations/update-flow-config.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import type {
2-
IFlowConfig,
3-
IFlowDemoConfig,
4-
IFlowErrorConfig,
5-
} from '@plumber/types'
1+
import type { IFlowConfig, IFlowErrorConfig } from '@plumber/types'
62

73
import Context from '@/types/express/context'
84

95
type Params = {
106
input: {
117
id: string
128
notificationFrequency: IFlowErrorConfig['notificationFrequency']
13-
hasLoadedOnce: IFlowDemoConfig['hasLoadedOnce']
9+
showSurvey: boolean
1410
}
1511
}
1612

@@ -37,12 +33,8 @@ const updateFlowConfig = async (
3733
}
3834
}
3935

40-
// TODO (mal): remove demo config
41-
if (params.input.hasLoadedOnce !== undefined) {
42-
newConfig.demoConfig = {
43-
...newConfig.demoConfig, // If ever undefined (should never be), it gets set to an empty object first
44-
hasLoadedOnce: params.input.hasLoadedOnce,
45-
}
36+
if (params.input.showSurvey !== undefined) {
37+
newConfig.showSurvey = params.input.showSurvey
4638
}
4739

4840
return await flow.$query().patchAndFetch({

packages/backend/src/graphql/mutations/update-flow-status.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ const updateFlowStatus: MutationResolvers['updateFlowStatus'] = async (
4141
await flow.$query().patch({
4242
active: params.input.active,
4343
publishedAt: params.input.active ? new Date().toISOString() : null,
44+
config: {
45+
...flow.config,
46+
// When publishing: set to true if undefined else false
47+
// When unpublishing: keep existing value
48+
showSurvey: params.input.active
49+
? flow.config?.showSurvey === undefined
50+
? true
51+
: false
52+
: flow.config?.showSurvey,
53+
},
4454
})
4555

4656
const triggerStep = await flow.getTriggerStep()

packages/backend/src/graphql/schema.graphql

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type Query {
4343
getTableConnections(tableIds: [String!]!): JSONObject
4444
getTables(limit: Int!, offset: Int!, name: String): PaginatedTables!
4545
# Tiles rows
46-
getAllRows(tableId: String!, stringifiedCursor: String): GetTableRowsResult!
46+
getAllRows(tableId: String!, stringifiedCursor: String): GetTableRowsResult!
4747
getCurrentUser: User
4848
healthcheck: AppHealth
4949
getPlumberStats: Stats
@@ -357,19 +357,14 @@ type Flow {
357357
type FlowConfig {
358358
errorConfig: FlowErrorConfig
359359
duplicateCount: Int
360-
demoConfig: FlowDemoConfig
361360
templateConfig: FlowTemplateConfig
361+
showSurvey: Boolean
362362
}
363363

364364
type FlowErrorConfig {
365365
notificationFrequency: NotificationFrequency!
366366
}
367367

368-
type FlowDemoConfig {
369-
hasLoadedOnce: Boolean!
370-
videoId: String!
371-
}
372-
373368
type FlowTemplateConfig {
374369
templateId: String!
375370
formId: String
@@ -448,7 +443,7 @@ enum NotificationFrequency {
448443
input UpdateFlowConfigInput {
449444
id: String!
450445
notificationFrequency: NotificationFrequency
451-
hasLoadedOnce: Boolean
446+
showSurvey: Boolean
452447
}
453448

454449
input ExecuteFlowInput {

packages/backend/src/helpers/csp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ const helmetOptions: HelmetOptions = {
1717
'https://*.launchdarkly.com',
1818
// For proxying datadog rum
1919
'https://rum-proxy.plumber.gov.sg',
20+
// For Lens Survey
21+
'https://lens.hack2025.com.sg',
2022
appConfig.baseUrl,
2123
],
2224
// for google fonts

packages/frontend/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
"@dnd-kit/core": "6.1.0",
1717
"@dnd-kit/modifiers": "7.0.0",
1818
"@dnd-kit/sortable": "8.0.0",
19-
"@emotion/react": "11.10.5",
20-
"@emotion/styled": "11.10.5",
19+
"@emotion/react": "^11.14.0",
20+
"@emotion/styled": "^11.14.0",
2121
"@fontsource/space-grotesk": "4.5.13",
2222
"@hookform/resolvers": "^2.8.8",
2323
"@mui/icons-material": "5.11.0",
@@ -41,6 +41,7 @@
4141
"dedent": "1.5.1",
4242
"escape-html": "1.0.3",
4343
"file-saver": "2.0.5",
44+
"lens-widget": "1.36.0",
4445
"lodash": "^4.17.21",
4546
"lodash.get": "4.4.2",
4647
"lottie-web": "5.12.2",
@@ -49,7 +50,7 @@
4950
"notistack": "2.0.8",
5051
"papaparse": "5.4.1",
5152
"react-hook-form": "^7.17.2",
52-
"react-icons": "4.10.1",
53+
"react-icons": "^4.12.0",
5354
"react-intl": "6.2.7",
5455
"react-json-tree": "^0.18.0",
5556
"react-markdown": "8.0.7",

packages/frontend/src/components/Editor/index.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,22 @@ import {
1212
Flex,
1313
} from '@chakra-ui/react'
1414
import { IconButton } from '@opengovsg/design-system-react'
15+
import { Rating } from 'lens-widget'
1516

1617
import FlowStep from '@/components/FlowStep'
1718
import FlowStepGroup from '@/components/FlowStepGroup'
19+
import appConfig from '@/config/app'
1820
import { EditorContext } from '@/contexts/Editor'
1921
import {
2022
StepExecutionsToIncludeContext,
2123
StepExecutionsToIncludeProvider,
2224
} from '@/contexts/StepExecutionsToInclude'
2325
import { CREATE_STEP } from '@/graphql/mutations/create-step'
26+
import { UPDATE_FLOW_CONFIG } from '@/graphql/mutations/update-flow-config'
2427
import { UPDATE_STEP } from '@/graphql/mutations/update-step'
2528
import { GET_APPS } from '@/graphql/queries/get-apps'
2629
import { GET_FLOW } from '@/graphql/queries/get-flow'
30+
import useAuthentication from '@/hooks/useAuthentication'
2731

2832
interface AddStepButtonProps {
2933
onClick: () => void
@@ -97,6 +101,16 @@ export default function Editor(props: EditorProps): React.ReactElement {
97101
)
98102

99103
const { flow, steps: rawSteps } = props
104+
const showSurvey = flow.active && flow.config?.showSurvey
105+
const { currentUser } = useAuthentication()
106+
107+
const [updateFlowConfig] = useMutation(UPDATE_FLOW_CONFIG)
108+
const onFlowConfigUpdate = useCallback(async () => {
109+
await updateFlowConfig({
110+
variables: { input: { id: flow.id, showSurvey: false } },
111+
})
112+
}, [updateFlowConfig, flow.id])
113+
100114
const steps = useMemo(
101115
// Populate each step's flowId so that IStep isn't LYING about flowId being
102116
// non-undefined. We do it here instead of fetching in GraphQL since all
@@ -302,6 +316,20 @@ export default function Editor(props: EditorProps): React.ReactElement {
302316
)}
303317
</StepExecutionsToIncludeProvider>
304318
</Flex>
319+
320+
{showSurvey && (
321+
<Rating
322+
clientKey={appConfig.lensSurveyClientKey}
323+
brandColour="#cf1a68"
324+
attributes={[
325+
`FlowId: ${flow.id}`,
326+
`UserEmail: ${currentUser?.email}`,
327+
...(appConfig.env !== 'prod' ? [`Env: ${appConfig.env}`] : []),
328+
]}
329+
onSubmit={onFlowConfigUpdate}
330+
onClose={onFlowConfigUpdate}
331+
/>
332+
)}
305333
</Flex>
306334
)
307335
}

packages/frontend/src/components/EditorLayout/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ export default function EditorLayout() {
3737
const { flowId } = useParams()
3838
const [searchParams, setSearchParams] = useSearchParams()
3939
const [updateFlow] = useMutation(UPDATE_FLOW)
40-
const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS)
40+
const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS, {
41+
refetchQueries: [GET_FLOW],
42+
})
4143
const { data, loading, error } = useQuery(GET_FLOW, {
4244
variables: { id: flowId },
4345
})

0 commit comments

Comments
 (0)