React V2.1.X
Introduction
SDK overview
Welcome to the Flagship ReactJS SDK documentation!
The following documentation helps you to run Flagship on your ReactJS environment (client-side or server-side).
Flagship ReactJS SDK provides a <FlagshipProvider />
, which makes Flagship features available to the rest of your app.
Flagship features are accessible using Flagship hooks, have a look at the documentation for details.
Feel free to contact us if you have any questions regarding this documentation.
SDK features
This SDK version helps you:
- Set a visitor ID
- Update visitor context
- Assign campaigns via the Decision API or via the Bucketing
- Get modifications
- Activate campaigns
- Send hits to our Universal Collect
Prerequisites
- Node.js: version 6.0.0 or later
- Npm: version 5.2.0 or later
- React: version 16.8.0 or later (the SDK only supports hooks for now)
Good to know
- Github repository
- Gzipped size: ~24.1 kB
- Typescript code supported
- Open-source demos using the SDK
Changelog
- Visible on Github
Getting started
Initialization
There are four steps to follow to get started with the React Flagship SDK.
- Install the node module
npm install @flagship.io/react-sdk
- Import the Flagship React provider
In most cases, do this in your App.js
file to wrap your entire app with the provider.
import React from "react";
import { FlagshipProvider } from "@flagship.io/react-sdk";
const App = () => (
<>
<FlagshipProvider>{/* [...] */}</FlagshipProvider>
</>
);
- Initialize the provider
You must at least include the required props like envId
, apiKey
, visitorData
.
NOTE: apiKey is not required for now but will be in next major release!
import React from "react";
import { FlagshipProvider } from "@flagship.io/react-sdk";
const App = () => (
<>
<FlagshipProvider
envId="YOUR_ENV_ID"
apiKey="YOUR_API_KEY" // <= Required in next major release
visitorData={{
id: "YOUR_VISITOR_ID",
context: {
// some context
},
isAuthenticated: false,
}}
enableConsoleLogs={true}
>
{/* [...] */}
</FlagshipProvider>
</>
);
- Use a Flagship hook in a component
In most case, you will get the desired modifications.
import React from "react";
import { useFsModifications } from "@flagship.io/react-sdk";
export const MyReactComponent = () => {
const fsModifications = useFsModifications([
{
key: "backgroundColor",
defaultValue: "green",
activate: true,
},
]);
return (
<div
style={{
height: "200px",
width: "200px",
backgroundColor: fsModifications.backgroundColor,
}}
>
{"I'm a square with color=" + fsModifications.backgroundColor}
</div>
);
};
API Reference
FlagshipProvider
FlagshipProvider
Here is the full list of props available to use inside the FlagshipProvider
React component:
Props | Type | Default | Description |
---|---|---|---|
envId | string | Required | Your Flagship environment id. |
visitorData | object | Required | This is the data to identify the current visitor using your app. See arguments |
onInitStart | function():void | null | Callback function called when the SDK starts initialization. |
onInitDone | function():void | null | Callback function called when the SDK ends initialization. |
onUpdate | function(object):void | null | Callback function called when the SDK is updated. For example, after a synchronize is triggered or visitor context has changed. See arguments |
onBucketingSuccess | function(object):void | null | Callback function called when the bucketing polling succeed. See arguments |
onBucketingFail | function(object):void | null | Callback function called when the bucketing polling failed. See arguments |
initialModifications | Array(object) | undefined | This is an array of modifications where each element must be same shape as the element inside the "campaigns" attribute. Providing this prop prevents the SDK from having an empty cache when it first initializes. If the shape of an element is not correct, an error log will give the reason why. |
initialBucketing | Array(object) | undefined | This is an object of the data received when fetching bucketing endpoint. Providing this prop will make bucketing ready to use and the first polling will immediatly check for an update. If the shape of an element is not correct, an error log will give the reason why. |
loadingComponent | React.ReactNode | undefined | This component will be rendered when Flagship is loading at first initialization only. By default, the value is undefined. It means it will display your app and it might display default modifications value for a very short moment. |
fetchNow | boolean | true | Decide to fetch automatically modifications when SDK is initialized. NOTE: When fetchNow=false the property loadingComponent will be ignored.fetchNow=false |
activateNow | boolean | false | Decide to trigger automatically the data when SDK is initialized. NOTE: when set to true , it will implicitly set fetchNow=true as well. |
enableConsoleLogs | boolean | false | Enable it to display logs on the console when SDK is running. This will only display logs such as Warnings, Errors, Fatal errors and Info. |
enableSafeMode | boolean | false | Enable it to run the SDK into a safe mode when an error might occurs through the SDK. When safe mode is triggered, default modifications will be returned and other function will just be executed without doing anything. NOTE: This feature is still experimental as it is currently catching errors globally (SDK + your app) which can gives side effects to your app. We're working on improvement. |
enableCache | boolean | true | Indicates wether enable or disable the client cache manager (local storage). By enabling the cache, this will allow keep cross sessions visitor experience. Note: The cache is useful only when you do not specify a visitor id when creating a visitor. From there, you only need to be focus on handling the visitor context and whether it is authenticated or not. That's it. |
enableErrorLayout | boolean | false | This is a small layout visible at the bottom of the screen. It is displayed only when an unexpected error occurred in the SDK. By default, it's set to false and if set to true , it will be only visible in a node environment other than production . Here a screenshot to have a look. |
nodeEnv | string | 'production' | If value is other than production , it will also display Debug logs. |
flagshipApi | string | 'https://decision-api.flagship.io/v1/' | This setting can be useful if you need to simulate the API for tests such as end-to-end or if you want to move to an earlier version of the Flagship API. Decision API V1 is set by default but deprecated, starting next version, only Decision API V2 will be implemnented. |
apiKey | string | null | If you want to use the Decision API V2 , you must contact the support team so they'll provide you an API Key to authenticate the calls. You can find the api key of your environment on the Flagship dashboard, in Parameters > Environment & Security. NOTE: apiKey will be required in the next major release. |
decisionMode | 'API' | 'Bucketing' | 'API' | Indicates the behavior of the SDK. In API mode, it will get the modifications using the Flagship decision API.With Bucketing , it will load all campaigns once and compute the variation assignment of the visitor locally.Note: When Bucketing mode is set, you mustspecify pollingInterval attribute as well, so that bucketing can check for updates.Note2: When Bucketing mode is set, and the polling detects an update, it won't apply new changes until you manually trigger a campaign synchronization.Note3: Bucketing mode only works when you allow to start it during SDK initialization ( fetchNow=true ). We will add the abilityto trigger it manually in the next minor release. |
pollingInterval | number | null | Indicates the polling interval period in seconds when SDK is running Bucketing mode (decisionMode="Bucketing" ).For example, if pollingInterval=5 , a bucketing call will betrigger in background every 5 seconds. Note1: pollingInterval can't be lower than 1 second.Note2: if bucketing is enabled and pollingInterval value is null , the polling will be done once and an error log will be trigger. |
timeout | number | 2 | Indicates the delay in seconds before it triggers a timeout when requesting campaigns via the Flagship decision api. Note: timeout can't be lower than 0 second. |
When bucketing is running (
decisionMode="Bucketing"
), the polling will restart its interval period based on the moment when one of the following props changes after first rendering :
- envId
- visitorData
- initialModifications
- initialBucketing
- fetchNow
- activateNow
- enableConsoleLogs
- nodeEnv
- flagshipApi
- apiKey
- pollingInterval
visitorData
The visitorData
object takes the following arguments:
Argument | Type | Description |
---|---|---|
id | string | Optional. Unique identifier for the current user. This can be an ID from your database or a session ID. Learn more NOTE: The visitor id must be a string and can't be a number .NOTE2: If you do not specify a value, the id will be either automatically generated or will be the visitor id from previous session (if enableCache equals true ). |
context | object | Optional. JSON object of key-value pairs describing the user and device context. Learn more |
isAuthenticated | boolean | Optional. Indicates if the visitor is authenticated (true) or not (false, by default). NOTE: The SDK will listen the change of this value (switching from true to false or from false to true ).This attribute is used for the experience continuity. Learn more |
onUpdate
The onUpdate
object has one argument with the following shape:
Key/Property | Description |
---|---|
fsModifications | Array or null. It contains the last modifications saved in cache. When null , it means the SDK still not ready. You can check the SDK status with other attributes. |
config | Object. The current configuration running on the SDK. |
status | Object. Contains informations about the actual SDK status. (Details below 👇) |
status
object shape:
Key/Property | Description |
---|---|
isLoading | Boolean. When true , the SDK is still not ready to render your App otherwise it'll use default modifications. |
isSdkReady | Boolean. true after it has fully finished initialization tasks, false otherwise. |
isVisitorDefined | Boolean. When true the flagship visitor instance is truthy, false otherwise. |
hasError | Boolean. true when an error occured inside the SDK, false otherwise. |
lastRefresh | String or null. The last update date occured on the flagship visitor instance. |
firstInitSuccess | String or null. When null no initialization succeed yet. When string contains stringified date of last successful initialization. |
onBucketingSuccess
The onBucketingSuccess
object has one argument with the following shape:
Key/Property | Description |
---|---|
status | String. Returns either 200 (fresh update) or 304 (no change). |
payload | Object. The latest bucketing data received. |
onBucketingFail
The onBucketingFail
object has one argument with the following shape:
Key/Property | Description |
---|---|
error | Object. Returns the error occurred and leads to bucketing polling failure. |
useFlagship
useFlagship
Demo:
This is the most used hook from the Flagship React SDK. It gives further functionalities such as getting current modifications of your actual visitor, send hit tracking, SDK status...
Returns an object. (Typescript: UseFlagshipOutput)
useFlagship input
useFlagship input
Argument | Type | Description |
---|---|---|
options | object | See description |
useFlagship input options
useFlagship input options
Key/Property | Type | Description |
---|---|---|
modifications | object | Node param to specify Flagship modifications. See description |
useFlagship input options modifications
useFlagship input options modifications
Argument | Description |
---|---|
requested | Required. An array of objects for each modifications. See description |
activateAll | Optional. The value is false by default |
useFlagship input options modifications requested
useFlagship input options modifications requested
Argument | Description |
---|---|
key | Required. The name of the modification. |
defaultValue | Required. The default value if no value for this modification is found. |
activate | Optional. |
useFlagship output
useFlagship output
Key/Property | Type | Description |
---|---|---|
modifications | object | An object where each key is a modification with corresponding value |
getModificationInfo | function | Returns a promise with an object containing informations about modification matching the key specified in argument. See description |
synchronizeModifications | function | Updates campaigns targeting that match the current visitor context. See description |
hit | object | Gives you functions to send one or further hits. See description |
status | object | Gives you information about SDK current state. See description |
startBucketingPolling | function() | Call this function to start the bucketing manually. See description |
stopBucketingPolling | function() | Call this function to stop the bucketing manually. See description |
useFlagship output getModificationInfo
useFlagship output getModificationInfo
Returns Promise<object | null>
. (Typescript: GetModificationInfoOutput)
Argument | Type | Description |
---|---|---|
key | string | The modification key. |
useFlagship output synchronizeModifications
useFlagship output synchronizeModifications
Returns Promise<number>
.
Argument | Type | Default | Description |
---|---|---|---|
activateAllModifications | boolean | false | If set to true , all modifications will be activated. If set to false (default behavior), none will be activated. |
useFlagship output hits
useFlagship output hits
useFlagship output status
useFlagship output status
Key/Property | Description |
---|---|
isLoading | If true , the SDK isn't ready. |
lastRefresh | Date cast string with ISO format. This is the date corresponding to the most recent moment where modifications were saved in cache. |
useFlagship output startBucketingPolling
useFlagship output startBucketingPolling
Key/Property | Description |
---|---|
success | Boolean. If true means it succeed, false otherwise. |
reason | String. A message telling the reason in case of failure. |
useFlagship output stopBucketingPolling
useFlagship output stopBucketingPolling
Key/Property | Description |
---|---|
success | Boolean. If true means it succeed, false otherwise. |
reason | String. A message telling the reason in case of failure. |
useFsModifications
useFsModifications
Demo:
The useFsModifications
hook gives you the modifications saved in the SDK cache.
If the SDK cache is empty, it will always return modification's default values.
Returns Flagship modifications. (Typescript: UseFsModificationsOutput)
useFsModifications input
useFsModifications input
Argument | Type | Default | Description |
---|---|---|---|
modificationsRequested | Array(object) | Required | List of all modifications you're looking for. See description |
activateAllModifications | boolean | false | If set to true, all modifications will be activated. If set to false, none will be activated. NOTE: Setting this argument will override the activate attribute set in each element of the modificationsRequested array. |
useFsModifications input modificationsRequested
useFsModifications input modificationsRequested
Argument | Description |
---|---|
key | Required. The name of the modification. |
defaultValue | Required. The default value if no value for this modification is found. |
activate | Optional. Determines whether or not the modification is activated (if activateAllModifications is not set). |
useFsActivate
useFsActivate
Demo:
Returns void
.
useFsActivate input
useFsActivate input
Argument | Type | Default | Description |
---|---|---|---|
modificationKeys | Array(string) | Required | An array of modification keys. For each key, a HTTP request will be sent to activate the corresponding modification. |
applyEffectScope | Array(string) | [] | This argument behaves the same way as the second argument in the React.useEffect hook. It will listen for the array values and synchronize if it detects any changes. By default, it is triggered when the component where it's used is first rendered. |
Getting modifications
with useFlagship hook (1/3)
Demo:
import { useFlagship } from "@flagship.io/react-sdk";
const fsParams = {
modifications: {
requested: [
{
key: "btnColor",
defaultValue: "green",
activate: true,
},
],
},
};
const {
modifications: fsModifications,
status: fsStatus,
hit: fsHit,
} = useFlagship(fsParams);
with useFsModifications hook
Demo:
import { useFsModifications } from "@flagship.io/react-sdk";
const fsModifications = useFsModifications([
{
key: "btnColor",
defaultValue: "green",
activate: true,
},
]);
Campaign synchronization
with useFlagship hook (2/3)
Demo:
import { useFlagship } from "@flagship.io/react-sdk";
const { synchronizeModifications } = useFlagship();
return (
<>
<Button
onClick={() => {
synchronizeModifications(activateAllModifications)
.then((statusCode) => {
if (statusCode < 300) {
// Notify success...
} else {
// Notify failure...
}
})
.catch((error) => {
// Notify error...
});
}}
>
Trigger a synchronize
</Button>
</>
);
Campaign activation
with useFsActivate hook
Demo:
import { useFsActivate } from "@flagship.io/react-sdk";
const [toggle, setToggle] = React.useState(false);
useFsActivate(["btnColor", "otherKey1", "otherKey2"], [toggle]); // trigger an activate when "toggle" value change.
// insider render function:
<Button variant="secondary" onClick={() => setToggle(!toggle)}>
Trigger activate
</Button>;
Experience continuity
It might happen that a visitor from your app is not yet recognized and is being authenticated (and recognized) later on...
From there, we provide the ability to ensure that during such transition, your visitor will keep same experience (meaning targetting still the same campaign's variations and thus same modifications).
In order to do a successful experience continuity, we will have to distinguish when the visitor is anonymous or authenticated.
Let's assume basic scenario to understand how things work:
- Your visitor arrives on your app for the first time.
As this visitor will be considered as anonymous, we'll not specify a visitor id so that the SDK will a generate automatically one us. You can also specify some visitor context if necessary.
import React from "react";
import { FlagshipProvider } from "@flagship.io/react-sdk";
const App = () => (
<>
<FlagshipProvider
envId="YOUR_ENV_ID"
apiKey="YOUR_API_KEY"
visitorData={{
id: null, // or remove this line
context: {
// some context
},
isAuthenticated: false, //
}}
>
{/* [...] */}
</FlagshipProvider>
</>
);
Whatever how it has been set, the actual visitor id will be what we call its anonymous id.
Be aware that when you do not specify a visitor id, as
enableCache
is enable by default, the SDK will try to find a previous visitor experience. If so, the new visitor will have the same visitor id as it was during the previous visitor session. From there, no id will be automatically generated.
- Your visitor is signing in.
We need to set the value of visitorData.isAuthenticated
to true
.
Moreover, the visitor id set should be an existing visitor id (that has previously seen specific campaign's modifications) in order to make the experience continuity effective.
import React from "react";
import { FlagshipProvider } from "@flagship.io/react-sdk";
const App = () => (
<>
<FlagshipProvider
envId="YOUR_ENV_ID"
apiKey="YOUR_API_KEY"
visitorData={{
id: "AUTHENTICATED_ID",
context: {
// some context
},
isAuthenticated: true,
}}
>
{/* [...] */}
</FlagshipProvider>
</>
);
This new visitor id is what we call its authenticated id.
The visitor is updated as authenticated, keeping the previous variations from campaigns that are still matched and thus gives you same modifications as before being logged in.
Keep in mind that if the visitor also has its context changed, you might still have changes on modifications as your visitor might target new campaigns.
- Your visitor decide to sign out.
We need to set the value of visitorData.isAuthenticated
back to false
.
If you want to keep the same visitor (anonymous) experience as before, depending on the value of prop enableCache
, you'll have to:
enableCache=true
: the sdk will put back the previous anonymous id
automatically based on data from cache.
import React from "react";
import { FlagshipProvider } from "@flagship.io/react-sdk";
const App = () => (
<>
<FlagshipProvider
envId="YOUR_ENV_ID"
apiKey="YOUR_API_KEY"
visitorData={{
id: null,
context: {
// some context
},
isAuthenticated: false, // <--- back to false
}}
>
{/* [...] */}
</FlagshipProvider>
</>
);
enableCache=false
: you should specify in
prop, the same value as you set in step 1.
import React from "react";
import { FlagshipProvider } from "@flagship.io/react-sdk";
const App = () => (
<>
<FlagshipProvider
envId="YOUR_ENV_ID"
apiKey="YOUR_API_KEY"
visitorData={{
id: "YOUR_VISITOR_ID", // <--- same value as step 1
context: {
// some context
},
isAuthenticated: false, // <--- back to false
}}
>
{/* [...] */}
</FlagshipProvider>
</>
);
If you need a completly brand new visitor experience, you should put a new visitor id in visitorData.id
prop instead:
import React from "react";
import { FlagshipProvider } from "@flagship.io/react-sdk";
const App = () => (
<>
<FlagshipProvider
envId="YOUR_ENV_ID"
apiKey="YOUR_API_KEY"
visitorData={{
id: "YOUR_BRAND_NEW_VISITOR_ID", // <--- new value
context: {
// some context
},
isAuthenticated: false, // <--- back to false
}}
>
{/* [...] */}
</FlagshipProvider>
</>
);
Hit Tracking
This section helps you track your users in your application and learn how to build hits in order to feed campaign goals. For more information about our measurement protocol, read our Universal Collect documentation.
There are four different types of hits available:
with useFlagship hook (3/3)
Demo:
import { useFlagship } from "@flagship.io/react-sdk";
const { hit: fsHit } = useFlagship();
// insider render function:
<Button
onClick={() => {
const mockHit = {
type: "Transaction",
data: {
transactionId: "12451342423",
affiliation: "myAffiliation",
totalRevenue: 999,
shippingCost: 888,
shippingMethod: "DHL",
currency: "USD",
taxes: 1234444,
paymentMethod: "creditCard",
itemCount: 2,
couponCode: "myCOUPON",
documentLocation:
"http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F",
pageTitle: "myScreen",
},
};
fsHit.send(mockHit);
}}
>
Send a transaction hit
</Button>;
Hit types
Screen
import { useFlagship } from "@flagship.io/react-sdk";
const { hit: fsHit } = useFlagship();
<Button
onClick={() => {
const mockHit = {
type: "Screen",
data: {
documentLocation: "screenName"
},
};
fsHit.send(mockHit);
}}
>
Send a screen hit
</Button>;
Attribute | Type | Description |
---|---|---|
documentLocation | string | Required. Specifies the current name of the screen when the hit is sent. |
Page
import { useFlagship } from "@flagship.io/react-sdk";
const { hit: fsHit } = useFlagship();
<Button
onClick={() => {
const mockHit = {
type: "Page",
data: {
documentLocation:
"http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F",
pageTitle: "HomePage"
},
};
fsHit.send(mockHit);
}}
>
Send a screen hit
</Button>;
Attribute | Type | Description |
---|---|---|
documentLocation | string | Required. Specifies the current URL of the page when the hit is sent. |
pageTitle | string | Optional. Specifies the current name of the page when the hit is sent. |
Transaction
import { useFlagship } from "@flagship.io/react-sdk";
const { hit: fsHit } = useFlagship();
<Button
onClick={() => {
const mockHit = {
type: "Transaction",
data: {
transactionId: "12451342423",
affiliation: "myAffiliation",
totalRevenue: 999,
shippingCost: 888,
shippingMethod: "DHL",
currency: "USD",
taxes: 1234444,
paymentMethod: "creditCard",
itemCount: 2,
couponCode: "myCOUPON",
documentLocation:
"http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F",
pageTitle: "myScreen",
},
};
fsHit.send(mockHit);
}}
>
Send a transaction hit
</Button>;
Attribute | Type | Description |
---|---|---|
transactionId | string | Required. The ID of your transaction. |
affiliation | string | Required. The name of the KPI that you will have inside your reporting. Learn more |
totalRevenue | number | Optional. Specifies the total revenue associated with the transaction. This value should include any shipping and/or tax amounts. |
shippingCost | number | Optional. The total shipping cost of your transaction. |
shippingMethod | string | Optional. The shipping method for your transaction. |
taxes | number | Optional. Specifies the total amount of taxes in your transaction. |
currency | string | Optional. Specifies the currency of your transaction. NOTE: This value should be a valid ISO 4217 currency code. |
paymentMethod | string | Optional. Specifies the payment method used for your transaction. |
itemCount | number | Optional. Specifies the number of items of your transaction. |
couponCode | string | Optional. The coupon code associated with the transaction. |
documentLocation | string | Optional. Specifies the current URL of the page when the hit is sent. |
pageTitle | string | Optional. Specifies the current name of the page when the hit is sent. |
Item
import { useFlagship } from "@flagship.io/react-sdk";
const { hit: fsHit } = useFlagship();
<Button
onClick={() => {
const mockHit = {
type: "Item",
data: {
transactionId: "0987654321",
name: "testItem",
price: 999,
code: "testCode",
category: "testCategory",
quantity: 123,
documentLocation:
"http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F",
pageTitle: "testItem",
},
};
fsHit.send(mockHit);
}}
>
Send a item hit
</Button>;
Attribute | Type | Description |
---|---|---|
transactionId | string | Required. The ID of your transaction. |
name | string | Required. The name of your item. |
price | number | Optional. Specifies the price for a single item/unit. |
code | string | Optional. Specifies the SKU or item code. |
category | string | Optional. Specifies the category that the item belongs to. |
quantity | number | Optional. Specifies the number of items purchased. |
documentLocation | string | Optional. Specifies the current URL of the page when the hit is sent. |
pageTitle | string | Optional. Specifies the current name of the page when the hit is sent. |
The
Item
hit isn't available yet in the Flagship reporting view.
Event
import { useFlagship } from "@flagship.io/react-sdk";
const { hit: fsHit } = useFlagship();
<Button
onClick={() => {
const mockHit = {
type: "Event",
data: {
category: "User Engagement",
action: "signOff",
label: "Hello world",
value: 123,
documentLocation:
"http%3A%2F%2Fabtastylab.com%2F60511af14f5e48764b83d36ddb8ece5a%2F",
pageTitle: "testEvent",
},
};
fsHit.send(mockHit);
}}
>
Send a event hit
</Button>;
Attribute | Type | Description |
---|---|---|
category | string | Required. Specifies the category of your event. NOTE: This value must be either 'Action Tracking' or 'User Engagement' . |
action | string | Required. The name of the KPI you will have inside the reporting. |
label | string | Optional. Specifies additional description of your event. |
value | number | Optional. Specifies the monetary value associated with an event (e.g. you earn 10 to 100 euros depending on the quality of lead generated). NOTE: this value must be non-negative. |
documentLocation | string | Optional. Specifies the current URL of the page when the hit is sent. |
pageTitle | string | Optional. Specifies the current name of the page when the hit is sent. |
Demos
Following demos illustrate how the SDK is used in practice ☕
Developer Demo
Code sandbox Demo
Ecommerce store Demo
Appendix
Release note
Take a look to the Release note.
Contributing
Take a look at the Contributors Guide if you're interested in contributing to the SDK.
License
This Flagship SDK is distributed under the Apache version 2.0 license.
Updated 5 months ago