• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / 9f082077-0a13-44b2-a288-da1a168d98c7

08 Nov 2023 05:01PM CUT coverage: 74.258% (+0.02%) from 74.239%
9f082077-0a13-44b2-a288-da1a168d98c7

push

circleci

web-flow
Merge pull request #4087 from mozilla/MPP-3562

MPP-3562 CSAT banner not showing every 90 days on website

1957 of 2847 branches covered (0.0%)

Branch coverage included in aggregate %.

0 of 1 new or added line in 1 file covered. (0.0%)

6195 of 8131 relevant lines covered (76.19%)

19.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.88
/frontend/src/components/layout/topmessage/CsatSurvey.tsx
1
import { event as gaEvent } from "react-ga";
8✔
2
import styles from "./CsatSurvey.module.scss";
8✔
3
import { useFirstSeen } from "../../../hooks/firstSeen";
8✔
4
import {
5
  DismissOptions,
6
  useLocalDismissal,
7
} from "../../../hooks/localDismissal";
8✔
8
import { ProfileData } from "../../../hooks/api/profile";
9
import { CloseIcon } from "../../Icons";
8✔
10
import { parseDate } from "../../../functions/parseDate";
8✔
11
import { useState } from "react";
8✔
12
import { getLocale } from "../../../functions/getLocale";
8✔
13
import { useL10n } from "../../../hooks/l10n";
8✔
14

15
type SurveyLinks = {
16
  "Very Dissatisfied": string;
17
  Dissatisfied: string;
18
  Neutral: string;
19
  Satisfied: string;
20
  "Very Satisfied": string;
21
};
22
const surveyLinks: Record<"free" | "premium", SurveyLinks> = {
8✔
23
  free: {
24
    "Very Dissatisfied": "https://survey.alchemer.com/s3/6665054/4ffc17ee53cc",
25
    Dissatisfied: "https://survey.alchemer.com/s3/6665054/5c8a66981273",
26
    Neutral: "https://survey.alchemer.com/s3/6665054/a9f6fc6493de",
27
    Satisfied: "https://survey.alchemer.com/s3/6665054/1669a032ed19",
28
    "Very Satisfied": "https://survey.alchemer.com/s3/6665054/ba159b3a792f",
29
  },
30
  premium: {
31
    "Very Dissatisfied": "https://survey.alchemer.com/s3/6665054/2e10c92e6360",
32
    Dissatisfied: "https://survey.alchemer.com/s3/6665054/1961150a57d1",
33
    Neutral: "https://survey.alchemer.com/s3/6665054/e606b4a664d3",
34
    Satisfied: "https://survey.alchemer.com/s3/6665054/1fb00ea39755",
35
    "Very Satisfied": "https://survey.alchemer.com/s3/6665054/a957749b3de6",
36
  },
37
};
38

39
type Props = {
40
  profile: ProfileData;
41
};
42
/**
43
 * Quickly survey the user about their satisfaction with Relay, and invite them to share more information.
44
 */
45
export const CsatSurvey = (props: Props) => {
104✔
46
  const free7DaysDismissal = useLocalDismissal(
104✔
47
    "csat-survey-free-7days_" + props.profile.id,
48
  );
49
  const free30DaysDismissal = useLocalDismissal(
104✔
50
    "csat-survey-free-30days_" + props.profile.id,
51
  );
52
  const free90DaysDismissal = useLocalDismissal(
104✔
53
    "csat-survey-free-90days_" + props.profile.id,
54
    // After the third month, show every three months:
55
    { duration: 90 * 24 * 60 * 60 },
56
  );
57
  const premium7DaysDismissal = useLocalDismissal(
104✔
58
    "csat-survey-premium-7days_" + props.profile.id,
59
  );
60
  const premium30DaysDismissal = useLocalDismissal(
104✔
61
    "csat-survey-premium-30days_" + props.profile.id,
62
  );
63
  const premium90DaysDismissal = useLocalDismissal(
104✔
64
    "csat-survey-premium-90days_" + props.profile.id,
65
    // After the third month, show every three months:
66
    { duration: 90 * 24 * 60 * 60 },
67
  );
68
  const firstSeen = useFirstSeen();
104✔
69
  const l10n = useL10n();
104✔
70
  const [answer, setAnswer] = useState<keyof SurveyLinks>();
104✔
71

72
  let reasonToShow:
73
    | null
74
    | "free7days"
75
    | "free30days"
76
    | "free90days"
77
    | "premium7days"
78
    | "premium30days"
79
    | "premium90days" = null;
104✔
80

81
  if (
104✔
82
    props.profile.has_premium &&
122✔
83
    (props.profile.date_subscribed || firstSeen instanceof Date)
84
  ) {
85
    // There are two reasons why someone might not have a subscription date set:
86
    // - They subscribed before we started tracking that.
87
    // - They have Premium because they have a Mozilla email address.
88
    // In the latter case, their first visit date is effectively their
89
    // subscription date. In the former case, they will have had Premium for
90
    // a while, so they can be shown the survey too. Their first visit will
91
    // have been a while ago, so we'll just use that as a proxy for the
92
    // subscription date:
93
    const subscriptionDate = props.profile.date_subscribed
16✔
94
      ? parseDate(props.profile.date_subscribed)
95
      : // We've verified that `firstSeen` is a Date if `date_subscribed` is null
96
        // with instanceof above, but that logic is a bit too complex to allow
97
        // TypeScript to be able to narrow the type of `firstSeen` by inference,
98
        // hence the type assertion:
99
        (firstSeen as Date);
100
    const daysSinceSubscription =
101
      (Date.now() - subscriptionDate.getTime()) / 1000 / 60 / 60 / 24;
16✔
102
    if (daysSinceSubscription >= 90) {
16✔
103
      if (!premium90DaysDismissal.isDismissed) {
6✔
104
        reasonToShow = "premium90days";
5✔
105
      }
106
    } else if (daysSinceSubscription >= 30) {
10✔
107
      if (!premium30DaysDismissal.isDismissed) {
4✔
108
        reasonToShow = "premium30days";
3✔
109
      }
110
    } else if (daysSinceSubscription >= 7) {
6✔
111
      if (!premium7DaysDismissal.isDismissed) {
3✔
112
        reasonToShow = "premium7days";
2✔
113
      }
114
    }
115
  } else if (!props.profile.has_premium && firstSeen instanceof Date) {
88✔
116
    const daysSinceFirstSeen =
117
      (Date.now() - firstSeen.getTime()) / 1000 / 60 / 60 / 24;
13✔
118
    if (daysSinceFirstSeen >= 90) {
13✔
119
      if (!free90DaysDismissal.isDismissed) {
8✔
120
        reasonToShow = "free90days";
7✔
121
      }
122
    } else if (daysSinceFirstSeen >= 30) {
5✔
123
      if (!free30DaysDismissal.isDismissed) {
3✔
124
        reasonToShow = "free30days";
2✔
125
      }
126
    } else if (daysSinceFirstSeen >= 7) {
2✔
127
      if (!free7DaysDismissal.isDismissed) {
1!
128
        reasonToShow = "free7days";
×
129
      }
130
    }
131
  }
132

133
  const locale = getLocale(l10n);
104✔
134
  if (
104✔
135
    reasonToShow === null ||
123✔
136
    !["en", "fr", "de"].includes(locale.split("-")[0])
137
  ) {
138
    return null;
86✔
139
  }
140

141
  const dismiss = (options?: DismissOptions) => {
18✔
142
    if (reasonToShow === "free7days") {
×
143
      free7DaysDismissal.dismiss(options);
×
144
    }
145
    if (reasonToShow === "free30days") {
×
NEW
146
      free30DaysDismissal.dismiss(options);
×
147
    }
148
    if (reasonToShow === "free90days") {
×
149
      free90DaysDismissal.dismiss(options);
×
150
    }
151
    if (reasonToShow === "premium7days") {
×
152
      premium7DaysDismissal.dismiss(options);
×
153
    }
154
    if (reasonToShow === "premium30days") {
×
155
      premium30DaysDismissal.dismiss(options);
×
156
    }
157
    if (reasonToShow === "premium90days") {
×
158
      premium90DaysDismissal.dismiss(options);
×
159
    }
160
  };
161

162
  const submit = (satisfaction: keyof SurveyLinks) => {
18✔
163
    setAnswer(satisfaction);
×
164
    dismiss({ soft: true });
×
165
    gaEvent({
×
166
      category: "CSAT Survey",
167
      action: "submitted",
168
      label: satisfaction,
169
      value: getNumericValueOfSatisfaction(satisfaction),
170
      // Custom dimension 3 in Google Analytics is "CSAT Category",
171
      // i.e. "Dissatisfied", "Neutral" or "Satisfied"
172
      dimension3: getCategoryOfSatisfaction(satisfaction),
173
      // Custom dimension 3 in Google Analytics is "CSAT Survey Rating",
174
      // i.e. "Very Dissatisfied", "Dissatisfied", "Neutral", "Satisfied" or "Very Satisfied"
175
      dimension4: satisfaction,
176
      // Metric 10 in Google Analytics is "CSAT Survey Count",
177
      // i.e. it tracks how many people have completed the CSAT survey:
178
      metric10: 1,
179
      // Metric 11 in Google Analytics is "CSAT Survey Rating",
180
      // i.e. it tracks which answer survey takers gave ("Very Dissatisfied",
181
      // "Dissatisfied", "Neutral", "Satisfied" or "Very Satisfied")
182
      metric11: getNumericValueOfSatisfaction(satisfaction),
183
      // Metric 12 in Google Analytics is "CSAT Satisfaction Value",
184
      // i.e. it tracks where users are Satisfied, Neutral or Dissatisfied:
185
      metric12: getNumericValueOfSatisfactionCategory(
186
        getCategoryOfSatisfaction(satisfaction),
187
      ),
188
    });
189
  };
190

191
  const question =
192
    typeof answer !== "undefined" ? (
193
      <div className={styles.prompt}>
×
194
        <a
195
          href={
196
            props.profile.has_premium
×
197
              ? surveyLinks.premium[answer]
198
              : surveyLinks.free[answer]
199
          }
200
          onClick={() => dismiss()}
×
201
          target="_blank"
202
          rel="noopen noreferrer"
203
        >
204
          {l10n.getString("survey-csat-followup")}
205
        </a>
206
      </div>
207
    ) : (
208
      <>
209
        <div className={styles.prompt}>
210
          {l10n.getString("survey-csat-question")}
211
        </div>
212
        <div className={styles.answers}>
213
          <ol>
214
            <li>
215
              <button
216
                className={styles.answer}
217
                onClick={() => submit("Very Dissatisfied")}
×
218
              >
219
                {l10n.getString("survey-csat-answer-very-dissatisfied")}
220
              </button>
221
            </li>
222
            <li>
223
              <button
224
                className={styles.answer}
225
                onClick={() => submit("Dissatisfied")}
×
226
              >
227
                {l10n.getString("survey-csat-answer-dissatisfied")}
228
              </button>
229
            </li>
230
            <li>
231
              <button
232
                className={styles.answer}
233
                onClick={() => submit("Neutral")}
×
234
              >
235
                {l10n.getString("survey-csat-answer-neutral")}
236
              </button>
237
            </li>
238
            <li>
239
              <button
240
                className={styles.answer}
241
                onClick={() => submit("Satisfied")}
×
242
              >
243
                {l10n.getString("survey-csat-answer-satisfied")}
244
              </button>
245
            </li>
246
            <li>
247
              <button
248
                className={styles.answer}
249
                onClick={() => submit("Very Satisfied")}
×
250
              >
251
                {l10n.getString("survey-csat-answer-very-satisfied")}
252
              </button>
253
            </li>
254
          </ol>
255
        </div>
256
      </>
257
    );
258

259
  return (
260
    <aside className={styles.wrapper}>
261
      {question}
262
      <button
263
        className={styles["dismiss-button"]}
264
        onClick={() => dismiss()}
×
265
        title={l10n.getString("survey-option-dismiss")}
266
      >
267
        <CloseIcon alt={l10n.getString("survey-option-dismiss")} />
268
      </button>
269
    </aside>
270
  );
271
};
272

273
function getNumericValueOfSatisfaction(
274
  satisfaction: keyof SurveyLinks,
275
): 1 | 2 | 3 | 4 | 5 {
276
  switch (satisfaction) {
×
277
    case "Very Dissatisfied":
278
      return 1;
×
279
    case "Dissatisfied":
280
      return 2;
×
281
    case "Neutral":
282
      return 3;
×
283
    case "Satisfied":
284
      return 4;
×
285
    case "Very Satisfied":
286
      return 5;
×
287
  }
288
}
289

290
type SatisfactionCategory = "Dissatisfied" | "Neutral" | "Satisfied";
291
function getCategoryOfSatisfaction(
292
  satisfaction: keyof SurveyLinks,
293
): SatisfactionCategory {
294
  switch (satisfaction) {
×
295
    case "Very Dissatisfied":
296
    case "Dissatisfied":
297
      return "Dissatisfied";
×
298
    case "Neutral":
299
      return "Neutral";
×
300
    case "Satisfied":
301
    case "Very Satisfied":
302
      return "Satisfied";
×
303
  }
304
}
305

306
function getNumericValueOfSatisfactionCategory(
307
  satisfactionCategory: SatisfactionCategory,
308
): -1 | 0 | 1 {
309
  switch (satisfactionCategory) {
×
310
    case "Dissatisfied":
311
      return -1;
×
312
    case "Neutral":
313
      return 0;
×
314
    case "Satisfied":
315
      return 1;
×
316
  }
317
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc