Repository: kiwicom/mobile
Pull request #1386 (2018-12-15 10:24:05)

Hotels: Extend logging
Still missing `hotelsBookNowPressed` because ios needs to change NSNumber to nonnull NSNumber. 

Still missing `hotelsDetailAbandoned` I would like some clarification on how to log it. 

RobinCsl: @pistelak Could you please have a look at the issue with this not nonnull `NSNumber`? Thank you
![simulator screen shot - iphone x - 2018-12-17 at 18 02 04](https://user-images.githubusercontent.com/22741774/50102679-f5a83d80-0225-11e9-91e8-9f2ac7e708a0.png)

pistelak: Will do :) 

Patch:
diff --git a/app/core/config/NativeModulesMocks/RNLoggingModule.js b/app/core/config/NativeModulesMocks/RNLoggingModule.js
index 58f6a2b4..87878edd 100644
--- a/app/core/config/NativeModulesMocks/RNLoggingModule.js
+++ b/app/core/config/NativeModulesMocks/RNLoggingModule.js
@@ -5,10 +5,21 @@ import { NativeModules } from 'react-native';
 NativeModules.RNLoggingModule = {
   ancillaryDisplayed: jest.fn(),
   ancillaryPurchased: jest.fn(),
+  hotelsResultsDisplayed: jest.fn(),
+  hotelsSelectedFilterTag: jest.fn(),
+  hotelsDetailOpened: jest.fn(),
+  hotelsDetailAbandoned: jest.fn(),
+  hotelsDetailDescriptionExpanded: jest.fn(),
+  hotelsDetailMapOpened: jest.fn(),
+  hotelsDetailRoomSelected: jest.fn(),
+  hotelsGalleryOpened: jest.fn(),
+  hotelsBookNowPressed: jest.fn(),
   ANCILLARY_STEP_DETAILS: 'ANCILLARY_STEP_DETAILS',
   ANCILLARY_STEP_PAYMENT: 'ANCILLARY_STEP_PAYMENT',
   ANCILLARY_STEP_RESULTS: 'ANCILLARY_STEP_RESULTS',
   ANCILLARY_STEP_SEARCH_FORM: 'ANCILLARY_STEP_SEARCH_FORM',
   ANCILLARY_PROVIDER_BOOKINGCOM: 'ANCILLARY_PROVIDER_BOOKINGCOM',
   ANCILLARY_PROVIDER_STAY22: 'ANCILLARY_PROVIDER_STAY22',
+  HOTELS_GALLERY_TYPE_HOTEL: 'HOTELS_GALLERY_TYPE_HOTEL',
+  HOTELS_GALLERY_TYPE_ROOM: 'HOTELS_GALLERY_TYPE_ROOM',
 };
diff --git a/app/hotels/src/allHotels/HotelsSearch.js b/app/hotels/src/allHotels/HotelsSearch.js
index 2521b982..ec594583 100644
--- a/app/hotels/src/allHotels/HotelsSearch.js
+++ b/app/hotels/src/allHotels/HotelsSearch.js
@@ -1,7 +1,7 @@
 // @flow
 
 import * as React from 'react';
-import { GeneralError, OfflineScreen } from '@kiwicom/mobile-shared';
+import { GeneralError, OfflineScreen, Logger } from '@kiwicom/mobile-shared';
 import { Translation } from '@kiwicom/mobile-localization';
 import { PublicApiRenderer } from '@kiwicom/mobile-relay';
 
@@ -16,6 +16,12 @@ type Props = {|
 |};
 
 export default class HotelsSearch extends React.Component<Props> {
+  componentDidMount() {
+    // $FlowExpectedError: Query is actually a function (tagged literal)
+    const { text: query } = this.props.query();
+    Logger.hotelsResultsDisplayed(query, JSON.stringify(this.props.variables));
+  }
+
   renderOfflineScreen = (retry: () => void) => {
     return <OfflineScreen onClose={this.props.onClose} onTryAgain={retry} />;
   };
diff --git a/app/hotels/src/filter/freeCancellation/FreeCancellationFilter.js b/app/hotels/src/filter/freeCancellation/FreeCancellationFilter.js
index 6a84555f..04bf32b9 100644
--- a/app/hotels/src/filter/freeCancellation/FreeCancellationFilter.js
+++ b/app/hotels/src/filter/freeCancellation/FreeCancellationFilter.js
@@ -2,6 +2,7 @@
 
 import * as React from 'react';
 import { Translation } from '@kiwicom/mobile-localization';
+import { Logger } from '@kiwicom/mobile-shared';
 
 import FilterButton from '../FilterButton';
 import type { OnChangeFilterParams } from '../FilterParametersType';
@@ -12,8 +13,12 @@ type Props = {|
 |};
 
 export default class FreeCancellationFilter extends React.Component<Props> {
-  onChange = () =>
+  onChange = () => {
     this.props.onChange({ freeCancellation: !this.props.isActive });
+    if (this.props.isActive === false) {
+      Logger.hotelsFilterTagSet('Free cancellation');
+    }
+  };
 
   render() {
     return (
diff --git a/app/hotels/src/filter/hotelFacilities/HotelFacilitiesFilter.js b/app/hotels/src/filter/hotelFacilities/HotelFacilitiesFilter.js
index 19126be4..eb2f3ebd 100644
--- a/app/hotels/src/filter/hotelFacilities/HotelFacilitiesFilter.js
+++ b/app/hotels/src/filter/hotelFacilities/HotelFacilitiesFilter.js
@@ -2,6 +2,7 @@
 
 import * as React from 'react';
 import { Translation, TranslationFragment } from '@kiwicom/mobile-localization';
+import { Logger } from '@kiwicom/mobile-shared';
 
 import HotelFacilitiesPopup from './HotelFacilitiesPopup';
 import FilterButton from '../FilterButton';
@@ -47,7 +48,12 @@ export default class HotelFacilitiesFilter extends React.Component<
   };
 
   handleSave = (hotelFacilities: string[]) =>
-    this.closePopup(() => this.props.onChange({ hotelFacilities }));
+    this.closePopup(() => {
+      this.props.onChange({ hotelFacilities });
+      if (hotelFacilities.length > 0) {
+        Logger.hotelsFilterTagSet('Amenities');
+      }
+    });
 
   getTitle = (facilities: string[]) => (
     <TranslationFragment>
diff --git a/app/hotels/src/filter/order/OrderFilter.js b/app/hotels/src/filter/order/OrderFilter.js
index cc98a755..4418da11 100644
--- a/app/hotels/src/filter/order/OrderFilter.js
+++ b/app/hotels/src/filter/order/OrderFilter.js
@@ -2,7 +2,7 @@
 
 import * as React from 'react';
 import { Translation } from '@kiwicom/mobile-localization';
-import { TextIcon } from '@kiwicom/mobile-shared';
+import { TextIcon, Logger } from '@kiwicom/mobile-shared';
 
 import FilterButton from '../FilterButton';
 import type {
@@ -55,6 +55,7 @@ export default class OrderFilter extends React.Component<Props, State> {
   handleSave = (orderBy: OrderByEnum | null) => {
     this.props.onChange({ orderBy });
     this.togglePopup();
+    Logger.hotelsFilterTagSet('Sort');
   };
 
   togglePopup = () => {
diff --git a/app/hotels/src/filter/price/PriceFilter.js b/app/hotels/src/filter/price/PriceFilter.js
index d47811bb..b12fc7fc 100644
--- a/app/hotels/src/filter/price/PriceFilter.js
+++ b/app/hotels/src/filter/price/PriceFilter.js
@@ -1,7 +1,7 @@
 // @flow
 
 import * as React from 'react';
-import { TextIcon } from '@kiwicom/mobile-shared';
+import { TextIcon, Logger } from '@kiwicom/mobile-shared';
 import {
   Translation,
   TranslationFragment,
@@ -70,7 +70,10 @@ class PriceFilter extends React.Component<PropsWithContext, State> {
       maxPrice:
         maxPrice !== this.props.currentSearchStats.priceMax ? maxPrice : null,
     };
-    this.closePopup(() => this.props.onChange(filter));
+    this.closePopup(() => {
+      this.props.onChange(filter);
+      Logger.hotelsFilterTagSet('Price');
+    });
   };
 
   getTitle = (
diff --git a/app/hotels/src/filter/score/ScoreFilter.js b/app/hotels/src/filter/score/ScoreFilter.js
index 3a0703e5..3070568f 100644
--- a/app/hotels/src/filter/score/ScoreFilter.js
+++ b/app/hotels/src/filter/score/ScoreFilter.js
@@ -1,7 +1,7 @@
 // @flow
 
 import * as React from 'react';
-import { TextIcon } from '@kiwicom/mobile-shared';
+import { TextIcon, Logger } from '@kiwicom/mobile-shared';
 import { Translation } from '@kiwicom/mobile-localization';
 
 import ScorePopup from './ScorePopup';
@@ -52,7 +52,12 @@ export default class ScoreFilter extends React.Component<Props, State> {
   };
 
   handleSave = (minScore: number | null) =>
-    this.closePopup(() => this.props.onChange({ minScore }));
+    this.closePopup(() => {
+      this.props.onChange({ minScore });
+      if (minScore !== null) {
+        Logger.hotelsFilterTagSet('Rating');
+      }
+    });
 
   getTitle = (minScore: number | null) =>
     minScore ? (
diff --git a/app/hotels/src/filter/stars/StarsPopup.js b/app/hotels/src/filter/stars/StarsPopup.js
index 79acd18e..2026e522 100644
--- a/app/hotels/src/filter/stars/StarsPopup.js
+++ b/app/hotels/src/filter/stars/StarsPopup.js
@@ -6,6 +6,7 @@ import {
   ButtonPopup,
   Checkbox,
   Text,
+  Logger,
 } from '@kiwicom/mobile-shared';
 import { View } from 'react-native';
 import { Translation } from '@kiwicom/mobile-localization';
@@ -45,7 +46,10 @@ export default class StarsPopup extends React.Component<Props, State> {
       return { stars };
     });
 
-  onSave = () => this.props.onSave(this.state.stars);
+  onSave = () => {
+    this.props.onSave(this.state.stars);
+    Logger.hotelsFilterTagSet('Stars');
+  };
 
   renderCheckboxes = (stars: number[]) => {
     const checkboxes = [];
diff --git a/app/hotels/src/navigation/singleHotel/SingleHotelNavigationScreen.js b/app/hotels/src/navigation/singleHotel/SingleHotelNavigationScreen.js
index 697b7466..822249f8 100644
--- a/app/hotels/src/navigation/singleHotel/SingleHotelNavigationScreen.js
+++ b/app/hotels/src/navigation/singleHotel/SingleHotelNavigationScreen.js
@@ -2,7 +2,7 @@
 
 import * as React from 'react';
 import { withMappedNavigationAndConfigProps } from 'react-navigation-props-mapper';
-import { WithStandaloneScreen } from '@kiwicom/mobile-shared';
+import { WithStandaloneScreen, Logger } from '@kiwicom/mobile-shared';
 
 import SingleHotel from '../../singleHotel/SingleHotelContainer';
 import type { NavigationProps } from '../HotelsNavigationStack';
@@ -29,6 +29,7 @@ class SingleHotelNavigationScreen extends React.Component<Props> {
       goBack: this.goBack,
       isStandAlonePackage: this.props.isStandAlonePackage,
     });
+    Logger.hotelsDetailOpened();
   }
 
   goBack = () => {
diff --git a/app/hotels/src/singleHotel/__generated__/BookingComSingleHotelQuery.graphql.js b/app/hotels/src/singleHotel/__generated__/BookingComSingleHotelQuery.graphql.js
index c650e663..4c85c714 100644
--- a/app/hotels/src/singleHotel/__generated__/BookingComSingleHotelQuery.graphql.js
+++ b/app/hotels/src/singleHotel/__generated__/BookingComSingleHotelQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 4b503439fe4ac4f5809e6a5fb1b18a1f
+ * @relayHash 14c1f6502f4de74d2b466b6f0e60d536
  */
 
 /* eslint-disable */
@@ -123,6 +123,7 @@ fragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {
   }
   room {
     __typename
+    id
     description {
       title
     }
@@ -134,7 +135,6 @@ fragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {
     }
     maxPersons
     ...BeddingInfo_room
-    id
   }
 }
 
@@ -274,7 +274,7 @@ return {
   "operationKind": "query",
   "name": "BookingComSingleHotelQuery",
   "id": null,
-  "text": "query BookingComSingleHotelQuery(\n  $search: AvailableHotelSearchInput!\n  $options: AvailableHotelOptionsInput\n) {\n  availableHotel(search: $search, options: $options) {\n    ... on HotelAvailabilityInterface {\n      ...HotelDetailScreen_availableHotel\n    }\n    id\n  }\n}\n\nfragment HotelDetailScreen_availableHotel on HotelAvailabilityInterface {\n  hotel {\n    __typename\n    ...Header_hotel\n    ...HotelInformation_hotel\n    id\n  }\n  availableRooms {\n    __typename\n    ...RoomList\n    id\n    incrementalPrice {\n      amount\n      currency\n    }\n  }\n}\n\nfragment Header_hotel on HotelInterface {\n  name\n  mainPhoto {\n    highResUrl\n    id\n  }\n  rating {\n    stars\n    categoryName\n  }\n  photos {\n    edges {\n      node {\n        id\n        lowResUrl\n        highResUrl\n      }\n    }\n  }\n}\n\nfragment HotelInformation_hotel on HotelInterface {\n  ...Location_hotel\n  ...Description_hotel\n  ...HotelReview\n}\n\nfragment RoomList on HotelRoomAvailabilityInterface {\n  id\n  ...RoomRow_availableRoom\n}\n\nfragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {\n  id\n  ...RoomBadges_availableRoom\n  minimalPrice {\n    amount\n    currency\n  }\n  incrementalPrice {\n    amount\n    currency\n  }\n  room {\n    __typename\n    description {\n      title\n    }\n    ...RoomRowTitle_room\n    roomPhotos {\n      highResUrl\n      lowResUrl\n      id\n    }\n    maxPersons\n    ...BeddingInfo_room\n    id\n  }\n}\n\nfragment RoomBadges_availableRoom on HotelRoomAvailabilityInterface {\n  isBreakfastIncluded\n  isRefundable\n}\n\nfragment RoomRowTitle_room on HotelRoomInterface {\n  description {\n    title\n  }\n}\n\nfragment BeddingInfo_room on HotelRoomInterface {\n  maxPersons\n  bedding {\n    type\n    amount\n  }\n}\n\nfragment Location_hotel on HotelInterface {\n  address {\n    street\n    city\n  }\n  coordinates {\n    lat\n    lng\n  }\n}\n\nfragment Description_hotel on HotelInterface {\n  summary\n  ...Amenities\n}\n\nfragment HotelReview on HotelInterface {\n  review {\n    score\n    count\n  }\n}\n\nfragment Amenities on HotelInterface {\n  amenities {\n    id\n    name\n  }\n}\n",
+  "text": "query BookingComSingleHotelQuery(\n  $search: AvailableHotelSearchInput!\n  $options: AvailableHotelOptionsInput\n) {\n  availableHotel(search: $search, options: $options) {\n    ... on HotelAvailabilityInterface {\n      ...HotelDetailScreen_availableHotel\n    }\n    id\n  }\n}\n\nfragment HotelDetailScreen_availableHotel on HotelAvailabilityInterface {\n  hotel {\n    __typename\n    ...Header_hotel\n    ...HotelInformation_hotel\n    id\n  }\n  availableRooms {\n    __typename\n    ...RoomList\n    id\n    incrementalPrice {\n      amount\n      currency\n    }\n  }\n}\n\nfragment Header_hotel on HotelInterface {\n  name\n  mainPhoto {\n    highResUrl\n    id\n  }\n  rating {\n    stars\n    categoryName\n  }\n  photos {\n    edges {\n      node {\n        id\n        lowResUrl\n        highResUrl\n      }\n    }\n  }\n}\n\nfragment HotelInformation_hotel on HotelInterface {\n  ...Location_hotel\n  ...Description_hotel\n  ...HotelReview\n}\n\nfragment RoomList on HotelRoomAvailabilityInterface {\n  id\n  ...RoomRow_availableRoom\n}\n\nfragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {\n  id\n  ...RoomBadges_availableRoom\n  minimalPrice {\n    amount\n    currency\n  }\n  incrementalPrice {\n    amount\n    currency\n  }\n  room {\n    __typename\n    id\n    description {\n      title\n    }\n    ...RoomRowTitle_room\n    roomPhotos {\n      highResUrl\n      lowResUrl\n      id\n    }\n    maxPersons\n    ...BeddingInfo_room\n  }\n}\n\nfragment RoomBadges_availableRoom on HotelRoomAvailabilityInterface {\n  isBreakfastIncluded\n  isRefundable\n}\n\nfragment RoomRowTitle_room on HotelRoomInterface {\n  description {\n    title\n  }\n}\n\nfragment BeddingInfo_room on HotelRoomInterface {\n  maxPersons\n  bedding {\n    type\n    amount\n  }\n}\n\nfragment Location_hotel on HotelInterface {\n  address {\n    street\n    city\n  }\n  coordinates {\n    lat\n    lng\n  }\n}\n\nfragment Description_hotel on HotelInterface {\n  summary\n  ...Amenities\n}\n\nfragment HotelReview on HotelInterface {\n  review {\n    score\n    count\n  }\n}\n\nfragment Amenities on HotelInterface {\n  amenities {\n    id\n    name\n  }\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
@@ -553,6 +553,7 @@ return {
                 "plural": false,
                 "selections": [
                   v2,
+                  v5,
                   {
                     "kind": "LinkedField",
                     "alias": null,
@@ -610,8 +611,7 @@ return {
                       },
                       v7
                     ]
-                  },
-                  v5
+                  }
                 ]
               }
             ]
diff --git a/app/hotels/src/singleHotel/__generated__/Stay22SingleHotelQuery.graphql.js b/app/hotels/src/singleHotel/__generated__/Stay22SingleHotelQuery.graphql.js
index 1b0e52b7..255b3c42 100644
--- a/app/hotels/src/singleHotel/__generated__/Stay22SingleHotelQuery.graphql.js
+++ b/app/hotels/src/singleHotel/__generated__/Stay22SingleHotelQuery.graphql.js
@@ -1,6 +1,6 @@
 /**
  * @flow
- * @relayHash 0015529da418dcb662cc984626097b63
+ * @relayHash 39b7a59ae6d5b1226f0f435ef92e5f49
  */
 
 /* eslint-disable */
@@ -109,6 +109,7 @@ fragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {
   }
   room {
     __typename
+    id
     description {
       title
     }
@@ -120,7 +121,6 @@ fragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {
     }
     maxPersons
     ...BeddingInfo_room
-    id
   }
 }
 
@@ -303,7 +303,7 @@ return {
   "operationKind": "query",
   "name": "Stay22SingleHotelQuery",
   "id": null,
-  "text": "query Stay22SingleHotelQuery(\n  $id: ID!\n  $guests: Int!\n  $currency: Currency\n  $checkin: Date!\n  $checkout: Date!\n) {\n  stay22HotelDetail(id: $id, guests: $guests, currency: $currency, checkin: $checkin, checkout: $checkout) {\n    ...HotelDetailScreen_availableHotel\n    paymentLink\n    id\n  }\n}\n\nfragment HotelDetailScreen_availableHotel on HotelAvailabilityInterface {\n  hotel {\n    __typename\n    ...Header_hotel\n    ...HotelInformation_hotel\n    id\n  }\n  availableRooms {\n    __typename\n    ...RoomList\n    id\n    incrementalPrice {\n      amount\n      currency\n    }\n  }\n}\n\nfragment Header_hotel on HotelInterface {\n  name\n  mainPhoto {\n    highResUrl\n    id\n  }\n  rating {\n    stars\n    categoryName\n  }\n  photos {\n    edges {\n      node {\n        id\n        lowResUrl\n        highResUrl\n      }\n    }\n  }\n}\n\nfragment HotelInformation_hotel on HotelInterface {\n  ...Location_hotel\n  ...Description_hotel\n  ...HotelReview\n}\n\nfragment RoomList on HotelRoomAvailabilityInterface {\n  id\n  ...RoomRow_availableRoom\n}\n\nfragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {\n  id\n  ...RoomBadges_availableRoom\n  minimalPrice {\n    amount\n    currency\n  }\n  incrementalPrice {\n    amount\n    currency\n  }\n  room {\n    __typename\n    description {\n      title\n    }\n    ...RoomRowTitle_room\n    roomPhotos {\n      highResUrl\n      lowResUrl\n      id\n    }\n    maxPersons\n    ...BeddingInfo_room\n    id\n  }\n}\n\nfragment RoomBadges_availableRoom on HotelRoomAvailabilityInterface {\n  isBreakfastIncluded\n  isRefundable\n}\n\nfragment RoomRowTitle_room on HotelRoomInterface {\n  description {\n    title\n  }\n}\n\nfragment BeddingInfo_room on HotelRoomInterface {\n  maxPersons\n  bedding {\n    type\n    amount\n  }\n}\n\nfragment Location_hotel on HotelInterface {\n  address {\n    street\n    city\n  }\n  coordinates {\n    lat\n    lng\n  }\n}\n\nfragment Description_hotel on HotelInterface {\n  summary\n  ...Amenities\n}\n\nfragment HotelReview on HotelInterface {\n  review {\n    score\n    count\n  }\n}\n\nfragment Amenities on HotelInterface {\n  amenities {\n    id\n    name\n  }\n}\n",
+  "text": "query Stay22SingleHotelQuery(\n  $id: ID!\n  $guests: Int!\n  $currency: Currency\n  $checkin: Date!\n  $checkout: Date!\n) {\n  stay22HotelDetail(id: $id, guests: $guests, currency: $currency, checkin: $checkin, checkout: $checkout) {\n    ...HotelDetailScreen_availableHotel\n    paymentLink\n    id\n  }\n}\n\nfragment HotelDetailScreen_availableHotel on HotelAvailabilityInterface {\n  hotel {\n    __typename\n    ...Header_hotel\n    ...HotelInformation_hotel\n    id\n  }\n  availableRooms {\n    __typename\n    ...RoomList\n    id\n    incrementalPrice {\n      amount\n      currency\n    }\n  }\n}\n\nfragment Header_hotel on HotelInterface {\n  name\n  mainPhoto {\n    highResUrl\n    id\n  }\n  rating {\n    stars\n    categoryName\n  }\n  photos {\n    edges {\n      node {\n        id\n        lowResUrl\n        highResUrl\n      }\n    }\n  }\n}\n\nfragment HotelInformation_hotel on HotelInterface {\n  ...Location_hotel\n  ...Description_hotel\n  ...HotelReview\n}\n\nfragment RoomList on HotelRoomAvailabilityInterface {\n  id\n  ...RoomRow_availableRoom\n}\n\nfragment RoomRow_availableRoom on HotelRoomAvailabilityInterface {\n  id\n  ...RoomBadges_availableRoom\n  minimalPrice {\n    amount\n    currency\n  }\n  incrementalPrice {\n    amount\n    currency\n  }\n  room {\n    __typename\n    id\n    description {\n      title\n    }\n    ...RoomRowTitle_room\n    roomPhotos {\n      highResUrl\n      lowResUrl\n      id\n    }\n    maxPersons\n    ...BeddingInfo_room\n  }\n}\n\nfragment RoomBadges_availableRoom on HotelRoomAvailabilityInterface {\n  isBreakfastIncluded\n  isRefundable\n}\n\nfragment RoomRowTitle_room on HotelRoomInterface {\n  description {\n    title\n  }\n}\n\nfragment BeddingInfo_room on HotelRoomInterface {\n  maxPersons\n  bedding {\n    type\n    amount\n  }\n}\n\nfragment Location_hotel on HotelInterface {\n  address {\n    street\n    city\n  }\n  coordinates {\n    lat\n    lng\n  }\n}\n\nfragment Description_hotel on HotelInterface {\n  summary\n  ...Amenities\n}\n\nfragment HotelReview on HotelInterface {\n  review {\n    score\n    count\n  }\n}\n\nfragment Amenities on HotelInterface {\n  amenities {\n    id\n    name\n  }\n}\n",
   "metadata": {},
   "fragment": {
     "kind": "Fragment",
@@ -583,6 +583,7 @@ return {
                 "plural": false,
                 "selections": [
                   v3,
+                  v6,
                   {
                     "kind": "LinkedField",
                     "alias": null,
@@ -640,8 +641,7 @@ return {
                       },
                       v8
                     ]
-                  },
-                  v6
+                  }
                 ]
               }
             ]
diff --git a/app/hotels/src/singleHotel/header/Header.js b/app/hotels/src/singleHotel/header/Header.js
index de2bd9e9..f020a47a 100644
--- a/app/hotels/src/singleHotel/header/Header.js
+++ b/app/hotels/src/singleHotel/header/Header.js
@@ -8,6 +8,7 @@ import {
   Text,
   Touchable,
   Stars,
+  Logger,
 } from '@kiwicom/mobile-shared';
 import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
 import { Translation } from '@kiwicom/mobile-localization';
@@ -52,6 +53,9 @@ export class Header extends React.Component<Props> {
       hotelName,
       images,
     });
+    Logger.hotelsGalleryOpened(
+      Logger.HotelGalleryType.HOTELS_GALLERY_TYPE_HOTEL,
+    );
   };
 
   render() {
diff --git a/app/hotels/src/singleHotel/hotelInformation/description/Description.js b/app/hotels/src/singleHotel/hotelInformation/description/Description.js
index 2f15a338..3a3b142b 100644
--- a/app/hotels/src/singleHotel/hotelInformation/description/Description.js
+++ b/app/hotels/src/singleHotel/hotelInformation/description/Description.js
@@ -3,7 +3,7 @@
 import * as React from 'react';
 import { View } from 'react-native';
 import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
-import { Text, StyleSheet, ReadMore } from '@kiwicom/mobile-shared';
+import { Text, StyleSheet, ReadMore, Logger } from '@kiwicom/mobile-shared';
 import { Translation } from '@kiwicom/mobile-localization';
 import { defaultTokens } from '@kiwicom/mobile-orbit';
 
@@ -31,6 +31,7 @@ export const Description = (props: Props) => (
       numberOfLines={5}
       truncatedText={<Translation id="single_hotel.description.show_more" />}
       revealedText={<Translation id="single_hotel.description.show_less" />}
+      onExpandText={Logger.hotelsDetailDescriptionExpanded}
     >
       <Text style={styles.summary}>
         <Translation passThrough={props.hotel?.summary} />
diff --git a/app/hotels/src/singleHotel/hotelInformation/location/Location.js b/app/hotels/src/singleHotel/hotelInformation/location/Location.js
index 0637a58d..0ae60afc 100644
--- a/app/hotels/src/singleHotel/hotelInformation/location/Location.js
+++ b/app/hotels/src/singleHotel/hotelInformation/location/Location.js
@@ -9,6 +9,7 @@ import {
   DropMarker,
   Text,
   TouchableWithoutFeedback,
+  Logger,
 } from '@kiwicom/mobile-shared';
 import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
 import { Translation } from '@kiwicom/mobile-localization';
@@ -50,6 +51,7 @@ export class Location extends React.Component<Props> {
       roomsConfiguration: this.props.roomsConfiguration,
       currency: this.props.currency,
     });
+    Logger.hotelsDetailMapOpened();
   };
 
   render() {
diff --git a/app/hotels/src/singleHotel/roomList/RoomRow.js b/app/hotels/src/singleHotel/roomList/RoomRow.js
index 103fecaa..fbab2d62 100644
--- a/app/hotels/src/singleHotel/roomList/RoomRow.js
+++ b/app/hotels/src/singleHotel/roomList/RoomRow.js
@@ -2,7 +2,7 @@
 
 import * as React from 'react';
 import { View } from 'react-native';
-import { StyleSheet } from '@kiwicom/mobile-shared';
+import { StyleSheet, Logger } from '@kiwicom/mobile-shared';
 import { createFragmentContainer, graphql } from '@kiwicom/mobile-relay';
 import {
   type NavigationType,
@@ -52,24 +52,27 @@ export class RoomRow extends React.Component<Props> {
         id: 'single_hotel.alert.cannot_book_more_rooms_than_guests',
       });
     } else {
-      const { id, maxPersons } = this.getIdAndMaxPersons();
+      const { id, maxPersons, type, roomId } = this.getSelectData();
       if (id && maxPersons) {
         this.props.select(id, maxPersons);
+        Logger.hotelsDetailRoomSelected(roomId, type);
       }
     }
   };
 
   deselect = () => {
-    const { id, maxPersons } = this.getIdAndMaxPersons();
+    const { id, maxPersons } = this.getSelectData();
     if (id && maxPersons) {
       this.props.deselect(id, maxPersons);
     }
   };
 
-  getIdAndMaxPersons = () => {
+  getSelectData = () => {
     const id = this.props.availableRoom?.id;
     const maxPersons = this.props.availableRoom?.room?.maxPersons;
-    return { id, maxPersons };
+    const type = this.props.availableRoom?.room?.description?.title ?? '';
+    const roomId = this.props.availableRoom?.room?.id ?? '';
+    return { id, maxPersons, type, roomId };
   };
 
   openGallery = () => {
@@ -92,6 +95,9 @@ export class RoomRow extends React.Component<Props> {
       hotelName: roomTitle,
       images: photos,
     });
+    Logger.hotelsGalleryOpened(
+      Logger.HotelGalleryType.HOTELS_GALLERY_TYPE_ROOM,
+    );
   };
 
   render() {
@@ -173,6 +179,7 @@ export default (createFragmentContainer(
         currency
       }
       room {
+        id
         description {
           title
         }
diff --git a/app/hotels/src/singleHotel/roomList/__generated__/RoomRow_availableRoom.graphql.js b/app/hotels/src/singleHotel/roomList/__generated__/RoomRow_availableRoom.graphql.js
index d61e68ad..08bbbf9e 100644
--- a/app/hotels/src/singleHotel/roomList/__generated__/RoomRow_availableRoom.graphql.js
+++ b/app/hotels/src/singleHotel/roomList/__generated__/RoomRow_availableRoom.graphql.js
@@ -24,6 +24,7 @@ export type RoomRow_availableRoom = {|
     +currency: ?string,
   |}>,
   +room: ?{|
+    +id: string,
     +description: ?{|
       +title: ?string
     |},
@@ -107,6 +108,7 @@ return {
       "concreteType": null,
       "plural": false,
       "selections": [
+        v0,
         {
           "kind": "LinkedField",
           "alias": null,
@@ -174,5 +176,5 @@ return {
 };
 })();
 // prettier-ignore
-(node/*: any*/).hash = '57f86108a4be2fcefd95d22224110b83';
+(node/*: any*/).hash = 'fbd7c495ebab8dc3c39eae6386da62a1';
 module.exports = node;
diff --git a/app/native.js b/app/native.js
index 288de36b..530c76b0 100644
--- a/app/native.js
+++ b/app/native.js
@@ -27,4 +27,6 @@ AppRegistry.registerComponent(
 );
 
 // This file is only used for native integration and we use CodePush there
-CodePush.sync();
+CodePush.sync({
+  installMode: CodePush.InstallMode.IMMEDIATE,
+});
diff --git a/packages/localization/src/TranslationHelpers.js b/packages/localization/src/TranslationHelpers.js
index b88b13bb..57ae977c 100644
--- a/packages/localization/src/TranslationHelpers.js
+++ b/packages/localization/src/TranslationHelpers.js
@@ -1,16 +1,21 @@
 //@flow
 
-import DefaultVocabulary, { type TranslationKeys } from './DefaultVocabulary';
+import DefaultVocabulary, {
+  type TranslationKeys,
+  type TranslationKeysObject,
+} from './DefaultVocabulary';
 
 export const getTranslation = (
   translatedString: ?string,
   translationId: TranslationKeys,
+  vocabulary: TranslationKeysObject = DefaultVocabulary,
 ): string => {
   const nativeKey = `mobile.${translationId}`;
+  const defaultTranslation = vocabulary[translationId];
 
   if (translatedString === nativeKey) {
     // fallback to our dummy vocabulary because native code didn't provide translation
-    return DefaultVocabulary[translationId];
+    return defaultTranslation;
   }
   if (translatedString == null) {
     // Everything failed - native translation even our fallback. This usually
@@ -18,7 +23,36 @@ export const getTranslation = (
     // we can actually do is to use original key as a text.
     return translationId;
   }
-  return translatedString;
+
+  // Translated strings from native come under the form
+  // %1$@, so we lose information about the variables.
+  // We add this information back in:
+  try {
+    const NATIVE_TRANSLATION_PARAM_REGEX = /%([0-9]+)\$@/g;
+    const compatibleTranslatedString = translatedString.replace(
+      NATIVE_TRANSLATION_PARAM_REGEX,
+      (match, variableIndex) => {
+        const ORIGINAL_PARAM_REGEX = new RegExp(
+          `__(${variableIndex}_)?([a-zA-Z-_]+)__`,
+          'g',
+        );
+        const originalParamMatches = defaultTranslation.match(
+          ORIGINAL_PARAM_REGEX,
+        );
+
+        if (originalParamMatches == null || originalParamMatches.length !== 1) {
+          throw new Error(
+            `There was a problem with translationId ${translationId}`,
+          );
+        }
+
+        return originalParamMatches[0];
+      },
+    );
+    return compatibleTranslatedString;
+  } catch (e) {
+    return translatedString;
+  }
 };
 
 export function replaceValues(text: string, values?: Object): string {
diff --git a/packages/playground/src/PlaygroundImports.js b/packages/playground/src/PlaygroundImports.js
index 2ed93556..b644fb4c 100644
--- a/packages/playground/src/PlaygroundImports.js
+++ b/packages/playground/src/PlaygroundImports.js
@@ -27,7 +27,7 @@ import '../../../packages/shared/src/buttons/__tests__/IncrementDecrementButtons
 import '../../../packages/shared/src/buttons/__tests__/Button.test.js';
 import '../../../packages/shared/src/__tests__/Text.test.js';
 import '../../../packages/shared/src/__tests__/SimpleCard.test.js';
-import '../../../packages/shared/src/__tests__/ReadMore.test.js';
+import '../../../packages/shared/src/readMore/__tests__/ReadMore.test.js';
 import '../../../packages/shared/src/__tests__/DropMarker.playground.test.js';
 import '../../../packages/shared/src/__tests__/AdaptableBadge.test.js';
 import '../../../packages/shared/src/offlineScreen/__tests__/OfflineScreen.test';
diff --git a/packages/shared/index.js b/packages/shared/index.js
index 42c593ba..85af55dd 100644
--- a/packages/shared/index.js
+++ b/packages/shared/index.js
@@ -74,7 +74,7 @@ export { default as SimpleCard } from './src/SimpleCard';
 export { default as GeneralError } from './src/errors/GeneralError';
 export { default as ErrorMessage } from './src/errors/ErrorMessage';
 
-export { default as ReadMore } from './src/ReadMore';
+export { default as ReadMore } from './src/readMore/ReadMore';
 export { default as BottomSheet } from './src/BottomSheet';
 export { default as RefreshableScrollView } from './src/RefreshableScrollView';
 
diff --git a/packages/shared/src/Logger.js b/packages/shared/src/Logger.js
index 765c8bdb..16f90538 100644
--- a/packages/shared/src/Logger.js
+++ b/packages/shared/src/Logger.js
@@ -43,7 +43,7 @@ function hotelsResultsDisplayed(searchQuery: ?string, params: ?string) {
 }
 
 function hotelsFilterTagSet(filterTag: string) {
-  LoggingModule.hotelsFilterTagSet(filterTag);
+  LoggingModule.hotelsSelectedFilterTag(filterTag);
 }
 
 function hotelsDetailOpened() {
diff --git a/packages/shared/src/ReadMore.js b/packages/shared/src/readMore/ReadMore.js
similarity index 58%
rename from packages/shared/src/ReadMore.js
rename to packages/shared/src/readMore/ReadMore.js
index 7152c21a..601ee874 100644
--- a/packages/shared/src/ReadMore.js
+++ b/packages/shared/src/readMore/ReadMore.js
@@ -1,15 +1,11 @@
 // @flow strict
 
 import * as React from 'react';
-import { View } from 'react-native';
 import OriginalReadMore from 'react-native-read-more-text'; // eslint-disable-line no-restricted-imports
 import { Translation } from '@kiwicom/mobile-localization';
-import { defaultTokens } from '@kiwicom/mobile-orbit';
 
-import Text from './Text';
-import Touchable from './Touchable';
-import StyleSheet from './PlatformStyleSheet';
-import type { StylePropType } from '../types/Styles';
+import ReadMoreLink from './ReadMoreLink';
+import type { StylePropType } from '../../types/Styles';
 
 type Props = {|
   +numberOfLines: number,
@@ -17,38 +13,30 @@ type Props = {|
   +truncatedText: React.Element<typeof Translation>,
   +revealedText: React.Element<typeof Translation>,
   +style?: StylePropType,
+  +onExpandText?: () => void,
+  +onHideText?: () => void,
 |};
 
-const Link = ({
-  label,
-  handlePress,
-  style,
-}: {|
-  label: React.Element<typeof Translation>,
-  handlePress: () => void,
-  style?: StylePropType,
-|}) => (
-  <View style={styles.linkView}>
-    <Touchable onPress={handlePress} noRipple={true}>
-      <Text style={[styles.linkText, style]}>{label}</Text>
-    </Touchable>
-  </View>
-);
-
 export default class ReadMore extends React.Component<Props> {
   renderTruncatedFooter = (handlePress: () => void) => (
-    <Link
+    <ReadMoreLink
       style={this.props.style}
       label={this.props.truncatedText}
       handlePress={handlePress}
+      type="truncated"
+      onExpandText={this.props.onExpandText}
+      onHideText={this.props.onHideText}
     />
   );
 
   renderRevealedFooter = (handlePress: () => void) => (
-    <Link
+    <ReadMoreLink
       style={this.props.style}
       label={this.props.revealedText}
       handlePress={handlePress}
+      type="revealed"
+      onExpandText={this.props.onExpandText}
+      onHideText={this.props.onHideText}
     />
   );
 
@@ -64,13 +52,3 @@ export default class ReadMore extends React.Component<Props> {
     );
   }
 }
-
-const styles = StyleSheet.create({
-  linkView: {
-    flexDirection: 'row',
-  },
-  linkText: {
-    color: defaultTokens.paletteProductNormal,
-    fontWeight: '500',
-  },
-});
diff --git a/packages/shared/src/readMore/ReadMoreLink.js b/packages/shared/src/readMore/ReadMoreLink.js
new file mode 100644
index 00000000..c888292a
--- /dev/null
+++ b/packages/shared/src/readMore/ReadMoreLink.js
@@ -0,0 +1,58 @@
+// @flow strict
+
+import * as React from 'react';
+import { View } from 'react-native';
+import { Translation } from '@kiwicom/mobile-localization';
+import { defaultTokens } from '@kiwicom/mobile-orbit';
+
+import Touchable from '../Touchable';
+import StyleSheet from '../PlatformStyleSheet';
+import Text from '../Text';
+import type { StylePropType } from '../../types/Styles';
+
+type Props = {|
+  +label: React.Element<typeof Translation>,
+  +handlePress: () => void,
+  +type: 'revealed' | 'truncated',
+  +onExpandText: ?() => void,
+  +onHideText: ?() => void,
+  +style?: StylePropType,
+|};
+
+class ReadMoreLink extends React.Component<Props> {
+  handlePress = () => {
+    const { type, onExpandText, onHideText } = this.props;
+    this.props.handlePress();
+
+    if (type === 'truncated' && onExpandText != null) {
+      onExpandText();
+    }
+    if (type === 'revealed' && onHideText != null) {
+      onHideText();
+    }
+  };
+
+  render() {
+    const { style, label } = this.props;
+
+    return (
+      <View style={styles.linkView}>
+        <Touchable onPress={this.handlePress} noRipple={true}>
+          <Text style={[styles.linkText, style]}>{label}</Text>
+        </Touchable>
+      </View>
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  linkView: {
+    flexDirection: 'row',
+  },
+  linkText: {
+    color: defaultTokens.paletteProductNormal,
+    fontWeight: '500',
+  },
+});
+
+export default ReadMoreLink;


Test patch:
diff --git a/packages/localization/src/__tests__/TranslationHelpers.test.js b/packages/localization/src/__tests__/TranslationHelpers.test.js
new file mode 100644
index 00000000..3d6ce67a
--- /dev/null
+++ b/packages/localization/src/__tests__/TranslationHelpers.test.js
@@ -0,0 +1,51 @@
+// @flow strict
+
+import { getTranslation } from '../TranslationHelpers';
+
+const vocabulary = {
+  'single_hotel.hotel_review.reviews': '__count__ Reviews',
+  'hotels.gallery.pagination': '__1_photoNumber__ of __2_totalPhotos__',
+};
+
+describe('getTranslation:', () => {
+  it('returns default translation if string is not translated', () => {
+    const translationId = 'single_hotel.hotel_review.reviews';
+    const notTranslatedKey = 'mobile.single_hotel.hotel_review.reviews';
+    expect(getTranslation(notTranslatedKey, translationId, vocabulary)).toBe(
+      '__count__ Reviews',
+    );
+  });
+
+  it('returns translation id if translated string is null/undefined', () => {
+    const translationId = 'single_hotel.hotel_review.reviews';
+    const notTranslatedKey = null;
+    expect(getTranslation(notTranslatedKey, translationId, vocabulary)).toBe(
+      translationId,
+    );
+  });
+
+  it('handles natively translated strings -- with one parameter', () => {
+    const translationId = 'single_hotel.hotel_review.reviews';
+    const translatedString = '%1$@ Reviews';
+    expect(getTranslation(translatedString, translationId, vocabulary)).toBe(
+      '__count__ Reviews',
+    );
+  });
+
+  it('handles natively translated strings -- with several parameters', () => {
+    const translationId = 'hotels.gallery.pagination';
+    const translatedString = '%1$@ of %2$@';
+    expect(getTranslation(translatedString, translationId, vocabulary)).toBe(
+      '__1_photoNumber__ of __2_totalPhotos__',
+    );
+  });
+
+  it('returns translated string if parameter matching is not possible', () => {
+    const translationId = 'hotels.gallery.pagination';
+    const translatedString = '%1$@ of %2$@';
+    const mistake = ' and MISTAKE %3$@';
+    expect(
+      getTranslation(translatedString + mistake, translationId, vocabulary),
+    ).toBe(translatedString + mistake);
+  });
+});
diff --git a/packages/shared/src/__tests__/ReadMore.test.js b/packages/shared/src/readMore/__tests__/ReadMore.test.js
similarity index 100%
rename from packages/shared/src/__tests__/ReadMore.test.js
rename to packages/shared/src/readMore/__tests__/ReadMore.test.js
diff --git a/packages/shared/src/readMore/__tests__/ReadMoreLink.test.js b/packages/shared/src/readMore/__tests__/ReadMoreLink.test.js
new file mode 100644
index 00000000..0846f689
--- /dev/null
+++ b/packages/shared/src/readMore/__tests__/ReadMoreLink.test.js
@@ -0,0 +1,49 @@
+// @flow
+
+import * as React from 'react';
+import renderer from 'react-test-renderer';
+import { Translation } from '@kiwicom/mobile-localization';
+
+import ReadMoreLink from '../ReadMoreLink';
+
+const getProps = (
+  type = 'revealed',
+  onExpandText = null,
+  onHideText = null,
+  optionalProps = {},
+) => ({
+  label: <Translation passThrough="test" />,
+  handlePress: jest.fn(),
+  type,
+  onExpandText,
+  onHideText,
+  ...optionalProps,
+});
+
+it('renders', () => {
+  expect(renderer.create(<ReadMoreLink {...getProps()} />)).toMatchSnapshot();
+});
+
+it('calls onExpandText', () => {
+  const onExpandText = jest.fn();
+  const onHideText = jest.fn();
+  const props = getProps('truncated', onExpandText, onHideText);
+
+  const wrapper = renderer.create(<ReadMoreLink {...props} />).getInstance();
+  wrapper.handlePress();
+
+  expect(props.onExpandText).toHaveBeenCalled();
+  expect(props.onHideText).not.toHaveBeenCalled();
+});
+
+it('calls onHideText', () => {
+  const onExpandText = jest.fn();
+  const onHideText = jest.fn();
+  const props = getProps('revealed', onExpandText, onHideText);
+
+  const wrapper = renderer.create(<ReadMoreLink {...props} />).getInstance();
+  wrapper.handlePress();
+
+  expect(props.onHideText).toHaveBeenCalled();
+  expect(props.onExpandText).not.toHaveBeenCalled();
+});
diff --git a/packages/shared/src/__tests__/__snapshots__/ReadMore.test.js.snap b/packages/shared/src/readMore/__tests__/__snapshots__/ReadMore.test.js.snap
similarity index 100%
rename from packages/shared/src/__tests__/__snapshots__/ReadMore.test.js.snap
rename to packages/shared/src/readMore/__tests__/__snapshots__/ReadMore.test.js.snap
diff --git a/packages/shared/src/readMore/__tests__/__snapshots__/ReadMoreLink.test.js.snap b/packages/shared/src/readMore/__tests__/__snapshots__/ReadMoreLink.test.js.snap
new file mode 100644
index 00000000..af1bbff5
--- /dev/null
+++ b/packages/shared/src/readMore/__tests__/__snapshots__/ReadMoreLink.test.js.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<View
+  style={
+    Object {
+      "flexDirection": "row",
+    }
+  }
+>
+  <View
+    accessible={true}
+    isTVSelectable={true}
+    onResponderGrant={[Function]}
+    onResponderMove={[Function]}
+    onResponderRelease={[Function]}
+    onResponderTerminate={[Function]}
+    onResponderTerminationRequest={[Function]}
+    onStartShouldSetResponder={[Function]}
+    style={
+      Object {
+        "opacity": 1,
+      }
+    }
+  >
+    <Text
+      style={
+        Array [
+          Object {
+            "color": "#171b1e",
+            "fontFamily": "CircularPro-Book",
+            "fontSize": 14,
+            "fontWeight": "normal",
+            "letterSpacing": -0.15,
+          },
+          Array [
+            Object {
+              "color": "#00a991",
+              "fontWeight": "500",
+            },
+            undefined,
+          ],
+        ]
+      }
+    >
+      <Text>
+        test
+      </Text>
+    </Text>
+  </View>
+</View>
+`;
diff --git a/scripts/test.sh b/scripts/test.sh
index 9791f37e..f20c9ec0 100755
--- a/scripts/test.sh
+++ b/scripts/test.sh
@@ -6,5 +6,5 @@ set -x # print a trace of simple commands
 
 yarn lint
 yarn flow
-node node_modules/jest/bin/jest.js --ci --color --logHeapUsage --maxWorkers=4 --config=.jest.json
+node node_modules/jest/bin/jest.js --ci --color --logHeapUsage --config=.jest.json
 yarn relay --validate