visit.queries.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. const { isAfter, subDays, subHours, set } = require("date-fns");
  2. const utils = require("../utils");
  3. const redis = require("../redis");
  4. const knex = require("../knex");
  5. async function add(params) {
  6. const data = {
  7. ...params,
  8. country: params.country.toLowerCase(),
  9. referrer: params.referrer.toLowerCase()
  10. };
  11. const truncatedNow = new Date();
  12. truncatedNow.setMinutes(0, 0, 0);
  13. // Create a subquery first that truncates the
  14. const subquery = knex("visits")
  15. .select("visits.*")
  16. .select({
  17. created_at_hours: utils.knexUtils(knex).truncatedTimestamp("created_at", "hour")
  18. })
  19. .where({ link_id: params.id })
  20. .as("subquery");
  21. const visit = await knex
  22. .select("*")
  23. .from(subquery)
  24. .where("created_at_hours", "=", truncatedNow.toISOString())
  25. .first();
  26. if (visit) {
  27. await knex("visits")
  28. .where({ id: visit.id })
  29. .increment(`br_${data.browser}`, 1)
  30. .increment(`os_${data.os}`, 1)
  31. .increment("total", 1)
  32. .update({
  33. updated_at: new Date().toISOString(),
  34. countries: knex.raw(
  35. "jsonb_set(countries, '{??}', (COALESCE(countries->>?,'0')::int + 1)::text::jsonb)",
  36. [data.country, data.country]
  37. ),
  38. referrers: knex.raw(
  39. "jsonb_set(referrers, '{??}', (COALESCE(referrers->>?,'0')::int + 1)::text::jsonb)",
  40. [data.referrer, data.referrer]
  41. )
  42. });
  43. } else {
  44. await knex("visits").insert({
  45. [`br_${data.browser}`]: 1,
  46. countries: { [data.country]: 1 },
  47. referrers: { [data.referrer]: 1 },
  48. [`os_${data.os}`]: 1,
  49. total: 1,
  50. link_id: data.id
  51. });
  52. }
  53. return visit;
  54. }
  55. async function find(match, total) {
  56. if (match.link_id) {
  57. const key = redis.key.stats(match.link_id);
  58. const cached = await redis.client.get(key);
  59. if (cached) return JSON.parse(cached);
  60. }
  61. const stats = {
  62. lastDay: {
  63. stats: utils.getInitStats(),
  64. views: new Array(24).fill(0),
  65. total: 0
  66. },
  67. lastWeek: {
  68. stats: utils.getInitStats(),
  69. views: new Array(7).fill(0),
  70. total: 0
  71. },
  72. lastMonth: {
  73. stats: utils.getInitStats(),
  74. views: new Array(30).fill(0),
  75. total: 0
  76. },
  77. lastYear: {
  78. stats: utils.getInitStats(),
  79. views: new Array(12).fill(0),
  80. total: 0
  81. }
  82. };
  83. const visitsStream = knex("visits").where(match).stream();
  84. const nowUTC = utils.getUTCDate();
  85. const now = new Date();
  86. const periods = utils.getStatsPeriods(now);
  87. for await (const visit of visitsStream) {
  88. periods.forEach(([type, fromDate]) => {
  89. const isIncluded = isAfter(new Date(visit.created_at), fromDate);
  90. if (!isIncluded) return;
  91. const diffFunction = utils.getDifferenceFunction(type);
  92. const diff = diffFunction(now, new Date(visit.created_at));
  93. const index = stats[type].views.length - diff - 1;
  94. const view = stats[type].views[index];
  95. const period = stats[type].stats;
  96. stats[type].stats = {
  97. browser: {
  98. chrome: period.browser.chrome + visit.br_chrome,
  99. edge: period.browser.edge + visit.br_edge,
  100. firefox: period.browser.firefox + visit.br_firefox,
  101. ie: period.browser.ie + visit.br_ie,
  102. opera: period.browser.opera + visit.br_opera,
  103. other: period.browser.other + visit.br_other,
  104. safari: period.browser.safari + visit.br_safari
  105. },
  106. os: {
  107. android: period.os.android + visit.os_android,
  108. ios: period.os.ios + visit.os_ios,
  109. linux: period.os.linux + visit.os_linux,
  110. macos: period.os.macos + visit.os_macos,
  111. other: period.os.other + visit.os_other,
  112. windows: period.os.windows + visit.os_windows
  113. },
  114. country: {
  115. ...period.country,
  116. ...Object.entries(visit.countries).reduce(
  117. (obj, [country, count]) => ({
  118. ...obj,
  119. [country]: (period.country[country] || 0) + count
  120. }),
  121. {}
  122. )
  123. },
  124. referrer: {
  125. ...period.referrer,
  126. ...Object.entries(visit.referrers).reduce(
  127. (obj, [referrer, count]) => ({
  128. ...obj,
  129. [referrer]: (period.referrer[referrer] || 0) + count
  130. }),
  131. {}
  132. )
  133. }
  134. };
  135. stats[type].views[index] += visit.total;
  136. stats[type].total += visit.total;
  137. });
  138. }
  139. const response = {
  140. lastYear: {
  141. stats: utils.statsObjectToArray(stats.lastYear.stats),
  142. views: stats.lastYear.views,
  143. total: stats.lastYear.total
  144. },
  145. lastDay: {
  146. stats: utils.statsObjectToArray(stats.lastDay.stats),
  147. views: stats.lastDay.views,
  148. total: stats.lastDay.total
  149. },
  150. lastMonth: {
  151. stats: utils.statsObjectToArray(stats.lastMonth.stats),
  152. views: stats.lastMonth.views,
  153. total: stats.lastMonth.total
  154. },
  155. lastWeek: {
  156. stats: utils.statsObjectToArray(stats.lastWeek.stats),
  157. views: stats.lastWeek.views,
  158. total: stats.lastWeek.total
  159. },
  160. updatedAt: new Date().toISOString()
  161. };
  162. if (match.link_id) {
  163. const cacheTime = utils.getStatsCacheTime(total);
  164. const key = redis.key.stats(match.link_id);
  165. redis.client.set(key, JSON.stringify(response), "EX", cacheTime);
  166. }
  167. return response;
  168. };
  169. module.exports = {
  170. add,
  171. find
  172. };