Commit f2751134 authored by Mark Harding's avatar Mark Harding

Merge branch 'master' of https://gitlab.com/Minds/engine into epic/SSR

1 merge request!196WIP: Frontend rendering module
Pipeline #109004073 passed with stages
in 7 minutes and 8 seconds
......@@ -78,7 +78,7 @@ class register implements Interfaces\Api, Interfaces\ApiIgnorePam
$hasSignupTags = false;
if (isset($_COOKIE['mexp'])) {
$manager = Core\Di\Di::_()->get('Experiments\Manager');
$bucket = $manager->getBucketForExperiment('Homepage200619');
$bucket = $manager->getBucketForExperiment('Homepage121119');
$user->expHomepage200619 = $bucket->getId();
}
......
......@@ -49,6 +49,7 @@ class experiments implements Interfaces\Api
$_COOKIE['mexp'] = $cookieid;
}
/** @var Core\Experiments\Manager $manager */
$manager = Di::_()->get('Experiments\Manager');
if (Core\Session::isLoggedIn()) {
......
......@@ -3,6 +3,7 @@
namespace Minds\Controllers\api\v2\onboarding;
use Minds\Api\Factory;
use Minds\Core\Di\Di;
use Minds\Core\Onboarding\Manager;
use Minds\Core\Session;
use Minds\Interfaces;
......@@ -11,7 +12,7 @@ class progress implements Interfaces\Api
{
/**
* Equivalent to HTTP GET method
* @param array $pages
* @param array $pages
* @return mixed|null
* @throws \Exception
*/
......@@ -23,11 +24,20 @@ class progress implements Interfaces\Api
$manager = new Manager();
$manager->setUser(Session::getLoggedInUser());
/** @var \Minds\Core\Features\Manager $manager */
$manager = Di::_()->get('Features\Manager');
if ($manager->has('onboarding-december-2019')) {
return Factory::response([
'show_onboarding' => !$manager->wasOnboardingShown(),
]);
}
$allItems = $manager->getAllItems();
$completedItems = $manager->getCompletedItems();
return Factory::response([
'show_onboarding' => !$manager->wasOnboardingShown() && count($allItems) > count($completedItems) ,
'show_onboarding' => !$manager->wasOnboardingShown() && count($allItems) > count($completedItems),
'all_items' => $allItems,
'completed_items' => $completedItems,
'creator_frequency' => $manager->getCreatorFrequency(),
......@@ -36,7 +46,7 @@ class progress implements Interfaces\Api
/**
* Equivalent to HTTP POST method
* @param array $pages
* @param array $pages
* @return mixed|null
* @throws \Exception
*/
......@@ -47,7 +57,7 @@ class progress implements Interfaces\Api
/**
* Equivalent to HTTP PUT method
* @param array $pages
* @param array $pages
* @return mixed|null
*/
public function put($pages)
......@@ -57,7 +67,7 @@ class progress implements Interfaces\Api
/**
* Equivalent to HTTP DELETE method
* @param array $pages
* @param array $pages
* @return mixed|null
*/
public function delete($pages)
......
......@@ -24,14 +24,15 @@ class deeplinks implements Interfaces\Api, Interfaces\ApiIgnorePam
[
'appID' => "35U3998VRZ.com.minds.mobile",
'paths' => [
'/groups/profile/*',
'/groups/*',
'/media/*',
'/newsfeed/*',
'/blog/view/*',
'/blog/*',
'/channels/*',
'/*'
'/email-confirmation'
// '/groups/profile/*',
// '/groups/*',
// '/media/*',
// '/newsfeed/*',
// '/blog/view/*',
// '/blog/*',
// '/channels/*',
// '/*'
]
]
]
......
<?php
namespace Minds\Core\Experiments\Hypotheses;
use Minds\Core\Experiments\Bucket;
class Homepage121119 implements HypothesisInterface
{
/**
* Return the id for the hypothesis
* @return string
*/
public function getId()
{
return "Homepage121119";
}
/**
* Return the buckets for the hypothesis
* @return Bucket[]
*/
public function getBuckets()
{
return [
(new Bucket)
->setId('base')
->setWeight(50),
(new Bucket)
->setId('form')
->setWeight(50),
];
}
}
......@@ -18,6 +18,7 @@ class Manager
private $experiments = [
'Homepage121118' => Hypotheses\Homepage121118::class,
'Homepage200619' => Hypotheses\Homepage200619::class,
'Homepage121119' => Hypotheses\Homepage121119::class,
];
public function __construct($sampler = null)
......@@ -49,6 +50,7 @@ class Manager
* Return the bucket for an experiment
* @param string $experimentId
* @return Bucket
* @throws \Exception
*/
public function getBucketForExperiment($experimentId)
{
......
......@@ -5,6 +5,9 @@
*/
namespace Minds\Core\Experiments;
use Minds\Core\Analytics\User;
use Minds\Core\Data\Client;
use Minds\Core\Experiments\Hypotheses\HypothesisInterface;
use Minds\Interfaces\ModuleInterface;
use Minds\Core\Di\Di;
use Minds\Core\Data\Cassandra\Prepared;
......@@ -17,7 +20,7 @@ class Sampler
/** @param User $user */
private $user;
/** @param HypthosesInterface $hypothesis */
/** @param HypothesisInterface $hypothesis */
private $hypothesis;
/** @param array $buckets */
......@@ -43,6 +46,7 @@ class Sampler
* Set the hypothesis to sample
* @param HypothesisInterface $hypothesis
* @return Sampler
* @throws \Exception
*/
public function setHypothesis($hypothesis)
{
......@@ -71,6 +75,7 @@ class Sampler
/**
* Return the bucket for a user
* @return Bucket
* @throws \Exception
*/
public function getBucket()
{
......
<?php
/**
* Logger
*
* @author edgebal
*/
namespace Minds\Core\Log;
use Exception;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ChromePHPHandler;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\FirePHPHandler;
use Monolog\Handler\PHPConsoleHandler;
use Monolog\Logger as MonologLogger;
use Sentry\Monolog\Handler as SentryHandler;
use Sentry\SentrySdk;
/**
* A PSR-3 logger tailored for Minds. Based off Monolog.
*
* @package Minds\Core\Log
* @see \Monolog\Logger
* @see \Psr\Log\LoggerInterface
*/
class Logger extends MonologLogger
{
/**
* Logger constructor.
* @param string $channel
* @param array $options
*/
public function __construct(string $channel = 'Minds', array $options = [])
{
$options = array_merge([
'isProduction' => true,
'devToolsLogger' => '',
], $options);
$handlers = [];
$errorLogHandler = new ErrorLogHandler(
ErrorLogHandler::OPERATING_SYSTEM,
$options['isProduction'] ? MonologLogger::INFO : MonologLogger::DEBUG,
true,
true
);
$errorLogHandler
->setFormatter(new LineFormatter(
"%channel%.%level_name%: %message% %context% %extra%\n",
'c',
!$options['isProduction'], // Allow newlines on dev mode
true
));
$handlers[] = $errorLogHandler;
if ($options['isProduction']) {
$handlers[] = new SentryHandler(SentrySdk::getCurrentHub());
} else {
// Extra handlers for Development Mode
switch ($options['devToolsLogger']) {
case 'firephp':
$handlers[] = new FirePHPHandler();
break;
case 'chromelogger':
$handlers[] = new ChromePHPHandler();
break;
case 'phpconsole':
try {
$handlers[] = new PHPConsoleHandler();
} catch (Exception $exception) {
// If the server-side vendor package is not installed, ignore any warnings.
}
break;
}
}
// Create Monolog instance
parent::__construct($channel, $handlers);
}
}
<?php
/**
* Module
*
* @author edgebal
*/
namespace Minds\Core\Log;
use Minds\Interfaces\ModuleInterface;
/**
* Module class
*
* @package Minds\Core\Log
*/
class Module implements ModuleInterface
{
/**
* @inheritDoc
*/
public function onInit(): void
{
(new Provider())->register();
}
}
<?php
/**
* Provider
*
* @author edgebal
*/
namespace Minds\Core\Log;
use Minds\Core\Config;
use Minds\Core\Di\Di;
use Minds\Core\Di\Provider as DiProvider;
/**
* DI Provider bindings
*
* @package Minds\Core\Log
*/
class Provider extends DiProvider
{
public function register()
{
$this->di->bind('Logger', function ($di) {
/** @var Di $di */
/** @var Config|false $config */
$config = $di->get('Config');
$options = [
'isProduction' => $config ? !$config->get('development_mode') : true,
'devToolsLogger' => $config ? $config->get('devtools_logger') : '',
];
return new Logger('Minds', $options);
}, [ 'useFactory' => false ]);
$this->di->bind('Logger\Singleton', function ($di) {
/** @var Di $di */
return $di->get('Logger');
}, [ 'useFactory' => true ]);
}
}
......@@ -16,6 +16,7 @@ class Minds extends base
public static $booted = false;
private $modules = [
Log\Module::class,
Events\Module::class,
SSO\Module::class,
Email\Module::class,
......
......@@ -921,7 +921,7 @@ CREATE TABLE minds.notifications (
CREATE MATERIALIZED VIEW minds.notifications_by_type_group AS
SELECT *
FROM minds.notifications
WHERE to_guid IS NOT NULL
WHERE to_guid IS NOT NULL
AND uuid IS NOT NULL
AND type_group IS NOT NULL
PRIMARY KEY (to_guid, type_group, uuid)
......@@ -1525,6 +1525,28 @@ CREATE TABLE minds.notification_batches (
primary key (user_guid, batch_id)
);
CREATE TABLE minds.experiments (
id text,
bucket text,
key text,
PRIMARY KEY (id, bucket, key)
) WITH CLUSTERING ORDER BY (bucket ASC, key ASC)
AND bloom_filter_fp_chance = 0.01
AND caching = {'keys': 'ALL', 'rows_per_partition': 'NONE'}
AND comment = ''
AND compaction = {'class': 'org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy', 'max_threshold': '32', 'min_threshold': '4'}
AND compression = {'chunk_length_in_kb': '64', 'class': 'org.apache.cassandra.io.compress.LZ4Compressor'}
AND crc_check_chance = 1.0
AND dclocal_read_repair_chance = 0.1
AND default_time_to_live = 0
AND gc_grace_seconds = 864000
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
AND read_repair_chance = 0.0
AND speculative_retry = '99PERCENTILE';
CREATE INDEX experiments_key_idx ON minds.experiments (key);
ALTER TABLE minds.withdrawals ADD (status text, address text, gas varint);
CREATE MATERIALIZED VIEW minds.withdrawals_by_status AS
......
......@@ -7,6 +7,8 @@
namespace Minds\Core\Router\Middleware\Kernel;
use Exception;
use Minds\Core\Di\Di;
use Minds\Core\Log\Logger;
use Minds\Core\Router\Exceptions\ForbiddenException;
use Minds\Core\Router\Exceptions\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
......@@ -15,21 +17,20 @@ use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use function Sentry\captureException;
class ErrorHandlerMiddleware implements MiddlewareInterface
{
/** @var bool */
protected $sentryEnabled = true;
/** @var Logger */
protected $logger;
/**
* @param bool $sentryEnabled
* @return ErrorHandlerMiddleware
* ErrorHandlerMiddleware constructor.
* @param Logger $logger
*/
public function setSentryEnabled(bool $sentryEnabled): ErrorHandlerMiddleware
{
$this->sentryEnabled = $sentryEnabled;
return $this;
public function __construct(
$logger = null
) {
$this->logger = $logger ?: Di::_()->get('Logger');
}
/**
......@@ -58,15 +59,7 @@ class ErrorHandlerMiddleware implements MiddlewareInterface
$status = 403;
} catch (Exception $e) {
// Log
// TODO: Monolog
error_log((string) $e);
// Sentry
if ($this->sentryEnabled) {
captureException($e);
}
$this->logger->critical($e, ['exception' => $e]);
}
switch ($request->getAttribute('accept')) {
......
<?php
/**
* Log
*
* @author edgebal
*/
namespace Minds\Helpers;
use Minds\Core\Di\Di;
use Psr\Log\InvalidArgumentException;
/**
* Static helper class for easy access to Logger singleton
* logging methods.
*
* @package Minds\Helpers
* @see \Minds\Core\Log\Logger
*/
class Log
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function emergency($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->emergency($message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function alert($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->alert($message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function critical($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->critical($message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function error($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->error($message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function warning($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->warning($message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function notice($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->notice($message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function info($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->info($message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public static function debug($message, array $context = [])
{
Di::_()->get('Logger\Singleton')->debug($message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*
* @throws InvalidArgumentException
*/
public static function log($level, $message, array $context = [])
{
Di::_()->get('Logger\Singleton')->log($level, $message, $context);
}
}
......@@ -166,92 +166,3 @@
}
}
}
{
"_source": [
"guid",
"owner_guid",
"@timestamp",
"time_created",
"access_id",
"moderator_guid"
],
"query": {
"function_score": {
"query": {
"bool": {
"must_not": [
{
"term": {
"deleted": true
}
},
{
"terms": {
"nsfw": [
1,
2,
3,
4,
5,
6
]
}
},
{
"term": {
"mature": true
}
}
],
"must": [
{
"terms": {
"access_id": [
"2"
]
}
},
{
"range": {
"@timestamp": {
"lte": 1576624259000,
"gt": 1576581059000
}
}
},
{
"exists": {
"field": "votes:up"
}
}
]
}
},
"score_mode": "sum",
"functions": [
{
"filter": {
"match_all": {}
},
"weight": 1
},
{
"script_score": {
"script": {
"source": "\n def up = (doc['votes:up'].value ?: 0) * 1.0;\n def down = (doc['votes:down'].value ?: 0) * 1.0;\n def magnitude = up + down;\n \n if (magnitude <= 0) {\n return -10;\n }\n \n def score = ((up + 1.9208) / (up + down) - 1.96 * Math.sqrt((up * down) / (up + down) + 0.9604) / (up + down)) / (1 + 3.8416 / (up + down));\n \n return score;\n "
}
}
}
]
}
},
"sort": [
{
"_score": {
"order": "desc"
}
}
]
}
\ No newline at end of file
......@@ -19,7 +19,7 @@ class ManagerSpec extends ObjectBehavior
public function it_should_return_a_list_of_experiments()
{
$this->getExperiments()->shouldHaveCount(2);
$this->getExperiments()->shouldHaveCount(3);
}
public function it_should_return_bucket_for_experiment(
......
......@@ -9,11 +9,23 @@ use Prophecy\Argument;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
class ErrorHandlerMiddlewareSpec extends ObjectBehavior
{
/** @var LoggerInterface */
protected $logger;
public function let(
LoggerInterface $logger
) {
$this->logger = $logger;
$this->beConstructedWith($logger);
}
public function it_is_initializable()
{
$this->shouldHaveType(ErrorHandlerMiddleware::class);
......@@ -29,7 +41,6 @@ class ErrorHandlerMiddlewareSpec extends ObjectBehavior
->willReturn($response);
$this
->setSentryEnabled(false)
->process($request, $handler)
->shouldReturn($response);
}
......@@ -39,16 +50,21 @@ class ErrorHandlerMiddlewareSpec extends ObjectBehavior
RequestHandlerInterface $handler,
ResponseInterface $response
) {
$exception = new Exception('PHPSpec');
$handler->handle($request)
->shouldBeCalled()
->willThrow(new Exception('PHPSpec'));
->willThrow($exception);
$this->logger->critical($exception, ['exception' => $exception])
->shouldBeCalled()
->willReturn(null);
$request->getAttribute('accept')
->shouldBeCalled()
->willReturn('html');
$this
->setSentryEnabled(false)
->process($request, $handler)
->shouldBeAnInstanceOf(HtmlResponse::class);
}
......@@ -58,16 +74,21 @@ class ErrorHandlerMiddlewareSpec extends ObjectBehavior
RequestHandlerInterface $handler,
ResponseInterface $response
) {
$exception = new Exception('PHPSpec');
$handler->handle($request)
->shouldBeCalled()
->willThrow(new Exception('PHPSpec'));
->willThrow($exception);
$this->logger->critical($exception, ['exception' => $exception])
->shouldBeCalled()
->willReturn(null);
$request->getAttribute('accept')
->shouldBeCalled()
->willReturn('json');
$this
->setSentryEnabled(false)
->process($request, $handler)
->shouldBeAnInstanceOf(JsonResponse::class);
}
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6c23473ac0e2036a05166b78c51a6080",
"content-hash": "017bf76d590078e33e984171db408c4a",
"packages": [
{
"name": "Minds/Surge",
......@@ -1903,16 +1903,16 @@
},
{
"name": "monolog/monolog",
"version": "1.25.1",
"version": "1.25.2",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf"
"reference": "d5e2fb341cb44f7e2ab639d12a1e5901091ec287"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
"reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/d5e2fb341cb44f7e2ab639d12a1e5901091ec287",
"reference": "d5e2fb341cb44f7e2ab639d12a1e5901091ec287",
"shasum": ""
},
"require": {
......@@ -1977,7 +1977,7 @@
"logging",
"psr-3"
],
"time": "2019-09-06T13:49:17+00:00"
"time": "2019-11-13T10:00:05+00:00"
},
{
"name": "mtdowling/jmespath.php",
......
......@@ -839,8 +839,14 @@ function elgg_trigger_plugin_hook($hook, $type, $params = null, $returnvalue = n
* @access private
*/
function _elgg_php_exception_handler($exception) {
$timestamp = time();
error_log("Exception #$timestamp: $exception");
try {
\Minds\Helpers\Log::critical($exception, [ 'exception' => $exception ]);
} catch (Exception $loggerException) {
$timestamp = time();
error_log("Exception #$timestamp: $exception");
Sentry\captureException($exception);
}
// Wipe any existing output buffer
ob_end_clean();
......@@ -850,8 +856,6 @@ function _elgg_php_exception_handler($exception) {
header("Cache-Control: no-cache, must-revalidate", true);
header('Expires: Fri, 05 Feb 1982 00:00:00 -0500', true);
// @note Do not send a 500 header because it is not a server error
Sentry\captureException($exception);
}
/**
......
......@@ -483,6 +483,9 @@ $CONFIG->set('features', [
'pro' => false,
'webtorrent' => false,
'top-feeds-by-age' => true,
'homepage-december-2019' => true,
'onboarding-december-2019' => true,
'register_pages-december-2019' => true,
]);
$CONFIG->set('email', [
......
......@@ -10,7 +10,7 @@ rm -rf ../vendor
# Setup composer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('SHA384', 'composer-setup.php') === 'baf1608c33254d00611ac1705c1d9958c817a1a33bce370c0595974b342601bd80b92a3f46067da89e3b06bff421f182') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php -r "if (hash_file('SHA384', 'composer-setup.php') === 'c5b9b6d368201a9db6f74e2611495f369991b72d9c8cbd3ffbc63edff210eb73d46ffbfce88669ad33695ef77dc76976') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
......
Please register or to comment